feat(theme): implement single-file CSS theming engine with dark, light, high-contrast, and soft themes
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 16s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 16s
This commit is contained in:
1 parent
0f24cbdbf4
commit
b3e651b3b9
18 files changed
+1453
-802
No files matched your search
@@ -9,10 +9,16 @@
|
||||
- [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.
|
||||
- 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`):**
|
||||
- Zentrale CSS-Variablen (`--bg-app`, `--bg-panel`, `--accent-primary`, `--border-color`, `--radius-card: 0px`) in `frontend/src/index.css`.
|
||||
- Eckige Tabs, eckige Panels/Karten (`0px` Radius), nur die Schnell-Eingabe-Blase oben im Header und Profil-Avatare sind rund (`rounded-full`).
|
||||
- Untere doppelte Navigationsleiste entfernt; Schnell-Notiz-Blase (`InstantCaptureBar`) sitzt ganz oben im Header über den Tabs.
|
||||
- [x] **Single-File CSS Theming Engine & Design-System (`frontend/src/themes/`, `theme.ts`, `tailwind.config.js`):**
|
||||
- Jedes Theme ist als **genau eine einzige, eigenständige CSS-Datei** unter `frontend/src/themes/` definiert.
|
||||
- Alle Farben, Kontraste, Oberflächen, Rahmen, Statusfarben und Rundungen sind in 6 klaren Gruppen organisiert.
|
||||
- 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).
|
||||
- [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.
|
||||
@@ -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.
|
||||
- [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ü:
|
||||
- **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).
|
||||
- **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).
|
||||
@@ -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
|
||||
:root {
|
||||
/* Monochromer Hintergrund (Dunkles Schiefer/Anthrazit) */
|
||||
--bg-app: #121316;
|
||||
--bg-panel: #1a1c20;
|
||||
--bg-panel-elevated: #22252a;
|
||||
--bg-panel-hover: #282c33;
|
||||
|
||||
/* Einzige Akzentfarbe + hellere/gedimmte Variante */
|
||||
--accent-primary: #2563eb;
|
||||
:root[data-theme="dein-theme"] {
|
||||
/* ==========================================================================
|
||||
1. AKZENTFARBE (Fokus, Primäraktionen, Highlights)
|
||||
========================================================================== */
|
||||
--accent: #2563eb;
|
||||
--accent-hover: #3b82f6;
|
||||
--accent-dim: rgba(37, 99, 235, 0.14);
|
||||
--accent-border: rgba(59, 130, 246, 0.45);
|
||||
--accent-dim: rgba(37, 99, 235, 0.15);
|
||||
--accent-contrast: #ffffff;
|
||||
|
||||
/* Kanten & Linien */
|
||||
--border-color: rgba(255, 255, 255, 0.09);
|
||||
--border-subtle: rgba(255, 255, 255, 0.05);
|
||||
/* ==========================================================================
|
||||
2. OBERFLÄCHEN & HINTERGRÜNDE
|
||||
========================================================================== */
|
||||
--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-btn: 2px;
|
||||
--radius-input: 2px;
|
||||
--radius-checkbox: 2px;
|
||||
--radius-pill: 9999px;
|
||||
}
|
||||
```
|
||||
|
||||
### Zentrale Komponenten-Klassen
|
||||
- `.panel-surface` / `.panel-elevated`: Eckige Container mit `1px solid var(--border-color)`.
|
||||
- `.btn-action`: Monochromer, kompakter Button (`border-radius: var(--radius-btn)`).
|
||||
- `.btn-primary`: Akzent-Button auf `--accent-primary`.
|
||||
- `.input-field`: Minimal gerundetes (`2px`) Eingabefeld für Eigenschaften/Optionen.
|
||||
- `.tab-btn` / `.tab-btn-active`: Eckige, monochrome Header-Tabs.
|
||||
- `.badge-accent`: Kompaktes Akzent-Badge für Zähler und Tags.
|
||||
- **Ausnahme für Rundungen (`rounded-full`):** Nur die Schnell-Eingabe-Blase (`InstantCaptureBar`) im Header, der Profil-Avatar und Toggle-Switches sind rund.
|
||||
### Verfügbare Themes
|
||||
|
||||
| Theme-ID | Datei | Charakter | Standard-Geometrie |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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`**
|
||||
- `id TEXT PRIMARY KEY`, `email TEXT UNIQUE`, `password_hash TEXT`, `display_name TEXT`
|
||||
- `role TEXT DEFAULT 'member'` (`'admin'` | `'member'`)
|
||||
- `is_active INTEGER DEFAULT 1`
|
||||
- `discord_webhook_url TEXT DEFAULT ''`
|
||||
- `notify_before_minutes INTEGER DEFAULT 15`
|
||||
- `created_at DATETIME`, `updated_at DATETIME`
|
||||
|
||||
2. **`thoughts` (Quick Dumps, Triage & Aufgaben)**
|
||||
- `id TEXT PRIMARY KEY`, `user_id TEXT`, `content TEXT`, `source TEXT`, `priority TEXT`
|
||||
- `status TEXT` (`'inbox'` | `'converted'` | `'discarded'`)
|
||||
- `converted_type TEXT` (`'task'` | `'document'` | `'list_item'`)
|
||||
- `converted_ref_id TEXT` (Referenz auf Document oder ListItem)
|
||||
- `task_list_id TEXT` (FK auf `task_lists.id`)
|
||||
- `kanban_status TEXT DEFAULT 'todo'` (`'todo'` | `'in_progress'` | `'done'`)
|
||||
- `notes TEXT DEFAULT ''` (zusätzliche Aufgabenbeschreibung/Notizen)
|
||||
- `due_date DATETIME` (Fälligkeitsdatum)
|
||||
- `alarm_sent INTEGER DEFAULT 0` (Flag für Discord-Benachrichtigung)
|
||||
- `created_at DATETIME`, `updated_at DATETIME`
|
||||
|
||||
3. **`task_lists` (Mehrere Aufgabenlisten)**
|
||||
- `id TEXT PRIMARY KEY`, `user_id TEXT`, `space_id TEXT` (optional für Team-Sharing)
|
||||
- `name TEXT NOT NULL`, `color TEXT DEFAULT '#2563eb'`, `icon TEXT DEFAULT 'list'`
|
||||
- `is_default INTEGER DEFAULT 0`, `created_at DATETIME`, `updated_at DATETIME`
|
||||
|
||||
4. **`organizations` & `teams` & `team_members`**
|
||||
- `organizations`: `id TEXT PRIMARY KEY`, `name TEXT`, `owner_id TEXT`, `created_at DATETIME`, `updated_at DATETIME`
|
||||
- `teams`: `id TEXT PRIMARY KEY`, `organization_id TEXT`, `name TEXT`, `created_at DATETIME`
|
||||
- `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.
|
||||
- `role TEXT DEFAULT 'member'` (`admin`, `team_admin`, `member`)
|
||||
- `is_active INTEGER DEFAULT 1`, `created_at DATETIME`, `updated_at DATETIME`
|
||||
2. **`system_settings`** (Instanzweite globale Konfiguration, 1 Row mit `id = 'singleton'`)
|
||||
- `sharing_mode TEXT DEFAULT 'teams_only'` (`strict_private`, `teams_only`, `open`)
|
||||
- `allow_registration INTEGER DEFAULT 0` (Öffentliche Registrierung erlaubt oder gesperrt)
|
||||
- `instance_name TEXT DEFAULT 'MindSift'`
|
||||
- `smtp_host TEXT`, `smtp_port INTEGER`, `smtp_user TEXT`, `smtp_pass TEXT`, `smtp_from TEXT`, `smtp_tls INTEGER`
|
||||
3. **`user_invites`** (Einladungscodes für neue Registrierungen)
|
||||
- `id TEXT PRIMARY KEY`, `code TEXT UNIQUE` (`MS-XXXXXXXX`), `email TEXT` (optional gebunden)
|
||||
- `role TEXT DEFAULT 'member'`, `team_id TEXT` (optional direkte Team-Zuweisung)
|
||||
- `created_by TEXT`, `used_by TEXT`, `expires_at DATETIME`, `created_at DATETIME`
|
||||
4. **`organizations` & `organization_teams` & `team_members`**
|
||||
- Strukturierung von Benutzern in Abteilungen, Familien oder Projekt-Teams.
|
||||
- Bildet die Basis für den Sichtbarkeits-Modus `teams_only`.
|
||||
5. **`spaces` & `space_members`**
|
||||
- Geteilte Kollaborations-Räume mit Einladungscode (`invite_code`), Echtzeit-WebSocket Sync und Rollen (`owner`, `editor`, `viewer`).
|
||||
6. **`shared_lists` & `list_items`**
|
||||
- Geteilte Einkaufs-, Pack- und Inventarlisten mit Kategorien, Mengen und Echtzeit-Abhaken.
|
||||
7. **`tasks` & `task_lists`**
|
||||
- Persönliche und geteilte Aufgabenlisten mit Kanban-Spalten (`todo`, `in_progress`, `done`), Fälligkeit, Priorität und Alarm-Optionen.
|
||||
8. **`quick_dumps` (Triage-Deck)**
|
||||
- Schnelle Einfälle (`status = 'inbox'`, `'discarded'`, `'converted'`), Fälligkeitsdatum und Text-Notiz.
|
||||
9. **`documents` (Workspace Markdown-Wiki)**
|
||||
- Notizen und Dokumentationen mit Folder-Pfad (`folder_path`), Tags, Pinned-Status, JSON-Blocks und physischer `.md`-Spiegelung auf Disk.
|
||||
10. **`user_alarms` & `user_webhooks`**
|
||||
- Multi-Channel Alarme (Mail, Discord, Webhook) und ausgehende Event-Webhooks.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 3. State-Machine: Quick Dump, Direkt-Zuordnung & Triage
|
||||
## ⚡ 3. Triage & Quick-Dump State Machine
|
||||
|
||||
```
|
||||
[ HEADER QUICK-CAPTURE BAR ]
|
||||
│
|
||||
┌───────────────────────┼───────────────────────┐
|
||||
│ Standard (Enter) │ Direkt-Option gewählt │
|
||||
▼ ▼ ▼
|
||||
status: 'inbox' status: 'converted' status: 'converted'
|
||||
(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)
|
||||
```
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> InstantCapture: Gedanke eingetippt
|
||||
InstantCapture --> QuickDump_Inbox: Enter (Standard)
|
||||
InstantCapture --> TaskCreated: Ziel = 'task'
|
||||
InstantCapture --> ListItemCreated: Ziel = 'list_item'
|
||||
InstantCapture --> DocCreated: Ziel = 'document'
|
||||
|
||||
### API-Endpunkt für Capture (`POST /api/v1/inbox`)
|
||||
Akzeptiert:
|
||||
- `content`: Fließtext des Gedankens
|
||||
- `priority`: `'low'` | `'medium'` | `'high'`
|
||||
- `target`: `'inbox'` (Standard) | `'task'` | `'list_item'` | `'document'`
|
||||
- `taskListId`: ID der Aufgabenliste (wenn `target === 'task'`)
|
||||
- `targetListId`: ID der Zielliste (wenn `target === 'list_item'`)
|
||||
- `dueDate`: Optionales ISO-Datum
|
||||
QuickDump_Inbox --> TriageDeck: Stapel sichten
|
||||
TriageDeck --> Discarded: Swipe links / H (Wegwerfen)
|
||||
TriageDeck --> TaskCreated: Swipe rechts / L (Als Aufgabe)
|
||||
TriageDeck --> TaskCreated: T (Termin-Aufgabe)
|
||||
TriageDeck --> DocCreated: D (In Notiz umwandeln)
|
||||
TriageDeck --> ListItemCreated: Auf Liste schieben
|
||||
TriageDeck --> QuickDump_Inbox: Leertaste (Später - ans Ende)
|
||||
|
||||
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`):**
|
||||
- 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.
|
||||
Der Server-Administrator (`role = 'admin'`) legt unter `AdminSettingsView.tsx` global fest, wie Benutzer miteinander interagieren dürfen:
|
||||
|
||||
2. **Eingehende Webhooks / Private API-Tokens:**
|
||||
- Erreichbar unter `POST /api/v1/inbox` mit Header `X-API-Key: ms_...`.
|
||||
- Ermöglicht direkte Einbindung in Shell-Skripte, Crons oder Drittsysteme via `curl`.
|
||||
1. **`strict_private` (Strikt Privat):**
|
||||
- Jeder Benutzer arbeitet komplett autark. Keine teamübergreifende Benutzersuche, keine geteilten Spaces.
|
||||
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
@@ -10,9 +10,16 @@ import {
|
||||
LogOut,
|
||||
ChevronDown,
|
||||
RotateCcw,
|
||||
Palette,
|
||||
} from 'lucide-react';
|
||||
import { AuthStatus, DashboardStats, SharedList, Thought, User } from './lib/types';
|
||||
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 { TriageDeck } from './features/triage/TriageDeck';
|
||||
import { TasksView } from './features/triage/TasksView';
|
||||
@@ -51,9 +58,9 @@ class ViewErrorBoundary extends React.Component<
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="p-6 bg-panel border border-border 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>
|
||||
<p className="text-zinc-400">{this.state.errorText}</p>
|
||||
<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-main">Ansicht konnte nicht geladen werden</h3>
|
||||
<p className="text-muted">{this.state.errorText}</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
this.setState({ hasError: false, errorText: '' });
|
||||
@@ -72,6 +79,7 @@ class ViewErrorBoundary extends React.Component<
|
||||
}
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const [activeTheme, setActiveThemeState] = useState<ThemeId>(() => initThemeOnBoot());
|
||||
const [bootLoading, setBootLoading] = useState(true);
|
||||
const [authStatus, setAuthStatus] = useState<AuthStatus>({
|
||||
needsSetup: false,
|
||||
@@ -98,6 +106,12 @@ export const App: React.FC = () => {
|
||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const handleChangeTheme = (themeId: ThemeId) => {
|
||||
triggerHaptic(10);
|
||||
applyTheme(themeId);
|
||||
setActiveThemeState(themeId);
|
||||
};
|
||||
|
||||
const refreshAll = async () => {
|
||||
try {
|
||||
const [meRes, deckRes, listsRes] = await Promise.all([
|
||||
@@ -228,7 +242,7 @@ export const App: React.FC = () => {
|
||||
|
||||
if (bootLoading) {
|
||||
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…
|
||||
</div>
|
||||
);
|
||||
@@ -253,9 +267,9 @@ export const App: React.FC = () => {
|
||||
: 'Mitglied';
|
||||
|
||||
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 */}
|
||||
<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">
|
||||
{/* Logo & Headline */}
|
||||
<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"
|
||||
>
|
||||
<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" />
|
||||
</div>
|
||||
<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'}
|
||||
</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
|
||||
</span>
|
||||
</div>
|
||||
@@ -294,29 +308,57 @@ export const App: React.FC = () => {
|
||||
triggerHaptic(10);
|
||||
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"
|
||||
>
|
||||
<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'}
|
||||
</div>
|
||||
<span className="max-w-[120px] truncate hidden sm:inline">
|
||||
{user.displayName}
|
||||
</span>
|
||||
<ChevronDown className="w-3.5 h-3.5 text-zinc-400" />
|
||||
<ChevronDown className="w-3.5 h-3.5 text-muted" />
|
||||
</button>
|
||||
|
||||
{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 */}
|
||||
<div className="p-3 bg-canvas">
|
||||
<p className="font-bold text-white truncate">{user.displayName}</p>
|
||||
<p className="text-[11px] text-zinc-400 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">
|
||||
<p className="font-bold text-main truncate">{user.displayName}</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 rounded-btn text-[10px] text-accent font-semibold">
|
||||
{roleLabel}
|
||||
</span>
|
||||
</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 */}
|
||||
<div className="py-1">
|
||||
<button
|
||||
@@ -326,7 +368,7 @@ export const App: React.FC = () => {
|
||||
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 ${
|
||||
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" />
|
||||
@@ -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 ${
|
||||
activeTab === 'admin_settings'
|
||||
? 'text-accent font-semibold'
|
||||
: 'text-zinc-200'
|
||||
: 'text-main'
|
||||
}`}
|
||||
>
|
||||
<Shield className="w-4 h-4 text-accent" />
|
||||
@@ -356,7 +398,7 @@ export const App: React.FC = () => {
|
||||
<div className="py-1">
|
||||
<button
|
||||
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" />
|
||||
<span>Abmelden</span>
|
||||
@@ -367,7 +409,7 @@ export const App: React.FC = () => {
|
||||
</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">
|
||||
<div className="flex items-stretch">
|
||||
<button
|
||||
@@ -384,7 +426,7 @@ export const App: React.FC = () => {
|
||||
className={
|
||||
activeTab === 'triage'
|
||||
? 'badge-accent'
|
||||
: 'px-1.5 py-0.5 text-[10px] bg-canvas text-zinc-400 border border-border'
|
||||
: 'badge-neutral'
|
||||
}
|
||||
>
|
||||
{stats.inboxCount}
|
||||
@@ -406,7 +448,7 @@ export const App: React.FC = () => {
|
||||
className={
|
||||
activeTab === 'tasks'
|
||||
? 'badge-accent'
|
||||
: 'px-1.5 py-0.5 text-[10px] bg-canvas text-zinc-400 border border-border'
|
||||
: 'badge-neutral'
|
||||
}
|
||||
>
|
||||
{stats.tasksOpenCount}
|
||||
@@ -427,7 +469,7 @@ export const App: React.FC = () => {
|
||||
className={
|
||||
activeTab === 'workspace'
|
||||
? 'badge-accent'
|
||||
: 'px-1.5 py-0.5 text-[10px] bg-canvas text-zinc-400 border border-border'
|
||||
: 'badge-neutral'
|
||||
}
|
||||
>
|
||||
{stats.docsCount}
|
||||
@@ -484,6 +526,8 @@ export const App: React.FC = () => {
|
||||
{activeTab === 'user_settings' && (
|
||||
<UserSettingsView
|
||||
currentUser={user}
|
||||
activeTheme={activeTheme}
|
||||
onThemeChange={handleChangeTheme}
|
||||
onUserUpdated={(u) => {
|
||||
setUser(u);
|
||||
refreshAll();
|
||||
|
||||
@@ -28,7 +28,9 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
const [setupEmail, setSetupEmail] = useState('');
|
||||
const [setupPassword, setSetupPassword] = useState('');
|
||||
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 [createSampleData, setCreateSampleData] = useState(false);
|
||||
|
||||
@@ -101,52 +103,50 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
// ============================================================================
|
||||
if (authStatus.needsSetup) {
|
||||
return (
|
||||
<div className="min-h-screen bg-canvas text-white 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="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 panel-surface shadow-panel overflow-hidden">
|
||||
{/* Top Brand Bar */}
|
||||
<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="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" />
|
||||
</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
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="px-2 py-0.5 bg-accent/15 border border-accent text-accent text-[10px] font-semibold uppercase tracking-wider">
|
||||
First Boot
|
||||
</span>
|
||||
<span className="badge-accent uppercase tracking-wider">First Boot</span>
|
||||
</div>
|
||||
|
||||
<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
|
||||
der Datenbank. Lege jetzt das erste <strong>Server-Admin-Konto</strong> an und bestimme
|
||||
die Sicherheitseinstellungen.
|
||||
der Datenbank. Lege jetzt das erste <strong className="text-main">Server-Admin-Konto</strong> an und
|
||||
bestimme die Sicherheitseinstellungen.
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-red-950/40 border border-red-500 text-red-200 flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" />
|
||||
<div className="alert-err flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-status-err flex-shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section 1: Admin Account */}
|
||||
<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" />
|
||||
<span>1. Server-Administrator anlegen</span>
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-zinc-400 mb-1">Dein Anzeigename *</label>
|
||||
<label className="block text-muted mb-1">Dein Anzeigename *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={setupName}
|
||||
@@ -154,24 +154,24 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
placeholder="z.B. Admin / Dein Name"
|
||||
required
|
||||
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>
|
||||
<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
|
||||
type="email"
|
||||
value={setupEmail}
|
||||
onChange={(e) => setSetupEmail(e.target.value)}
|
||||
placeholder="admin@deinedomain.de"
|
||||
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>
|
||||
<label className="block text-zinc-400 mb-1">Administrator-Passwort *</label>
|
||||
<label className="block text-muted mb-1">Administrator-Passwort *</label>
|
||||
<input
|
||||
type="password"
|
||||
value={setupPassword}
|
||||
@@ -179,31 +179,31 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
placeholder="Mindestens 4 Zeichen"
|
||||
minLength={4}
|
||||
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>
|
||||
|
||||
{/* Section 2: Server & Isolation */}
|
||||
<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" />
|
||||
<span>2. Instanz & Berechtigungen</span>
|
||||
</h2>
|
||||
|
||||
<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
|
||||
type="text"
|
||||
value={instanceName}
|
||||
onChange={(e) => setInstanceName(e.target.value)}
|
||||
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>
|
||||
<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" />
|
||||
<span>Sichtbarkeit & Sharing-Isolation zwischen Benutzern</span>
|
||||
</label>
|
||||
@@ -213,12 +213,12 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
onClick={() => setSharingMode('strict_private')}
|
||||
className={`p-2.5 text-left border transition-colors ${
|
||||
sharingMode === 'strict_private'
|
||||
? 'bg-panel border-accent text-white'
|
||||
: 'border-border text-zinc-400 hover:text-white'
|
||||
? 'bg-panel border-accent text-main'
|
||||
: 'border-border text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<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.
|
||||
</p>
|
||||
</button>
|
||||
@@ -228,12 +228,12 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
onClick={() => setSharingMode('teams_only')}
|
||||
className={`p-2.5 text-left border transition-colors ${
|
||||
sharingMode === 'teams_only'
|
||||
? 'bg-panel border-accent text-white'
|
||||
: 'border-border text-zinc-400 hover:text-white'
|
||||
? 'bg-panel border-accent text-main'
|
||||
: 'border-border text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<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.
|
||||
</p>
|
||||
</button>
|
||||
@@ -243,12 +243,12 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
onClick={() => setSharingMode('open')}
|
||||
className={`p-2.5 text-left border transition-colors ${
|
||||
sharingMode === 'open'
|
||||
? 'bg-panel border-accent text-white'
|
||||
: 'border-border text-zinc-400 hover:text-white'
|
||||
? 'bg-panel border-accent text-main'
|
||||
: 'border-border text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<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.
|
||||
</p>
|
||||
</button>
|
||||
@@ -258,11 +258,12 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
<div className="pt-2 border-t border-border space-y-2">
|
||||
<label className="flex items-center justify-between cursor-pointer select-none">
|
||||
<div>
|
||||
<span className="font-medium text-white block">
|
||||
<span className="font-medium text-main block">
|
||||
Öffentliche Selbstregistrierung erlauben
|
||||
</span>
|
||||
<span className="text-[11px] text-zinc-400">
|
||||
Wenn deaktiviert, können neue Nutzer nur per Einladungscode oder durch einen Admin angelegt werden.
|
||||
<span className="text-[11px] text-muted">
|
||||
Wenn deaktiviert, können neue Nutzer nur per Einladungscode oder durch einen
|
||||
Admin angelegt werden.
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
@@ -275,10 +276,10 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
|
||||
<label className="flex items-center justify-between cursor-pointer select-none pt-1">
|
||||
<div>
|
||||
<span className="font-medium text-white block">
|
||||
<span className="font-medium text-main block">
|
||||
Start-Beispieldaten anlegen
|
||||
</span>
|
||||
<span className="text-[11px] text-zinc-400">
|
||||
<span className="text-[11px] text-muted">
|
||||
Erstellt wenige Beispiel-Gedanken und ein Einführungsdokument zum Testen.
|
||||
</span>
|
||||
</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"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -312,24 +317,22 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
// STANDARD LOGIN / INVITE CODE / REGISTRATION VIEW
|
||||
// ============================================================================
|
||||
return (
|
||||
<div className="min-h-screen bg-canvas text-white 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="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 panel-surface shadow-panel overflow-hidden">
|
||||
{/* Top Header */}
|
||||
<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="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" />
|
||||
</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'}
|
||||
</h1>
|
||||
<p className="text-[11px] text-zinc-400 mt-0.5">
|
||||
Der Kopf frei in drei Sekunden
|
||||
</p>
|
||||
<p className="text-[11px] text-muted mt-0.5">Der Kopf frei in drei Sekunden</p>
|
||||
</div>
|
||||
</div>
|
||||
<Lock className="w-4 h-4 text-zinc-500" />
|
||||
<Lock className="w-4 h-4 text-faint" />
|
||||
</div>
|
||||
|
||||
{/* 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 ${
|
||||
authTab === 'login'
|
||||
? 'bg-panel text-white border-b-2 border-b-accent'
|
||||
: 'text-zinc-400 hover:text-white'
|
||||
? 'bg-panel text-main border-b-2 border-b-accent'
|
||||
: 'text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<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 ${
|
||||
authTab === 'invite'
|
||||
? 'bg-panel text-white border-b-2 border-b-accent'
|
||||
: 'text-zinc-400 hover:text-white'
|
||||
? 'bg-panel text-main border-b-2 border-b-accent'
|
||||
: 'text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<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 ${
|
||||
authTab === 'register'
|
||||
? 'bg-panel text-white border-b-2 border-b-accent'
|
||||
: 'text-zinc-400 hover:text-white'
|
||||
? 'bg-panel text-main border-b-2 border-b-accent'
|
||||
: 'text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<UserPlus className="w-3.5 h-3.5" />
|
||||
@@ -388,8 +391,8 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
{/* Form Body */}
|
||||
<div className="p-5 text-xs">
|
||||
{error && (
|
||||
<div className="mb-4 p-2.5 bg-red-950/40 border border-red-500 text-red-200 flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" />
|
||||
<div className="mb-4 alert-err flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 text-status-err flex-shrink-0" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -397,7 +400,7 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
{authTab === 'login' && (
|
||||
<form onSubmit={handleLogin} className="space-y-3.5">
|
||||
<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
|
||||
type="email"
|
||||
value={email}
|
||||
@@ -405,19 +408,19 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
placeholder="name@beispiel.de"
|
||||
required
|
||||
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>
|
||||
<label className="block text-zinc-400 mb-1">Passwort</label>
|
||||
<label className="block text-muted mb-1">Passwort</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
className="w-full h-8 px-2.5 input-field text-white"
|
||||
className="w-full h-8 px-2.5 input-field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -433,7 +436,7 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
</div>
|
||||
|
||||
{!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{' '}
|
||||
<button
|
||||
type="button"
|
||||
@@ -452,7 +455,7 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
<form onSubmit={handleRegisterOrInvite} className="space-y-3.5">
|
||||
{authTab === 'invite' && (
|
||||
<div>
|
||||
<label className="block text-zinc-400 mb-1">Einladungscode *</label>
|
||||
<label className="block text-muted mb-1">Einladungscode *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={inviteCode}
|
||||
@@ -460,37 +463,37 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
placeholder="MS-XXXXXXXX"
|
||||
required
|
||||
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>
|
||||
<label className="block text-zinc-400 mb-1">Anzeigename *</label>
|
||||
<label className="block text-muted mb-1">Anzeigename *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="Dein Name"
|
||||
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>
|
||||
<label className="block text-zinc-400 mb-1">E-Mail-Adresse *</label>
|
||||
<label className="block text-muted mb-1">E-Mail-Adresse *</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="name@beispiel.de"
|
||||
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>
|
||||
<label className="block text-zinc-400 mb-1">Passwort *</label>
|
||||
<label className="block text-muted mb-1">Passwort *</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
@@ -498,7 +501,7 @@ export const SetupAndAuthView: React.FC<SetupAndAuthViewProps> = ({
|
||||
placeholder="Mindestens 4 Zeichen"
|
||||
minLength={4}
|
||||
required
|
||||
className="w-full h-8 px-2.5 input-field text-white"
|
||||
className="w-full h-8 px-2.5 input-field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -210,15 +210,15 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
return (
|
||||
<div className="w-full">
|
||||
<form ref={containerRef} onSubmit={handleSubmit} className="relative">
|
||||
{/* Round Quick-Capture Input Bar */}
|
||||
{/* Quick-Capture Input Bar (Radius controlled by --radius-pill) */}
|
||||
<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
|
||||
? 'border-accent bg-panel'
|
||||
: '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" />
|
||||
</div>
|
||||
|
||||
@@ -244,7 +244,7 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
? 'Neue Workspace-Notiz direkt anlegen…'
|
||||
: '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"
|
||||
/>
|
||||
|
||||
@@ -254,10 +254,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
type="button"
|
||||
onClick={() => setShowOptions((prev) => !prev)}
|
||||
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
|
||||
? 'bg-accent/20 text-accent border border-accent/40 font-medium'
|
||||
: 'text-zinc-400 hover:text-white hover:bg-panel-elevated border border-transparent'
|
||||
? 'bg-accent-dim text-accent border border-accent-border font-medium'
|
||||
: 'text-muted hover:text-main hover:bg-panel-elevated border border-transparent'
|
||||
}`}
|
||||
>
|
||||
{target === 'task' ? (
|
||||
@@ -281,10 +281,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
type="button"
|
||||
onClick={toggleVoiceDictation}
|
||||
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
|
||||
? 'bg-accent/20 text-accent'
|
||||
: 'text-zinc-400 hover:text-white hover:bg-panel-elevated'
|
||||
? 'bg-accent-dim text-accent'
|
||||
: 'text-muted hover:text-main hover:bg-panel-elevated'
|
||||
}`}
|
||||
>
|
||||
{isListening ? <MicOff className="w-3 h-3" /> : <Mic className="w-3 h-3" />}
|
||||
@@ -294,10 +294,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
<button
|
||||
type="submit"
|
||||
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()
|
||||
? 'bg-accent hover:bg-accent-hover text-white cursor-pointer shadow-sm'
|
||||
: 'bg-panel-elevated text-zinc-500 cursor-not-allowed'
|
||||
? 'bg-accent hover:bg-accent-hover text-on-accent cursor-pointer shadow-sm'
|
||||
: 'bg-panel-elevated text-faint cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{justSavedLabel ? (
|
||||
@@ -323,16 +323,16 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expandable Target & Metadata Panel (Square Monochromatic Dropdown) */}
|
||||
{/* Expandable Target & Metadata Panel (Theme-controlled Dropdown) */}
|
||||
{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? */}
|
||||
<div>
|
||||
<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)'}
|
||||
</span>
|
||||
<span className="text-[11px] text-zinc-500">
|
||||
<span className="text-[11px] text-faint">
|
||||
Standard: Quick Dump für späteres Swipen
|
||||
</span>
|
||||
</div>
|
||||
@@ -347,10 +347,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
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'
|
||||
? 'bg-accent/20 border-accent text-white font-medium'
|
||||
: 'bg-canvas border-border text-zinc-300 hover:bg-panel-hover'
|
||||
? 'bg-accent-dim border-accent text-main font-medium'
|
||||
: '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" />
|
||||
@@ -366,10 +366,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
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'
|
||||
? 'bg-accent/20 border-accent text-white font-medium'
|
||||
: 'bg-canvas border-border text-zinc-300 hover:bg-panel-hover'
|
||||
? 'bg-accent-dim border-accent text-main font-medium'
|
||||
: '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" />
|
||||
@@ -385,10 +385,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
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'
|
||||
? 'bg-accent/20 border-accent text-white font-medium'
|
||||
: 'bg-canvas border-border text-zinc-300 hover:bg-panel-hover'
|
||||
? 'bg-accent-dim border-accent text-main font-medium'
|
||||
: '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" />
|
||||
@@ -404,10 +404,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
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'
|
||||
? 'bg-accent/20 border-accent text-white font-medium'
|
||||
: 'bg-canvas border-border text-zinc-300 hover:bg-panel-hover'
|
||||
? 'bg-accent-dim border-accent text-main font-medium'
|
||||
: '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" />
|
||||
@@ -416,10 +416,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
</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 && (
|
||||
<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" />
|
||||
Liste:
|
||||
</span>
|
||||
@@ -436,10 +436,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
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
|
||||
? 'bg-accent text-white border-accent font-medium'
|
||||
: 'bg-canvas border-border text-zinc-300 hover:text-white hover:bg-panel-hover'
|
||||
? 'bg-accent text-on-accent border-accent font-medium'
|
||||
: 'bg-canvas border-border text-muted hover:text-main hover:bg-panel-hover'
|
||||
}`}
|
||||
>
|
||||
<span>{lst.icon}</span>
|
||||
@@ -453,7 +453,7 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
{/* Row 2: Priority & Due Date */}
|
||||
<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">
|
||||
<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) => {
|
||||
const labels: Record<PriorityLevel, string> = {
|
||||
low: 'Niedrig',
|
||||
@@ -467,10 +467,10 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
key={p}
|
||||
type="button"
|
||||
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
|
||||
? 'bg-accent text-white border-accent font-medium'
|
||||
: 'bg-canvas text-zinc-400 hover:text-white border-border'
|
||||
? 'bg-accent text-on-accent border-accent font-medium'
|
||||
: 'bg-canvas text-muted hover:text-main border-border'
|
||||
}`}
|
||||
>
|
||||
{labels[p]}
|
||||
@@ -480,7 +480,7 @@ export const InstantCaptureBar: React.FC<InstantCaptureBarProps> = ({
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
Fällig am:
|
||||
</span>
|
||||
|
||||
@@ -148,54 +148,39 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
||||
|
||||
const text = newItemText.trim();
|
||||
const qty = newItemQty.trim();
|
||||
const cat = newItemCategory;
|
||||
const cat = newItemCategory || undefined;
|
||||
|
||||
setNewItemText('');
|
||||
setNewItemQty('');
|
||||
|
||||
const created = await api.createListItem(activeList.id, text, cat, qty);
|
||||
try {
|
||||
const item = await api.createListItem(activeList.id, text, qty, cat);
|
||||
setLists((prev) =>
|
||||
prev.map((l) => {
|
||||
if (l.id !== activeList.id) return l;
|
||||
if (l.items.some((i) => i.id === created.id)) return l;
|
||||
return { ...l, items: [created, ...l.items] };
|
||||
})
|
||||
prev.map((l) => (l.id === activeList.id ? { ...l, items: [item, ...l.items] } : l))
|
||||
);
|
||||
onRefreshGlobal();
|
||||
} catch (err: any) {
|
||||
alert(err.message || 'Fehler beim Erstellen des Listeneintrags');
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleItem = async (item: ListItem) => {
|
||||
if (!activeList) return;
|
||||
triggerHaptic(18);
|
||||
triggerHaptic(10);
|
||||
const nextCompleted = !item.isCompleted;
|
||||
|
||||
// Optimistic UI update (BRAIN.md Section 3)
|
||||
setLists((prev) =>
|
||||
prev.map((l) => {
|
||||
if (l.id !== activeList.id) return l;
|
||||
return {
|
||||
...l,
|
||||
items: l.items.map((i) =>
|
||||
i.id === item.id ? { ...i, isCompleted: nextCompleted } : i
|
||||
),
|
||||
items: l.items.map((i) => (i.id === item.id ? { ...i, isCompleted: nextCompleted } : i)),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
const updated = await api.toggleListItem(activeList.id, item.id, nextCompleted);
|
||||
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);
|
||||
}
|
||||
await api.toggleListItem(activeList.id, item.id);
|
||||
onRefreshGlobal();
|
||||
};
|
||||
|
||||
const handleDeleteItem = async (itemId: string) => {
|
||||
@@ -272,10 +257,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
||||
<button
|
||||
key={sp.id}
|
||||
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
|
||||
? 'bg-accent text-white border-accent'
|
||||
: 'bg-canvas text-zinc-300 hover:text-white border-border'
|
||||
? 'bg-accent text-on-accent border-accent'
|
||||
: 'bg-canvas text-muted hover:text-main border-border'
|
||||
}`}
|
||||
>
|
||||
<span>{sp.icon}</span>
|
||||
@@ -302,10 +287,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
||||
{activeSpace && (
|
||||
<div className="flex items-center gap-2">
|
||||
<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
|
||||
? 'bg-[#22c55e]/15 text-[#22c55e] border border-[#22c55e]/30'
|
||||
: 'bg-panel-elevated text-zinc-400 border border-border'
|
||||
? 'badge-ok'
|
||||
: 'badge-neutral'
|
||||
}`}
|
||||
>
|
||||
<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" />
|
||||
<span>Code: {activeSpace.inviteCode}</span>
|
||||
{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>
|
||||
</div>
|
||||
@@ -383,16 +368,16 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
||||
<button
|
||||
key={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
|
||||
? 'bg-accent text-white border-accent'
|
||||
: 'bg-panel text-zinc-300 hover:text-white border-border'
|
||||
? 'bg-accent text-on-accent border-accent'
|
||||
: 'bg-panel text-muted hover:text-main border-border'
|
||||
}`}
|
||||
>
|
||||
<span>{lst.icon}</span>
|
||||
<span>{lst.title}</span>
|
||||
<span className={`px-1.5 py-0.2 text-[10px] font-semibold ${
|
||||
isSelected ? 'bg-white/20 text-white' : 'bg-canvas text-zinc-400'
|
||||
<span className={`px-1.5 py-0.2 text-[10px] font-semibold rounded-pill ${
|
||||
isSelected ? 'bg-white/20 text-on-accent' : 'badge-neutral'
|
||||
}`}>
|
||||
{openCount}
|
||||
</span>
|
||||
@@ -420,7 +405,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
||||
key={ic}
|
||||
type="button"
|
||||
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
|
||||
? 'bg-accent/20 border border-accent text-accent'
|
||||
: 'bg-canvas hover:bg-panel-elevated border border-border'
|
||||
@@ -481,16 +466,16 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
||||
|
||||
{/* Category Quick-Select Pills */}
|
||||
<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) => (
|
||||
<button
|
||||
key={cat}
|
||||
type="button"
|
||||
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
|
||||
? 'bg-accent text-white border-accent font-medium'
|
||||
: 'bg-canvas text-zinc-400 hover:text-white border-border'
|
||||
? 'bg-accent text-on-accent border-accent font-medium'
|
||||
: 'bg-canvas text-muted hover:text-main border-border'
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
@@ -500,9 +485,9 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
||||
</form>
|
||||
|
||||
{/* 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 ? (
|
||||
<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" />
|
||||
<span>Alles erledigt. Keine offenen Punkte auf dieser Liste.</span>
|
||||
</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"
|
||||
>
|
||||
<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" />
|
||||
<span className="text-sm font-normal text-white truncate">
|
||||
<Circle className="w-4 h-4 text-faint group-hover:text-accent flex-shrink-0 transition-colors" />
|
||||
<span className="text-sm font-normal text-main truncate">
|
||||
{item.content}
|
||||
</span>
|
||||
{item.quantity && (
|
||||
@@ -524,7 +509,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
||||
</span>
|
||||
)}
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
@@ -549,7 +534,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
||||
{completedItems.length > 0 && (
|
||||
<div className="mt-3 pt-2.5 border-t border-border">
|
||||
<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})
|
||||
</span>
|
||||
<button
|
||||
@@ -560,7 +545,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
||||
</button>
|
||||
</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) => (
|
||||
<div
|
||||
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">
|
||||
<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}
|
||||
</span>
|
||||
{item.quantity && (
|
||||
<span className="text-xs text-zinc-500">({item.quantity})</span>
|
||||
<span className="text-xs text-faint">({item.quantity})</span>
|
||||
)}
|
||||
</div>
|
||||
{item.completedByName && (
|
||||
<span className="text-xs text-zinc-500">✓ {item.completedByName}</span>
|
||||
<span className="text-xs text-faint">✓ {item.completedByName}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -382,40 +382,38 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
<span>Zurück</span>
|
||||
</button>
|
||||
<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" />
|
||||
<span>Administration & Team-Steuerung</span>
|
||||
</h1>
|
||||
<p className="text-xs text-zinc-400">
|
||||
<p className="text-xs text-muted">
|
||||
Benutzerverwaltung, Einladungscodes, Organisationen, Teams, Isolationsmodus & Mail-Server
|
||||
</p>
|
||||
</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'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{statusMsg && (
|
||||
<div
|
||||
className={`p-3 border text-xs flex items-center justify-between gap-2 ${
|
||||
statusMsg.type === 'ok'
|
||||
? 'bg-blue-950/30 border-accent text-blue-200'
|
||||
: 'bg-red-950/30 border-red-500 text-red-200'
|
||||
className={`p-3 text-xs flex items-center justify-between gap-2 rounded-card ${
|
||||
statusMsg.type === 'ok' ? 'alert-ok' : 'alert-err'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{statusMsg.type === 'ok' ? (
|
||||
<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>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setStatusMsg(null)}
|
||||
className="text-zinc-400 hover:text-white text-[11px]"
|
||||
className="text-muted hover:text-main text-[11px]"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
@@ -425,19 +423,19 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
{/* Main 2-Column Layout */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-start">
|
||||
{/* 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
|
||||
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 ${
|
||||
section === 'users'
|
||||
? 'bg-panel-elevated text-white border-l-2 border-l-accent'
|
||||
: 'text-zinc-400 hover:text-white hover:bg-white/[0.02]'
|
||||
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
|
||||
: 'text-muted hover:text-main hover:bg-canvas/50'
|
||||
}`}
|
||||
>
|
||||
<Users className="w-4 h-4 text-accent flex-shrink-0" />
|
||||
<div>
|
||||
<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>
|
||||
</button>
|
||||
|
||||
@@ -445,14 +443,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
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 ${
|
||||
section === 'orgs'
|
||||
? 'bg-panel-elevated text-white border-l-2 border-l-accent'
|
||||
: 'text-zinc-400 hover:text-white hover:bg-white/[0.02]'
|
||||
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
|
||||
: 'text-muted hover:text-main hover:bg-canvas/50'
|
||||
}`}
|
||||
>
|
||||
<Building2 className="w-4 h-4 text-accent flex-shrink-0" />
|
||||
<div>
|
||||
<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>
|
||||
</button>
|
||||
|
||||
@@ -462,14 +460,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
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 ${
|
||||
section === 'server'
|
||||
? 'bg-panel-elevated text-white border-l-2 border-l-accent'
|
||||
: 'text-zinc-400 hover:text-white hover:bg-white/[0.02]'
|
||||
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
|
||||
: 'text-muted hover:text-main hover:bg-canvas/50'
|
||||
}`}
|
||||
>
|
||||
<Server className="w-4 h-4 text-accent flex-shrink-0" />
|
||||
<div>
|
||||
<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>
|
||||
</button>
|
||||
|
||||
@@ -477,14 +475,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
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 ${
|
||||
section === 'smtp'
|
||||
? 'bg-panel-elevated text-white border-l-2 border-l-accent'
|
||||
: 'text-zinc-400 hover:text-white hover:bg-white/[0.02]'
|
||||
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
|
||||
: 'text-muted hover:text-main hover:bg-canvas/50'
|
||||
}`}
|
||||
>
|
||||
<Mail className="w-4 h-4 text-accent flex-shrink-0" />
|
||||
<div>
|
||||
<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>
|
||||
</button>
|
||||
</>
|
||||
@@ -492,23 +490,23 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
</aside>
|
||||
|
||||
{/* 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 === 'users' && (
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-border pb-3">
|
||||
<h2 className="text-sm font-bold text-white">Benutzersteuerung, Rollen & Einladungen</h2>
|
||||
<p className="text-zinc-400 mt-0.5">
|
||||
<h2 className="text-sm font-bold text-main">Benutzersteuerung, Rollen & Einladungen</h2>
|
||||
<p className="text-muted mt-0.5">
|
||||
Lege Benutzerkonten direkt an oder erzeuge Einladungscodes, mit denen sich eingeladene
|
||||
Personen selbst registrieren können, auch wenn die öffentliche Registrierung gesperrt ist.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Direct User Creation Form (Fixed Layout: 4 clean columns with min-w-0, no overflow!) */}
|
||||
<div className="p-4 bg-canvas border border-border space-y-3">
|
||||
<h3 className="font-bold text-white flex items-center gap-1.5">
|
||||
{/* Direct User Creation Form */}
|
||||
<div className="p-4 bg-canvas border border-border rounded-card space-y-3">
|
||||
<h3 className="font-bold text-main flex items-center gap-1.5">
|
||||
<UserPlus className="w-3.5 h-3.5 text-accent" />
|
||||
<span>Neues Benutzerkonto direkt anlegen</span>
|
||||
</h3>
|
||||
@@ -516,31 +514,31 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
<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="min-w-0">
|
||||
<label className="block text-zinc-400 mb-1">Anzeigename</label>
|
||||
<label className="block text-muted mb-1">Anzeigename</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newUserName}
|
||||
onChange={(e) => setNewUserName(e.target.value)}
|
||||
placeholder="z.B. Anna Beispiel"
|
||||
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 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
|
||||
type="email"
|
||||
value={newUserEmail}
|
||||
onChange={(e) => setNewUserEmail(e.target.value)}
|
||||
placeholder="anna@beispiel.de"
|
||||
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 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
|
||||
type="password"
|
||||
value={newUserPassword}
|
||||
@@ -548,16 +546,16 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
placeholder="Mind. 4 Zeichen"
|
||||
minLength={4}
|
||||
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 className="min-w-0">
|
||||
<label className="block text-zinc-400 mb-1">Benutzerrolle</label>
|
||||
<label className="block text-muted mb-1">Benutzerrolle</label>
|
||||
<select
|
||||
value={newUserRole}
|
||||
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="team_admin">Team-Admin (Darf einladen)</option>
|
||||
@@ -579,8 +577,8 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Invite Code Generator */}
|
||||
<div className="p-4 bg-canvas border border-border space-y-3">
|
||||
<h3 className="font-bold text-white flex items-center gap-1.5">
|
||||
<div className="p-4 bg-canvas border border-border rounded-card space-y-3">
|
||||
<h3 className="font-bold text-main flex items-center gap-1.5">
|
||||
<KeyRound className="w-3.5 h-3.5 text-accent" />
|
||||
<span>Einladungscode erzeugen (gültig für 7 Tage)</span>
|
||||
</h3>
|
||||
@@ -588,7 +586,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
<form onSubmit={handleCreateInvite} className="space-y-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div className="min-w-0">
|
||||
<label className="block text-zinc-400 mb-1">
|
||||
<label className="block text-muted mb-1">
|
||||
Gebundene E-Mail (optional)
|
||||
</label>
|
||||
<input
|
||||
@@ -596,16 +594,16 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
value={inviteEmail}
|
||||
onChange={(e) => setInviteEmail(e.target.value)}
|
||||
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 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
|
||||
value={inviteRole}
|
||||
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="team_admin">Team-Admin</option>
|
||||
@@ -614,13 +612,13 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="min-w-0">
|
||||
<label className="block text-zinc-400 mb-1">
|
||||
<label className="block text-muted mb-1">
|
||||
Automatisch Team zuweisen (optional)
|
||||
</label>
|
||||
<select
|
||||
value={inviteTeamId}
|
||||
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>
|
||||
{flattenedTeams.map((t) => (
|
||||
@@ -635,7 +633,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
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" />
|
||||
<span>Einladungscode generieren</span>
|
||||
@@ -646,29 +644,29 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
{/* Active Invites List */}
|
||||
{invites.length > 0 && (
|
||||
<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:
|
||||
</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) => (
|
||||
<div
|
||||
key={inv.id}
|
||||
className="p-2 flex flex-wrap items-center justify-between gap-2"
|
||||
>
|
||||
<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}
|
||||
</code>
|
||||
<span className="text-zinc-300">
|
||||
Rolle: <strong>{inv.role}</strong>
|
||||
<span className="text-muted">
|
||||
Rolle: <strong className="text-main">{inv.role}</strong>
|
||||
{inv.email ? ` · Für: ${inv.email}` : ''}
|
||||
</span>
|
||||
{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
|
||||
</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
|
||||
</span>
|
||||
)}
|
||||
@@ -701,23 +699,23 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
|
||||
{/* Users List */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-bold text-white">
|
||||
<h3 className="font-bold text-main">
|
||||
Registrierte Benutzer ({users.length})
|
||||
</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) => (
|
||||
<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>
|
||||
<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 && (
|
||||
<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
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-zinc-400 text-[11px]">{u.email}</p>
|
||||
<p className="text-muted text-[11px]">{u.email}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -726,7 +724,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
value={u.role}
|
||||
disabled={!isServerAdmin && u.role === 'admin'}
|
||||
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"
|
||||
>
|
||||
<option value="member">Mitglied</option>
|
||||
@@ -775,7 +773,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
value={resetPwdVal}
|
||||
onChange={(e) => setResetPwdVal(e.target.value)}
|
||||
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
|
||||
onClick={() => handleConfirmResetPassword(u.id)}
|
||||
@@ -804,8 +802,8 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
{section === 'orgs' && (
|
||||
<div className="space-y-6">
|
||||
<div className="border-b border-border pb-3">
|
||||
<h2 className="text-sm font-bold text-white">Organisationen & Teams verwalten</h2>
|
||||
<p className="text-zinc-400 mt-0.5">
|
||||
<h2 className="text-sm font-bold text-main">Organisationen & Teams verwalten</h2>
|
||||
<p className="text-muted mt-0.5">
|
||||
Erstelle Organisationen und unterteile sie in Teams. Weise Benutzern Teams zu, damit
|
||||
sie im Modus "Teams isoliert" gemeinsam an Listen und Aufgaben arbeiten können.
|
||||
</p>
|
||||
@@ -813,8 +811,8 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
|
||||
{/* Create Organization & Team */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div className="p-4 bg-canvas border border-border space-y-3">
|
||||
<h3 className="font-bold text-white">1. Neue Organisation gründen</h3>
|
||||
<div className="p-4 bg-canvas border border-border rounded-card space-y-3">
|
||||
<h3 className="font-bold text-main">1. Neue Organisation gründen</h3>
|
||||
<form onSubmit={handleCreateOrg} className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
@@ -822,7 +820,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
onChange={(e) => setNewOrgName(e.target.value)}
|
||||
placeholder="z.B. Homelab, Familie, Firma…"
|
||||
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">
|
||||
Anlegen
|
||||
@@ -830,10 +828,10 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-canvas border border-border space-y-3">
|
||||
<h3 className="font-bold text-white">2. Neues Team in Organisation anlegen</h3>
|
||||
<div className="p-4 bg-canvas border border-border rounded-card space-y-3">
|
||||
<h3 className="font-bold text-main">2. Neues Team in Organisation anlegen</h3>
|
||||
{orgs.length === 0 ? (
|
||||
<p className="text-zinc-500">
|
||||
<p className="text-faint">
|
||||
Bitte lege zuerst links eine Organisation an.
|
||||
</p>
|
||||
) : (
|
||||
@@ -842,7 +840,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
<select
|
||||
value={selectedOrgId}
|
||||
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) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
@@ -856,7 +854,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
onChange={(e) => setNewTeamName(e.target.value)}
|
||||
placeholder="Team-Name (z.B. Infrastruktur)"
|
||||
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 className="flex gap-2">
|
||||
@@ -865,7 +863,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
value={newTeamDesc}
|
||||
onChange={(e) => setNewTeamDesc(e.target.value)}
|
||||
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">
|
||||
Team erstellen
|
||||
@@ -879,19 +877,19 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
{/* Organizations & Nested Teams Display */}
|
||||
<div className="space-y-4">
|
||||
{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.
|
||||
</div>
|
||||
) : (
|
||||
orgs.map((org) => {
|
||||
const orgTeams = Array.isArray(org.teams) ? org.teams : [];
|
||||
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="flex items-center gap-2">
|
||||
<Building2 className="w-4 h-4 text-accent" />
|
||||
<span className="font-bold text-sm text-white">{org.name}</span>
|
||||
<span className="text-[11px] text-zinc-400">
|
||||
<span className="font-bold text-sm text-main">{org.name}</span>
|
||||
<span className="text-[11px] text-muted">
|
||||
({orgTeams.length} {orgTeams.length === 1 ? 'Team' : 'Teams'})
|
||||
</span>
|
||||
</div>
|
||||
@@ -906,7 +904,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
|
||||
<div className="p-3 space-y-3">
|
||||
{orgTeams.length === 0 ? (
|
||||
<p className="text-zinc-500 text-xs">
|
||||
<p className="text-faint text-xs">
|
||||
Noch keine Teams in dieser Organisation.
|
||||
</p>
|
||||
) : (
|
||||
@@ -915,16 +913,16 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
return (
|
||||
<div
|
||||
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>
|
||||
<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" />
|
||||
<span>{team.name}</span>
|
||||
</span>
|
||||
{team.description && (
|
||||
<p className="text-[11px] text-zinc-400 mt-0.5">
|
||||
<p className="text-[11px] text-muted mt-0.5">
|
||||
{team.description}
|
||||
</p>
|
||||
)}
|
||||
@@ -940,11 +938,11 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
|
||||
{/* Team Members */}
|
||||
<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}):
|
||||
</p>
|
||||
{members.length === 0 ? (
|
||||
<p className="text-[11px] text-zinc-500">
|
||||
<p className="text-[11px] text-faint">
|
||||
Noch keine Mitglieder zugewiesen.
|
||||
</p>
|
||||
) : (
|
||||
@@ -952,19 +950,19 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
{members.map((m) => (
|
||||
<div
|
||||
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}
|
||||
</span>
|
||||
<span className="text-[10px] text-zinc-400">
|
||||
<span className="text-[10px] text-muted">
|
||||
({m.role})
|
||||
</span>
|
||||
<button
|
||||
onClick={() =>
|
||||
handleRemoveMemberFromTeam(team.id, m.userId)
|
||||
}
|
||||
className="text-zinc-500 hover:text-red-400"
|
||||
className="text-faint hover:text-status-err"
|
||||
title="Aus Team entfernen"
|
||||
>
|
||||
×
|
||||
@@ -985,7 +983,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
[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>
|
||||
{users.map((u) => (
|
||||
@@ -1003,7 +1001,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
[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="team_admin">Team-Admin</option>
|
||||
@@ -1037,10 +1035,10 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
{section === 'server' && isServerAdmin && (
|
||||
<div className="space-y-6">
|
||||
<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
|
||||
</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
|
||||
arbeiten, sich nur innerhalb ihrer Teams sehen oder ob alle Nutzer für alle sichtbar sind.
|
||||
</p>
|
||||
@@ -1048,7 +1046,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-zinc-300 font-semibold mb-1">
|
||||
<label className="block text-muted font-semibold mb-1">
|
||||
Name der MindSift-Instanz
|
||||
</label>
|
||||
<div className="flex gap-2 max-w-md">
|
||||
@@ -1058,7 +1056,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
onChange={(e) =>
|
||||
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
|
||||
type="button"
|
||||
@@ -1072,7 +1070,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
|
||||
{/* Sharing Isolation Cards */}
|
||||
<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" />
|
||||
<span>Globale Sharing- & Sichtbarkeits-Isolation</span>
|
||||
</label>
|
||||
@@ -1081,14 +1079,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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'
|
||||
? 'bg-canvas border-accent text-white'
|
||||
: 'bg-canvas/50 border-border text-zinc-400 hover:text-white'
|
||||
? 'bg-canvas border-accent text-main shadow-panel'
|
||||
: '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="text-[11px] text-zinc-400 mt-1 leading-relaxed">
|
||||
<p className="font-bold text-xs text-main">1. Strikt Privat (Keiner sieht andere)</p>
|
||||
<p className="text-[11px] text-muted mt-1 leading-relaxed">
|
||||
Jeder Benutzer arbeitet komplett für sich allein. Keine geteilten Spaces und keine
|
||||
Benutzersuche möglich.
|
||||
</p>
|
||||
@@ -1097,14 +1095,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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'
|
||||
? 'bg-canvas border-accent text-white'
|
||||
: 'bg-canvas/50 border-border text-zinc-400 hover:text-white'
|
||||
? 'bg-canvas border-accent text-main shadow-panel'
|
||||
: '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="text-[11px] text-zinc-400 mt-1 leading-relaxed">
|
||||
<p className="font-bold text-xs text-main">2. Teams isoliert (Empfohlen)</p>
|
||||
<p className="text-[11px] text-muted mt-1 leading-relaxed">
|
||||
Benutzer sehen ausschließlich Personen, mit denen sie mindestens ein gemeinsames Team
|
||||
teilen.
|
||||
</p>
|
||||
@@ -1113,14 +1111,14 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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'
|
||||
? 'bg-canvas border-accent text-white'
|
||||
: 'bg-canvas/50 border-border text-zinc-400 hover:text-white'
|
||||
? 'bg-canvas border-accent text-main shadow-panel'
|
||||
: '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="text-[11px] text-zinc-400 mt-1 leading-relaxed">
|
||||
<p className="font-bold text-xs text-main">3. Offen (Alle sehen alle)</p>
|
||||
<p className="text-[11px] text-muted mt-1 leading-relaxed">
|
||||
Alle registrierten Benutzer dieser Instanz können sich gegenseitig sehen und Spaces
|
||||
miteinander teilen.
|
||||
</p>
|
||||
@@ -1129,10 +1127,10 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<p className="font-bold text-white">Öffentliche Selbstregistrierung erlauben</p>
|
||||
<p className="text-[11px] text-zinc-400 mt-0.5">
|
||||
<p className="font-bold text-main">Öffentliche Selbstregistrierung erlauben</p>
|
||||
<p className="text-[11px] text-muted mt-0.5">
|
||||
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
|
||||
angelegt werden.
|
||||
@@ -1144,10 +1142,10 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
onClick={() =>
|
||||
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
|
||||
? '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)'}
|
||||
@@ -1164,17 +1162,17 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
<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>
|
||||
<h2 className="text-sm font-bold text-white">
|
||||
<h2 className="text-sm font-bold text-main">
|
||||
E-Mail / SMTP-Server für Aufgaben-Alarme
|
||||
</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
|
||||
direkt hier über die Oberfläche (wird persistent in der SQLite-Datenbank gespeichert).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{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
|
||||
</span>
|
||||
)}
|
||||
@@ -1182,7 +1180,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-zinc-300 font-medium mb-1">
|
||||
<label className="block text-muted font-medium mb-1">
|
||||
SMTP Host (`SMTP_HOST`)
|
||||
</label>
|
||||
<input
|
||||
@@ -1191,13 +1189,13 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
onChange={(e) =>
|
||||
setSettings((prev) => ({ ...prev, smtpHost: e.target.value }))
|
||||
}
|
||||
placeholder="z.B. smtp.gmail.com oder mail.deinedomain.de"
|
||||
className="w-full h-9 px-3 input-field text-white font-mono"
|
||||
placeholder="z.B. smtp.example.com oder mail.deinedomain.de"
|
||||
className="w-full h-9 px-3 input-field font-mono"
|
||||
/>
|
||||
</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`)
|
||||
</label>
|
||||
<input
|
||||
@@ -1207,12 +1205,12 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
setSettings((prev) => ({ ...prev, smtpPort: Number(e.target.value) }))
|
||||
}
|
||||
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>
|
||||
<label className="block text-zinc-300 font-medium mb-1">
|
||||
<label className="block text-muted font-medium mb-1">
|
||||
SMTP Benutzername (`SMTP_USER`)
|
||||
</label>
|
||||
<input
|
||||
@@ -1222,12 +1220,12 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
setSettings((prev) => ({ ...prev, smtpUser: e.target.value }))
|
||||
}
|
||||
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>
|
||||
<label className="block text-zinc-300 font-medium mb-1">
|
||||
<label className="block text-muted font-medium mb-1">
|
||||
SMTP Passwort (`SMTP_PASS`)
|
||||
</label>
|
||||
<input
|
||||
@@ -1237,12 +1235,12 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
setSettings((prev) => ({ ...prev, smtpPass: e.target.value }))
|
||||
}
|
||||
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>
|
||||
<label className="block text-zinc-300 font-medium mb-1">
|
||||
<label className="block text-muted font-medium mb-1">
|
||||
Absender-Adresse (`SMTP_FROM`)
|
||||
</label>
|
||||
<input
|
||||
@@ -1252,7 +1250,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
setSettings((prev) => ({ ...prev, smtpFrom: e.target.value }))
|
||||
}
|
||||
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>
|
||||
|
||||
@@ -1264,9 +1262,9 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
onChange={(e) =>
|
||||
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`)
|
||||
</span>
|
||||
</label>
|
||||
@@ -1284,9 +1282,9 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Test SMTP Delivery */}
|
||||
<div className="p-4 bg-canvas border border-border space-y-2">
|
||||
<h3 className="font-bold text-white">SMTP-Verbindung testen</h3>
|
||||
<p className="text-[11px] text-zinc-400">
|
||||
<div className="p-4 bg-canvas border border-border rounded-card space-y-2">
|
||||
<h3 className="font-bold text-main">SMTP-Verbindung testen</h3>
|
||||
<p className="text-[11px] text-muted">
|
||||
Sendet sofort eine Test-E-Mail mit den oben eingetragenen Daten.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
@@ -1295,7 +1293,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
value={testSmtpEmail}
|
||||
onChange={(e) => setTestSmtpEmail(e.target.value)}
|
||||
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
|
||||
type="button"
|
||||
|
||||
@@ -14,20 +14,26 @@ import {
|
||||
Globe,
|
||||
Send,
|
||||
AlertCircle,
|
||||
Palette,
|
||||
} from 'lucide-react';
|
||||
import { APIKey, User } from '../../lib/types';
|
||||
import { api, triggerHaptic } from '../../lib/api';
|
||||
import { AVAILABLE_THEMES, ThemeId } from '../../lib/theme';
|
||||
|
||||
interface UserSettingsViewProps {
|
||||
currentUser: User;
|
||||
activeTheme: ThemeId;
|
||||
onThemeChange: (themeId: ThemeId) => void;
|
||||
onUserUpdated: (u: User) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
type UserSettingsSection = 'profile' | 'alarms' | 'webhooks';
|
||||
type UserSettingsSection = 'profile' | 'appearance' | 'alarms' | 'webhooks';
|
||||
|
||||
export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
currentUser,
|
||||
activeTheme,
|
||||
onThemeChange,
|
||||
onUserUpdated,
|
||||
onBack,
|
||||
}) => {
|
||||
@@ -219,7 +225,8 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
};
|
||||
|
||||
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 \\
|
||||
-H "X-API-Key: ${activeKeyPlaceholder}" \\
|
||||
@@ -247,14 +254,14 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
<span>Zurück</span>
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-base font-bold text-white tracking-tight">Kontoeinstellungen</h1>
|
||||
<p className="text-xs text-zinc-400">
|
||||
Persönliches Profil, Aufgaben-Alarme, Private Webhooks & Daten-Backup
|
||||
<h1 className="text-base font-bold text-main tracking-tight">Kontoeinstellungen</h1>
|
||||
<p className="text-xs text-muted">
|
||||
Persönliches Profil, Design-Themes, Aufgaben-Alarme, Private Webhooks & Daten-Backup
|
||||
</p>
|
||||
</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>
|
||||
</span>
|
||||
</div>
|
||||
@@ -262,19 +269,34 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
{/* Main 2-Column Layout */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-start">
|
||||
{/* 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
|
||||
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 ${
|
||||
section === 'profile'
|
||||
? 'bg-panel-elevated text-white border-l-2 border-l-accent'
|
||||
: 'text-zinc-400 hover:text-white hover:bg-white/[0.02]'
|
||||
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
|
||||
: 'text-muted hover:text-main hover:bg-panel-hover'
|
||||
}`}
|
||||
>
|
||||
<UserIcon className="w-4 h-4 text-accent flex-shrink-0" />
|
||||
<div>
|
||||
<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>
|
||||
</button>
|
||||
|
||||
@@ -282,14 +304,14 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
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 ${
|
||||
section === 'alarms'
|
||||
? 'bg-panel-elevated text-white border-l-2 border-l-accent'
|
||||
: 'text-zinc-400 hover:text-white hover:bg-white/[0.02]'
|
||||
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
|
||||
: 'text-muted hover:text-main hover:bg-panel-hover'
|
||||
}`}
|
||||
>
|
||||
<Bell className="w-4 h-4 text-accent flex-shrink-0" />
|
||||
<div>
|
||||
<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>
|
||||
</button>
|
||||
|
||||
@@ -297,44 +319,42 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
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 ${
|
||||
section === 'webhooks'
|
||||
? 'bg-panel-elevated text-white border-l-2 border-l-accent'
|
||||
: 'text-zinc-400 hover:text-white hover:bg-white/[0.02]'
|
||||
? 'bg-panel-elevated text-main border-l-2 border-l-accent'
|
||||
: 'text-muted hover:text-main hover:bg-panel-hover'
|
||||
}`}
|
||||
>
|
||||
<Key className="w-4 h-4 text-accent flex-shrink-0" />
|
||||
<div>
|
||||
<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>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
{/* 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 === 'profile' && (
|
||||
<div className="space-y-5 max-w-2xl">
|
||||
<div className="border-b border-border pb-3">
|
||||
<h2 className="text-sm font-bold text-white">Profil & Anmeldedaten</h2>
|
||||
<p className="text-zinc-400 mt-0.5">
|
||||
<h2 className="text-sm font-bold text-main">Profil & Anmeldedaten</h2>
|
||||
<p className="text-muted mt-0.5">
|
||||
Verwalte deinen Anzeigenamen, deine E-Mail-Adresse und dein persönliches Passwort.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{profileMsg && (
|
||||
<div
|
||||
className={`p-3 border flex items-center gap-2 ${
|
||||
profileMsg.type === 'ok'
|
||||
? 'bg-blue-950/30 border-accent text-blue-200'
|
||||
: 'bg-red-950/30 border-red-500 text-red-200'
|
||||
className={`flex items-center gap-2 ${
|
||||
profileMsg.type === 'ok' ? 'alert-ok' : 'alert-err'
|
||||
}`}
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
@@ -343,30 +363,32 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
<form onSubmit={handleSaveProfile} className="space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<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
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
required
|
||||
className="w-full h-9 px-3 input-field text-white"
|
||||
className="w-full h-9 px-3 input-field"
|
||||
/>
|
||||
</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
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full h-9 px-3 input-field text-white"
|
||||
className="w-full h-9 px-3 input-field"
|
||||
/>
|
||||
</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)
|
||||
</label>
|
||||
<input
|
||||
@@ -374,7 +396,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
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>
|
||||
|
||||
@@ -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 & 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' && (
|
||||
<div className="space-y-5">
|
||||
<div className="border-b border-border pb-3">
|
||||
<h2 className="text-sm font-bold text-white">Aufgaben-Alarme & Benachrichtigungsziele</h2>
|
||||
<p className="text-zinc-400 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.
|
||||
<h2 className="text-sm font-bold text-main">
|
||||
Aufgaben-Alarme & Benachrichtigungsziele
|
||||
</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{alarmMsg && (
|
||||
<div
|
||||
className={`p-3 border flex items-center gap-2 ${
|
||||
alarmMsg.type === 'ok'
|
||||
? 'bg-blue-950/30 border-accent text-blue-200'
|
||||
: 'bg-red-950/30 border-red-500 text-red-200'
|
||||
className={`flex items-center gap-2 ${
|
||||
alarmMsg.type === 'ok' ? 'alert-ok' : 'alert-err'
|
||||
}`}
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
@@ -425,17 +546,17 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
{/* 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>
|
||||
<label className="font-semibold text-white block">
|
||||
<label className="font-semibold text-main block">
|
||||
Vorwarnzeit vor Fälligkeit
|
||||
</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?
|
||||
</p>
|
||||
</div>
|
||||
<select
|
||||
value={notifyBeforeMinutes}
|
||||
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={15}>15 Minuten vorher</option>
|
||||
@@ -448,10 +569,8 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
|
||||
{/* Channel 1: E-Mail */}
|
||||
<div
|
||||
className={`p-4 border transition-colors space-y-3 ${
|
||||
alarmChannels.includes('email')
|
||||
? 'bg-canvas border-accent'
|
||||
: 'bg-canvas/60 border-border'
|
||||
className={`p-4 border transition-colors space-y-3 bg-canvas ${
|
||||
alarmChannels.includes('email') ? 'border-accent' : 'border-border'
|
||||
}`}
|
||||
>
|
||||
<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" />
|
||||
<div>
|
||||
<span className="font-bold text-white block">Ziel: E-Mail</span>
|
||||
<span className="text-[11px] text-zinc-400">
|
||||
<span className="font-bold text-main block">Ziel: E-Mail</span>
|
||||
<span className="text-[11px] text-muted">
|
||||
Sendet eine E-Mail über den konfigurierten SMTP-Server der Instanz.
|
||||
</span>
|
||||
</div>
|
||||
@@ -483,7 +602,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
</div>
|
||||
|
||||
<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})
|
||||
</label>
|
||||
<input
|
||||
@@ -491,17 +610,15 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
value={alarmEmail}
|
||||
onChange={(e) => setAlarmEmail(e.target.value)}
|
||||
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>
|
||||
|
||||
{/* Channel 2: Discord Webhook */}
|
||||
<div
|
||||
className={`p-4 border transition-colors space-y-3 ${
|
||||
alarmChannels.includes('discord')
|
||||
? 'bg-canvas border-accent'
|
||||
: 'bg-canvas/60 border-border'
|
||||
className={`p-4 border transition-colors space-y-3 bg-canvas ${
|
||||
alarmChannels.includes('discord') ? 'border-accent' : 'border-border'
|
||||
}`}
|
||||
>
|
||||
<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" />
|
||||
<div>
|
||||
<span className="font-bold text-white block">Ziel: Discord Webhook</span>
|
||||
<span className="text-[11px] text-zinc-400">
|
||||
Sendet eine formatierte Embed-Benachrichtigung direkt in deinen Discord-Kanal.
|
||||
<span className="font-bold text-main block">Ziel: Discord Webhook</span>
|
||||
<span className="text-[11px] text-muted">
|
||||
Sendet eine formatierte Embed-Benachrichtigung direkt in deinen
|
||||
Discord-Kanal.
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
@@ -533,23 +651,21 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
</div>
|
||||
|
||||
<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
|
||||
type="url"
|
||||
value={discordWebhookUrl}
|
||||
onChange={(e) => setDiscordWebhookUrl(e.target.value)}
|
||||
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>
|
||||
|
||||
{/* Channel 3: Custom HTTP Webhook */}
|
||||
<div
|
||||
className={`p-4 border transition-colors space-y-3 ${
|
||||
alarmChannels.includes('webhook')
|
||||
? 'bg-canvas border-accent'
|
||||
: 'bg-canvas/60 border-border'
|
||||
className={`p-4 border transition-colors space-y-3 bg-canvas ${
|
||||
alarmChannels.includes('webhook') ? 'border-accent' : 'border-border'
|
||||
}`}
|
||||
>
|
||||
<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" />
|
||||
<div>
|
||||
<span className="font-bold text-white block">
|
||||
<span className="font-bold text-main block">
|
||||
Ziel: Eigener Webhook (HTTP POST JSON)
|
||||
</span>
|
||||
<span className="text-[11px] text-zinc-400">
|
||||
Für eigene Empfänger-Skripte, Ntfy, Gotify, Slack, Mattermost oder Automatisierungen.
|
||||
<span className="text-[11px] text-muted">
|
||||
Für eigene Empfänger-Skripte, Ntfy, Gotify, Slack, Mattermost oder
|
||||
Automatisierungen.
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
@@ -583,13 +700,15 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
</div>
|
||||
|
||||
<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
|
||||
type="url"
|
||||
value={customWebhookUrl}
|
||||
onChange={(e) => setCustomWebhookUrl(e.target.value)}
|
||||
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>
|
||||
@@ -608,15 +727,17 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
)}
|
||||
|
||||
{/* ================================================================= */}
|
||||
{/* SECTION 3: PRIVATE WEBHOOKS & EXPORT */}
|
||||
{/* SECTION 4: PRIVATE WEBHOOKS & EXPORT */}
|
||||
{/* ================================================================= */}
|
||||
{section === 'webhooks' && (
|
||||
<div className="space-y-5">
|
||||
<div className="border-b border-border pb-3">
|
||||
<h2 className="text-sm font-bold text-white">Private Webhooks (Eingehend) & Daten-Export</h2>
|
||||
<p className="text-zinc-400 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.
|
||||
<h2 className="text-sm font-bold text-main">
|
||||
Private Webhooks (Eingehend) & Daten-Export
|
||||
</h2>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -627,7 +748,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
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
|
||||
type="submit"
|
||||
@@ -639,12 +760,13 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
</form>
|
||||
|
||||
{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">
|
||||
<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>
|
||||
<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>
|
||||
<button
|
||||
onClick={() => handleCopy(newlyCreatedKey, 'key')}
|
||||
@@ -658,16 +780,17 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
{/* Existing Keys List */}
|
||||
<div className="border border-border divide-y divide-border bg-canvas">
|
||||
{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.
|
||||
</div>
|
||||
) : (
|
||||
apiKeys.map((k) => (
|
||||
<div key={k.id} className="p-3 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="font-semibold text-white">{k.name}</p>
|
||||
<p className="text-[11px] text-zinc-500 font-mono">
|
||||
Präfix: {k.keyPrefix}•••••••• · Erstellt: {new Date(k.createdAt).toLocaleDateString()}
|
||||
<p className="font-semibold text-main">{k.name}</p>
|
||||
<p className="text-[11px] text-faint font-mono">
|
||||
Präfix: {k.keyPrefix}•••••••• · Erstellt:{' '}
|
||||
{new Date(k.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -685,7 +808,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
{/* Curl Example */}
|
||||
<div className="space-y-1.5">
|
||||
<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" />
|
||||
<span>Curl-Beispiel: Direkt als Aufgabe einspeisen</span>
|
||||
</span>
|
||||
@@ -696,7 +819,7 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
{copiedLabel === 'curl' ? 'Kopiert!' : 'Befehl kopieren'}
|
||||
</button>
|
||||
</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}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -704,9 +827,10 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
|
||||
{/* 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>
|
||||
<h3 className="font-bold text-white">Vollständiger Daten-Export (JSON Backup)</h3>
|
||||
<p className="text-zinc-400 text-[11px]">
|
||||
Lädt alle deine Gedanken, Aufgaben, Markdown-Dokumente und Listen als portable JSON-Datei herunter.
|
||||
<h3 className="font-bold text-main">Vollständiger Daten-Export (JSON Backup)</h3>
|
||||
<p className="text-muted text-[11px]">
|
||||
Lädt alle deine Gedanken, Aufgaben, Markdown-Dokumente und Listen als portable
|
||||
JSON-Datei herunter.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -6,16 +6,12 @@ import {
|
||||
Trash2,
|
||||
RotateCcw,
|
||||
FileText,
|
||||
Tag,
|
||||
LayoutList,
|
||||
Columns,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Edit3,
|
||||
X,
|
||||
Clock,
|
||||
Layers,
|
||||
Share2,
|
||||
} from 'lucide-react';
|
||||
@@ -85,7 +81,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
const nextState = !task.isCompleted;
|
||||
const nextKanban = nextState ? 'done' : 'todo';
|
||||
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 });
|
||||
onRefreshStats();
|
||||
@@ -182,7 +180,12 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
};
|
||||
|
||||
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 {
|
||||
await api.deleteTaskList(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 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 doneTasks = safeTasks.filter((t) => t.isCompleted);
|
||||
|
||||
const todoKanban = safeTasks.filter((t) => (!t.kanbanStatus || t.kanbanStatus === 'todo') && !t.isCompleted);
|
||||
const inProgressKanban = safeTasks.filter((t) => t.kanbanStatus === 'in_progress' && !t.isCompleted);
|
||||
const todoKanban = safeTasks.filter(
|
||||
(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 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">
|
||||
<button
|
||||
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'
|
||||
? 'bg-panel border-accent text-white'
|
||||
: 'bg-canvas border-border text-zinc-400 hover:text-white'
|
||||
? 'bg-panel border-accent text-main'
|
||||
: 'bg-canvas border-border text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
Alle Aufgaben
|
||||
@@ -225,18 +234,16 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
<div key={tl.id} className="relative group flex items-center">
|
||||
<button
|
||||
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
|
||||
? 'bg-panel border-accent text-white'
|
||||
: 'bg-canvas border-border text-zinc-400 hover:text-white'
|
||||
? 'bg-panel border-accent text-main'
|
||||
: 'bg-canvas border-border text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<span>{tl.icon}</span>
|
||||
<span>{tl.title}</span>
|
||||
{tl.tasksCount !== undefined && tl.tasksCount > 0 && (
|
||||
<span className="ml-1 px-1.5 py-0.2 bg-zinc-800 text-[10px] text-zinc-300">
|
||||
{tl.tasksCount}
|
||||
</span>
|
||||
<span className="ml-1 badge-neutral">{tl.tasksCount}</span>
|
||||
)}
|
||||
{tl.spaceId && (
|
||||
<span title="Geteilt mit Team/Space">
|
||||
@@ -248,7 +255,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
<button
|
||||
onClick={() => handleDeleteTaskList(tl.id)}
|
||||
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" />
|
||||
</button>
|
||||
@@ -258,7 +265,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
@@ -268,11 +275,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
|
||||
{/* View Switcher: List vs Kanban */}
|
||||
<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
|
||||
onClick={() => setViewMode('list')}
|
||||
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"
|
||||
>
|
||||
@@ -282,7 +289,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
<button
|
||||
onClick={() => setViewMode('kanban')}
|
||||
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"
|
||||
>
|
||||
@@ -294,7 +303,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 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" />
|
||||
<input
|
||||
type="text"
|
||||
@@ -305,14 +317,14 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
? `Aufgabe zu "${currentSelectedList.title}" hinzufügen… (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">
|
||||
<input
|
||||
type="date"
|
||||
value={quickDueDate}
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
@@ -327,21 +339,24 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
|
||||
{/* NEW LIST MODAL */}
|
||||
{showNewListModal && (
|
||||
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm 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="fixed inset-0 modal-overlay z-50 flex items-center justify-center p-4">
|
||||
<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">
|
||||
<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" />
|
||||
<span>Neue Aufgabenliste erstellen</span>
|
||||
</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" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleCreateTaskList} className="space-y-3">
|
||||
<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">
|
||||
<input
|
||||
type="text"
|
||||
@@ -358,21 +373,23 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
placeholder="z.B. Homelab, Einkäufe, Steuern"
|
||||
autoFocus
|
||||
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>
|
||||
|
||||
{spaces.length > 0 && (
|
||||
{safeSpaces.length > 0 && (
|
||||
<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
|
||||
value={newListSpaceId}
|
||||
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>
|
||||
{spaces.map((sp) => (
|
||||
{safeSpaces.map((sp) => (
|
||||
<option key={sp.id} value={sp.id}>
|
||||
{sp.icon} {sp.name}
|
||||
</option>
|
||||
@@ -405,29 +422,27 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
{/* COLUMN 1: TODO */}
|
||||
<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">
|
||||
<span className="text-xs font-semibold text-zinc-300 flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 bg-zinc-500 inline-block" />
|
||||
<span className="text-xs font-semibold text-main flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 bg-faint inline-block" />
|
||||
<span>Zu erledigen</span>
|
||||
</span>
|
||||
<span className="px-1.5 py-0.2 bg-canvas border border-border text-[10px] text-zinc-400 font-mono">
|
||||
{todoKanban.length}
|
||||
</span>
|
||||
<span className="badge-neutral font-mono">{todoKanban.length}</span>
|
||||
</div>
|
||||
|
||||
<div className="p-2 space-y-2 min-h-[360px] flex-1 overflow-y-auto">
|
||||
{todoKanban.map((task) => (
|
||||
<div
|
||||
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">
|
||||
<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}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => handleConvertToDoc(task)}
|
||||
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" />
|
||||
</button>
|
||||
@@ -441,7 +456,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
onChange={(e) => setNotesDraft(e.target.value)}
|
||||
placeholder="Detailnotizen eingeben…"
|
||||
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">
|
||||
<button
|
||||
@@ -464,7 +479,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
setActiveNotesTaskId(task.id);
|
||||
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"
|
||||
>
|
||||
{task.notes}
|
||||
@@ -472,10 +487,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
) : null}
|
||||
|
||||
{/* 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 gap-1.5 text-zinc-400">
|
||||
<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-muted">
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
@@ -485,7 +500,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
setActiveNotesTaskId(task.id);
|
||||
setNotesDraft('');
|
||||
}}
|
||||
className="text-[10px] text-zinc-500 hover:text-zinc-300"
|
||||
className="text-[10px] text-faint hover:text-main"
|
||||
>
|
||||
+ Notiz
|
||||
</button>
|
||||
@@ -506,7 +521,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</div>
|
||||
))}
|
||||
{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>
|
||||
@@ -518,25 +533,23 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
<span className="w-2 h-2 bg-accent inline-block" />
|
||||
<span>In Bearbeitung</span>
|
||||
</span>
|
||||
<span className="px-1.5 py-0.2 bg-canvas border border-border text-[10px] text-zinc-400 font-mono">
|
||||
{inProgressKanban.length}
|
||||
</span>
|
||||
<span className="badge-neutral font-mono">{inProgressKanban.length}</span>
|
||||
</div>
|
||||
|
||||
<div className="p-2 space-y-2 min-h-[360px] flex-1 overflow-y-auto">
|
||||
{inProgressKanban.map((task) => (
|
||||
<div
|
||||
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">
|
||||
<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}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => handleConvertToDoc(task)}
|
||||
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" />
|
||||
</button>
|
||||
@@ -550,7 +563,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
onChange={(e) => setNotesDraft(e.target.value)}
|
||||
placeholder="Detailnotizen eingeben…"
|
||||
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">
|
||||
<button
|
||||
@@ -573,20 +586,20 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
setActiveNotesTaskId(task.id);
|
||||
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}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* 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
|
||||
onClick={() => handleKanbanMove(task, 'todo')}
|
||||
className="btn-action h-6 px-1.5 text-[10px] flex items-center gap-1"
|
||||
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>
|
||||
</button>
|
||||
|
||||
@@ -602,7 +615,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</div>
|
||||
))}
|
||||
{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>
|
||||
@@ -610,26 +623,24 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
{/* COLUMN 3: DONE */}
|
||||
<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">
|
||||
<span className="text-xs font-semibold text-emerald-400 flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 bg-emerald-500 inline-block" />
|
||||
<span className="text-xs font-semibold text-status-ok flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 bg-status-ok inline-block" />
|
||||
<span>Erledigt</span>
|
||||
</span>
|
||||
<span className="px-1.5 py-0.2 bg-canvas border border-border text-[10px] text-zinc-400 font-mono">
|
||||
{doneKanban.length}
|
||||
</span>
|
||||
<span className="badge-neutral font-mono">{doneKanban.length}</span>
|
||||
</div>
|
||||
|
||||
<div className="p-2 space-y-2 min-h-[360px] flex-1 overflow-y-auto">
|
||||
{doneKanban.map((task) => (
|
||||
<div
|
||||
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}
|
||||
</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
|
||||
onClick={() => handleKanbanMove(task, 'todo')}
|
||||
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
|
||||
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"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
@@ -649,7 +660,9 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</div>
|
||||
))}
|
||||
{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>
|
||||
@@ -659,11 +672,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
<div className="space-y-2">
|
||||
{/* Subfilter tabs for List View */}
|
||||
<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
|
||||
onClick={() => setFilter('open')}
|
||||
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})
|
||||
@@ -672,7 +685,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
<button
|
||||
onClick={() => setFilter('done')}
|
||||
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})
|
||||
@@ -681,14 +694,14 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
<button
|
||||
onClick={() => setFilter('discarded')}
|
||||
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>
|
||||
</div>
|
||||
|
||||
{filter === 'discarded' && discarded.length > 0 && (
|
||||
{filter === 'discarded' && safeDiscarded.length > 0 && (
|
||||
<button onClick={handlePurgeAllDiscarded} className="btn-action">
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
Papierkorb leeren
|
||||
@@ -697,22 +710,22 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
</div>
|
||||
|
||||
{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' ? (
|
||||
<div className="panel-surface divide-y divide-border">
|
||||
{discarded.length === 0 ? (
|
||||
<div className="px-4 py-6 text-center text-zinc-400 text-sm">
|
||||
{safeDiscarded.length === 0 ? (
|
||||
<div className="px-4 py-6 text-center text-muted text-sm">
|
||||
Keine verworfenen Gedanken im Papierkorb.
|
||||
</div>
|
||||
) : (
|
||||
discarded.map((item) => (
|
||||
safeDiscarded.map((item) => (
|
||||
<div
|
||||
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">
|
||||
<Trash2 className="w-4 h-4 text-zinc-500 flex-shrink-0" />
|
||||
<p className="text-sm text-zinc-400 line-through truncate">{item.content}</p>
|
||||
<Trash2 className="w-4 h-4 text-faint flex-shrink-0" />
|
||||
<p className="text-sm text-muted line-through truncate">{item.content}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<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">
|
||||
{(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'
|
||||
? 'Keine offenen Aufgaben vorhanden.'
|
||||
: 'Noch keine erledigten Aufgaben.'}
|
||||
@@ -742,18 +755,18 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
(filter === 'open' ? openTasks : doneTasks).map((task) => (
|
||||
<div
|
||||
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' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3 min-w-0 flex-1">
|
||||
{/* Square minimalist checkbox */}
|
||||
{/* Theme-controlled checkbox (--radius-checkbox) */}
|
||||
<button
|
||||
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
|
||||
? 'bg-accent border-accent text-white'
|
||||
: 'border-zinc-500 hover:border-accent text-transparent'
|
||||
? 'bg-accent border-accent text-on-accent'
|
||||
: 'border-strong hover:border-accent text-transparent'
|
||||
}`}
|
||||
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">
|
||||
<p
|
||||
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}
|
||||
@@ -777,7 +790,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
onChange={(e) => setNotesDraft(e.target.value)}
|
||||
placeholder="Detailnotizen eingeben…"
|
||||
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">
|
||||
<button
|
||||
@@ -800,7 +813,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
setActiveNotesTaskId(task.id);
|
||||
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"
|
||||
>
|
||||
{task.notes}
|
||||
@@ -812,20 +825,15 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
<span className="badge-accent">Dringend</span>
|
||||
)}
|
||||
{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
|
||||
</span>
|
||||
)}
|
||||
{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]">
|
||||
⚡ In Bearbeitung
|
||||
</span>
|
||||
<span className="badge-accent">⚡ In Bearbeitung</span>
|
||||
)}
|
||||
{task.tags?.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-1.5 py-0.2 bg-canvas text-zinc-400 text-[10px]"
|
||||
>
|
||||
<span key={tag} className="badge-neutral">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
@@ -850,7 +858,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
)}
|
||||
|
||||
<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
|
||||
type="date"
|
||||
value={task.dueDate || ''}
|
||||
|
||||
@@ -193,19 +193,19 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
switch (priority) {
|
||||
case 'urgent':
|
||||
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
|
||||
</span>
|
||||
);
|
||||
case 'high':
|
||||
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
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
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
|
||||
</span>
|
||||
);
|
||||
@@ -215,14 +215,14 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
const sourceBadge = (source: string) => {
|
||||
if (source === 'voice') {
|
||||
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
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (source === 'webhook') {
|
||||
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
|
||||
</span>
|
||||
);
|
||||
@@ -232,13 +232,15 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
|
||||
if (!activeCard) {
|
||||
return (
|
||||
<div className="max-w-xl mx-auto mt-2 p-6 bg-panel border border-border 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="max-w-xl mx-auto mt-2 p-6 panel-surface text-center">
|
||||
<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" />
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-white mb-1">Triage-Deck ist leer</h2>
|
||||
<p className="text-zinc-400 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.
|
||||
<h2 className="text-lg font-bold text-main mb-1">Triage-Deck ist leer</h2>
|
||||
<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 rounded-btn text-main">C</kbd>
|
||||
), um neue Gedanken reinzurotzen.
|
||||
</p>
|
||||
|
||||
<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">
|
||||
{cards.length} {cards.length === 1 ? 'Quick Dump' : 'Quick Dumps'} offen
|
||||
</span>
|
||||
<span className="hidden sm:inline text-xs text-zinc-600">•</span>
|
||||
<span className="hidden sm:flex items-center gap-1 text-xs text-zinc-400">
|
||||
<span className="hidden sm:inline text-xs text-faint">•</span>
|
||||
<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
|
||||
</span>
|
||||
</div>
|
||||
@@ -294,11 +296,11 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
{nextCard && (
|
||||
<div
|
||||
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>
|
||||
<p className="text-sm text-zinc-400 line-clamp-2">{nextCard.content}</p>
|
||||
<div className="text-[11px] text-zinc-500">Nächster Quick Dump…</div>
|
||||
<p className="text-sm text-muted line-clamp-2">{nextCard.content}</p>
|
||||
<div className="text-[11px] text-faint">Nächster Quick Dump…</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -321,19 +323,19 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
initial={{ scale: 0.97, opacity: 0, y: 10 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 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 */}
|
||||
<motion.div
|
||||
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 👉
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
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
|
||||
</motion.div>
|
||||
@@ -352,12 +354,12 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
setIsEditingCard((prev) => !prev);
|
||||
}}
|
||||
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" />
|
||||
<span>Bearbeiten</span>
|
||||
</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" />
|
||||
{new Date(activeCard.createdAt).toLocaleString('de-DE', {
|
||||
day: '2-digit',
|
||||
@@ -378,7 +380,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
onChange={(e) => setEditDraft(e.target.value)}
|
||||
rows={3}
|
||||
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">
|
||||
<button
|
||||
@@ -398,7 +400,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
</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}
|
||||
</p>
|
||||
)}
|
||||
@@ -414,14 +416,14 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-zinc-500">
|
||||
<span className="text-xs text-faint">
|
||||
Links wischen = Wegwerfen • Rechts wischen = Als Aufgabe sichern
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
@@ -432,8 +434,8 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
|
||||
{/* Convert to Task with Due Date Popover */}
|
||||
{showDateModal && (
|
||||
<div className="p-3 bg-panel border border-border flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 text-xs text-zinc-300">
|
||||
<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-muted">
|
||||
<Calendar className="w-4 h-4 text-accent" />
|
||||
<span>Aufgabe mit Fälligkeitsdatum anlegen:</span>
|
||||
</div>
|
||||
@@ -456,8 +458,8 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
|
||||
{/* Convert to Shared List Item Popover */}
|
||||
{showListSelector && (
|
||||
<div className="p-3 bg-panel border border-border">
|
||||
<p className="text-xs text-zinc-400 mb-2">
|
||||
<div className="p-3 panel-surface">
|
||||
<p className="text-xs text-muted mb-2">
|
||||
Quick Dump direkt einer Liste zuordnen:
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
@@ -475,7 +477,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-zinc-400">Keine Listen vorhanden.</span>
|
||||
<span className="text-xs text-muted">Keine Listen vorhanden.</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -489,7 +491,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
title="Wegwerfen (Taste H oder Pfeil links)"
|
||||
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>
|
||||
</button>
|
||||
|
||||
@@ -502,7 +504,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
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>
|
||||
</button>
|
||||
|
||||
@@ -514,7 +516,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
}}
|
||||
title="Auf eine geteilte Liste setzen"
|
||||
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" />
|
||||
@@ -539,7 +541,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
}}
|
||||
title="Als Aufgabe mit Fälligkeitsdatum (Taste T)"
|
||||
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" />
|
||||
@@ -561,16 +563,16 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
{cards.length > 1 && (
|
||||
<div className="pt-2">
|
||||
<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" />
|
||||
Warteschlange ({cards.length - 1} weitere Quick Dumps)
|
||||
</span>
|
||||
<span className="text-[11px] text-zinc-500">
|
||||
<span className="text-[11px] text-faint">
|
||||
Klicken zum Vorholen oder direkt zuordnen
|
||||
</span>
|
||||
</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) => (
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
@@ -593,7 +595,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
handleAction('keep', { convertTo: 'task', thoughtOverride: item })
|
||||
}
|
||||
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" />
|
||||
<span className="hidden sm:inline">Aufgabe</span>
|
||||
@@ -605,7 +607,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
handleAction('keep', { convertTo: 'document', thoughtOverride: item })
|
||||
}
|
||||
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" />
|
||||
<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`}
|
||||
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" />
|
||||
<span className="hidden md:inline">{lists[0].title}</span>
|
||||
@@ -633,7 +635,7 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
type="button"
|
||||
onClick={() => handleAction('discard', { thoughtOverride: item })}
|
||||
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" />
|
||||
</button>
|
||||
|
||||
@@ -6,15 +6,12 @@ import {
|
||||
FolderOpen,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
FileText,
|
||||
Search,
|
||||
Pin,
|
||||
Trash2,
|
||||
Download,
|
||||
CheckSquare,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
List,
|
||||
Code,
|
||||
Sparkles,
|
||||
@@ -389,10 +386,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
e.stopPropagation();
|
||||
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
|
||||
? '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`}
|
||||
>
|
||||
@@ -467,8 +464,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
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 ${
|
||||
isSelected
|
||||
? 'bg-panel-elevated border-accent text-white font-medium'
|
||||
: 'border-transparent text-zinc-400 hover:text-white hover:bg-white/[0.02]'
|
||||
? 'bg-panel-elevated border-accent text-main font-medium'
|
||||
: 'border-transparent text-muted hover:text-main hover:bg-canvas/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
@@ -490,10 +487,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<div
|
||||
onClick={() => toggleFolder(node.path)}
|
||||
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">
|
||||
<span className="text-zinc-500 hover:text-zinc-300">
|
||||
<span className="text-faint hover:text-muted">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-3 h-3 flex-shrink-0" />
|
||||
) : (
|
||||
@@ -503,10 +500,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
{isExpanded ? (
|
||||
<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="text-[10px] text-zinc-500 font-mono">({totalItems})</span>
|
||||
<span className="font-medium text-xs text-main truncate">{node.name}</span>
|
||||
<span className="text-[10px] text-faint font-mono">({totalItems})</span>
|
||||
</div>
|
||||
|
||||
<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();
|
||||
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"
|
||||
>
|
||||
<FilePlus className="w-3 h-3" />
|
||||
@@ -527,7 +524,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
e.stopPropagation();
|
||||
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"
|
||||
>
|
||||
<FolderPlus className="w-3 h-3" />
|
||||
@@ -540,7 +537,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
style={{ paddingLeft: `${(node.depth + 1) * 14 + 8}px` }}
|
||||
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
|
||||
type="text"
|
||||
autoFocus
|
||||
@@ -556,7 +553,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleConfirmCreateFolder}
|
||||
className="p-1 text-accent hover:text-white"
|
||||
className="p-1 text-accent hover:text-main rounded-btn"
|
||||
title="Bestätigen"
|
||||
>
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
@@ -564,7 +561,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreatingFolder(false)}
|
||||
className="p-1 text-zinc-400 hover:text-white"
|
||||
className="p-1 text-muted hover:text-main rounded-btn"
|
||||
title="Abbrechen"
|
||||
>
|
||||
<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 && (
|
||||
<div
|
||||
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)
|
||||
</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="p-2 panel-header space-y-2">
|
||||
<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
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
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>
|
||||
|
||||
@@ -615,7 +612,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
className="flex-1 btn-action"
|
||||
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>
|
||||
</button>
|
||||
|
||||
@@ -625,7 +622,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
className="flex-1 btn-action"
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
@@ -633,7 +630,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
{isCreatingFolder && newFolderParent === null && (
|
||||
<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
|
||||
type="text"
|
||||
autoFocus
|
||||
@@ -644,12 +641,12 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
if (e.key === 'Escape') setIsCreatingFolder(false);
|
||||
}}
|
||||
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
|
||||
type="button"
|
||||
onClick={handleConfirmCreateFolder}
|
||||
className="p-1 text-accent hover:text-white"
|
||||
className="p-1 text-accent hover:text-main rounded-btn"
|
||||
title="Bestätigen"
|
||||
>
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
@@ -657,7 +654,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreatingFolder(false)}
|
||||
className="p-1 text-zinc-400 hover:text-white"
|
||||
className="p-1 text-muted hover:text-main rounded-btn"
|
||||
title="Abbrechen"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
@@ -666,10 +663,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
)}
|
||||
|
||||
{isEmpty ? (
|
||||
<div className="p-6 my-3 mx-2 border border-dashed border-border bg-canvas/50 text-center">
|
||||
<Folder className="w-7 h-7 text-zinc-600 mx-auto mb-2" />
|
||||
<h5 className="text-xs font-semibold text-zinc-300 mb-1">Keine Dateien vorhanden</h5>
|
||||
<p className="text-[11px] text-zinc-500 mb-3 max-w-xs mx-auto leading-relaxed">
|
||||
<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-faint mx-auto mb-2" />
|
||||
<h5 className="text-xs font-semibold text-muted mb-1">Keine Dateien vorhanden</h5>
|
||||
<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.
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
@@ -678,7 +675,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
onClick={() => handleCreateDoc(null)}
|
||||
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>
|
||||
</button>
|
||||
<button
|
||||
@@ -686,7 +683,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
onClick={() => handleStartCreateFolder(null)}
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
@@ -696,7 +693,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
{rootTree.map(renderFolder)}
|
||||
{rootDocs.map((doc) => renderDocItem(doc, 0))}
|
||||
{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 "{searchQuery}"
|
||||
</div>
|
||||
)}
|
||||
@@ -710,14 +707,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<div className="p-4 panel-surface space-y-3">
|
||||
{/* 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 gap-2 text-xs text-zinc-400">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted">
|
||||
<span className="badge-accent">
|
||||
<Check className="w-3 h-3" />
|
||||
{saveState === 'saving' ? 'Speichert…' : 'Gespeichert'}
|
||||
</span>
|
||||
|
||||
<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
|
||||
value={getDocFolder(selectedDoc)}
|
||||
onChange={(e) => handleChangeDocFolder(e.target.value)}
|
||||
@@ -773,14 +770,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
{selectedDoc.icon}
|
||||
</button>
|
||||
|
||||
{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) => (
|
||||
<button
|
||||
key={ic}
|
||||
@@ -789,7 +786,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
persistDocument({ ...selectedDoc, icon: ic });
|
||||
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}
|
||||
</button>
|
||||
@@ -802,17 +799,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
value={selectedDoc.title}
|
||||
onChange={(e) => persistDocument({ ...selectedDoc, title: e.target.value })}
|
||||
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>
|
||||
|
||||
{/* 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 */}
|
||||
<button
|
||||
type="button"
|
||||
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**)"
|
||||
>
|
||||
<Bold className="w-3.5 h-3.5" />
|
||||
@@ -820,7 +817,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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*)"
|
||||
>
|
||||
<Italic className="w-3.5 h-3.5" />
|
||||
@@ -828,7 +825,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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~~)"
|
||||
>
|
||||
<Strikethrough className="w-3.5 h-3.5" />
|
||||
@@ -836,7 +833,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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`)"
|
||||
>
|
||||
<Code className="w-3.5 h-3.5" />
|
||||
@@ -844,7 +841,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<Link className="w-3.5 h-3.5" />
|
||||
@@ -857,14 +854,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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]])"
|
||||
>
|
||||
<span>[[ Wiki-Link ]]</span>
|
||||
</button>
|
||||
{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">
|
||||
<p className="text-[10px] text-zinc-400 px-1.5 pb-1 border-b border-border">
|
||||
<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-muted px-1.5 pb-1 border-b border-border">
|
||||
Dokument zum Verlinken wählen:
|
||||
</p>
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
@@ -875,14 +872,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
key={d.id}
|
||||
type="button"
|
||||
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 className="truncate">{d.title}</span>
|
||||
</button>
|
||||
))}
|
||||
{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
|
||||
</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 |'
|
||||
)
|
||||
}
|
||||
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"
|
||||
>
|
||||
<TableIcon className="w-3 h-3 text-accent" />
|
||||
@@ -913,7 +910,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<Percent className="w-3 h-3 text-accent" />
|
||||
@@ -923,7 +920,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<Minus className="w-3.5 h-3.5" />
|
||||
@@ -946,7 +943,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
value={block.content}
|
||||
onChange={(e) => updateBlock(block.id, { content: e.target.value })}
|
||||
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}
|
||||
onChange={(e) => updateBlock(block.id, { content: e.target.value })}
|
||||
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}
|
||||
onChange={(e) => updateBlock(block.id, { content: e.target.value })}
|
||||
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}
|
||||
onChange={(e) => updateBlock(block.id, { content: e.target.value })}
|
||||
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('[[') && (
|
||||
<div className="text-xs text-zinc-400 pt-0.5">
|
||||
<div className="text-xs text-muted pt-0.5">
|
||||
{renderTextWithWikiLinks(block.content)}
|
||||
</div>
|
||||
)}
|
||||
@@ -997,7 +994,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
type="checkbox"
|
||||
checked={!!block.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">
|
||||
<input
|
||||
@@ -1005,12 +1002,12 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
value={block.content}
|
||||
onChange={(e) => updateBlock(block.id, { content: e.target.value })}
|
||||
placeholder="To-Do Punkt…"
|
||||
className={`w-full bg-transparent text-xs sm:text-sm focus:outline-none ${
|
||||
block.checked ? 'line-through text-zinc-500' : 'text-white'
|
||||
className={`w-full bg-transparent text-xs sm:text-sm focus:outline-none placeholder:text-faint ${
|
||||
block.checked ? 'line-through text-faint' : 'text-main'
|
||||
}`}
|
||||
/>
|
||||
{block.content.includes('[[') && (
|
||||
<div className="text-xs text-zinc-400">
|
||||
<div className="text-xs text-muted">
|
||||
{renderTextWithWikiLinks(block.content)}
|
||||
</div>
|
||||
)}
|
||||
@@ -1021,17 +1018,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
{/* BULLET */}
|
||||
{block.type === 'bullet' && (
|
||||
<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">
|
||||
<input
|
||||
type="text"
|
||||
value={block.content}
|
||||
onChange={(e) => updateBlock(block.id, { content: e.target.value })}
|
||||
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('[[') && (
|
||||
<div className="text-xs text-zinc-400">
|
||||
<div className="text-xs text-muted">
|
||||
{renderTextWithWikiLinks(block.content)}
|
||||
</div>
|
||||
)}
|
||||
@@ -1041,7 +1038,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
{/* 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" />
|
||||
<div className="flex-1 space-y-1">
|
||||
<textarea
|
||||
@@ -1049,10 +1046,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
value={block.content}
|
||||
onChange={(e) => updateBlock(block.id, { content: e.target.value })}
|
||||
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('[[') && (
|
||||
<div className="text-xs text-zinc-400">
|
||||
<div className="text-xs text-muted">
|
||||
{renderTextWithWikiLinks(block.content)}
|
||||
</div>
|
||||
)}
|
||||
@@ -1062,22 +1059,22 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
{/* 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
|
||||
rows={Math.max(2, (block.content || '').split('\n').length)}
|
||||
value={block.content}
|
||||
onChange={(e) => updateBlock(block.id, { content: e.target.value })}
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* TABLE */}
|
||||
{block.type === 'table' && (
|
||||
<div className="p-2.5 bg-canvas border border-border space-y-2">
|
||||
<div className="flex items-center justify-between text-[11px] text-zinc-400 border-b border-border pb-1">
|
||||
<span className="flex items-center gap-1 font-semibold text-zinc-300">
|
||||
<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-muted border-b border-border pb-1">
|
||||
<span className="flex items-center gap-1 font-semibold text-main">
|
||||
<TableIcon className="w-3 h-3 text-accent" /> Markdown Tabelle
|
||||
</span>
|
||||
</div>
|
||||
@@ -1086,25 +1083,25 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
value={block.content}
|
||||
onChange={(e) => updateBlock(block.id, { content: e.target.value })}
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* PROGRESS BAR */}
|
||||
{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">
|
||||
<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
|
||||
</span>
|
||||
<span className="font-mono text-accent font-semibold">
|
||||
{parseInt(block.content) || 0}%
|
||||
</span>
|
||||
</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
|
||||
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))}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -1132,7 +1129,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<ArrowUp className="w-3 h-3" />
|
||||
@@ -1140,7 +1137,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<ArrowDown className="w-3 h-3" />
|
||||
@@ -1148,7 +1145,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
@@ -1160,7 +1157,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
{/* Bottom Add Block Toolbar */}
|
||||
<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
|
||||
type="button"
|
||||
@@ -1242,7 +1239,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
</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.
|
||||
</div>
|
||||
)}
|
||||
|
||||
+123
-50
@@ -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 components;
|
||||
@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;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
@@ -52,9 +28,10 @@ body {
|
||||
color: var(--text-main);
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
overflow-x: hidden;
|
||||
transition: background-color 140ms ease, color 140ms ease;
|
||||
}
|
||||
|
||||
/* Subtiler monochromer Scrollbar */
|
||||
/* Scrollbar über Theme-Variablen */
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
@@ -65,16 +42,16 @@ body {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: var(--radius-btn);
|
||||
}
|
||||
|
||||
::-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 {
|
||||
@@ -85,11 +62,33 @@ body {
|
||||
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 {
|
||||
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 {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -122,8 +121,9 @@ body {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
background-color: var(--accent-primary);
|
||||
border: 1px solid var(--accent-border);
|
||||
border-radius: var(--radius-btn);
|
||||
color: #ffffff;
|
||||
color: var(--text-on-accent);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
transition: all 120ms ease;
|
||||
@@ -133,7 +133,7 @@ body {
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Kleiner eckiger Icon-Button */
|
||||
/* Kleiner Icon-Button */
|
||||
.btn-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -166,9 +166,9 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-icon-danger:hover {
|
||||
background-color: rgba(239, 68, 68, 0.15);
|
||||
background-color: var(--bg-danger);
|
||||
color: var(--color-danger);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
border-color: var(--border-danger);
|
||||
}
|
||||
|
||||
/* Zentrales Texteingabefeld */
|
||||
@@ -211,7 +211,7 @@ body {
|
||||
background-color: var(--bg-panel-elevated);
|
||||
}
|
||||
|
||||
/* Status Badge */
|
||||
/* Status Badges */
|
||||
.badge-accent {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -219,11 +219,84 @@ body {
|
||||
padding: 0.125rem 0.5rem;
|
||||
background-color: var(--accent-dim);
|
||||
border: 1px solid var(--accent-border);
|
||||
border-radius: var(--radius-btn);
|
||||
color: var(--accent-primary);
|
||||
font-size: 0.75rem;
|
||||
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 */
|
||||
.tree-node-active {
|
||||
background-color: var(--bg-panel-elevated);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -17,6 +17,13 @@ export default {
|
||||
hover: 'var(--bg-panel-hover)',
|
||||
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: {
|
||||
DEFAULT: 'var(--accent-primary)',
|
||||
hover: 'var(--accent-hover)',
|
||||
@@ -28,20 +35,45 @@ export default {
|
||||
hover: 'var(--accent-hover)',
|
||||
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)',
|
||||
},
|
||||
borderColor: {
|
||||
DEFAULT: 'var(--border-color)',
|
||||
border: 'var(--border-color)',
|
||||
subtle: 'var(--border-subtle)',
|
||||
strong: 'var(--border-strong)',
|
||||
},
|
||||
borderRadius: {
|
||||
card: 'var(--radius-card)',
|
||||
btn: 'var(--radius-btn)',
|
||||
input: 'var(--radius-input)',
|
||||
checkbox: 'var(--radius-checkbox)',
|
||||
pill: 'var(--radius-pill)',
|
||||
},
|
||||
boxShadow: {
|
||||
panel: 'var(--shadow-panel)',
|
||||
},
|
||||
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'],
|
||||
},
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user