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

This commit is contained in:
sandrodach committed 2026-10-24 23:26:31 +02:00
1 parent 0f24cbdbf4
commit b3e651b3b9
18 files changed
+1453 -802

No files matched your search

+133 -119
View File
@@ -9,10 +9,16 @@
- [x] **CI/CD & Multi-Stage Docker Build (`Dockerfile`, `.gitea/workflows/build-and-publish.yaml`):**
- 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
View File
@@ -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();
+72 -69
View File
@@ -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>
+40 -55
View File
@@ -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 &quot;Teams isoliert&quot; 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 &amp; Schatten</strong>, <strong>5. Statusfarben</strong> und{' '}
<strong>6. Rundungen</strong>.
</p>
</div>
</div>
)}
{/* ================================================================= */}
{/* SECTION 3: ALARME (MULTI-CHANNEL: EMAIL, DISCORD, CUSTOM WEBHOOK) */}
{/* ================================================================= */}
{section === 'alarms' && (
<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
+110 -102
View File
@@ -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 || ''}
+43 -41
View File
@@ -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 &quot;{searchQuery}&quot;
</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
View File
@@ -1,47 +1,23 @@
/* ==========================================================================
MINDSIFT THEME ENGINE — ZENTRALE EINBINDUNG
Jedes Theme ist eine eigenständige CSS-Datei unter src/themes/ mit 6 Gruppen:
1. Akzentfarbe
2. Oberflächen & Hintergründe
3. Text & Kontraste
4. Rahmen, Linien & Schatten
5. Status- & Signalfarben
6. Rundungen (Geometrie)
========================================================================== */
@import './themes/theme-dark.css';
@import './themes/theme-light.css';
@import './themes/theme-high-contrast.css';
@import './themes/theme-soft.css';
@tailwind base;
@tailwind 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);
+93
View File
@@ -0,0 +1,93 @@
export type ThemeId = 'dark' | 'light' | 'high-contrast' | 'soft';
export interface ThemeOption {
id: ThemeId;
name: string;
subtitle: string;
file: string;
previewBg: string;
previewPanel: string;
previewAccent: string;
previewText: string;
isRounded: boolean;
}
export const THEME_STORAGE_KEY = 'mindsift_active_theme';
export const AVAILABLE_THEMES: ThemeOption[] = [
{
id: 'dark',
name: 'MindSift Dark (Standard)',
subtitle: 'Monochrom, dunkel & eckig (0px)',
file: 'src/themes/theme-dark.css',
previewBg: '#121316',
previewPanel: '#1a1c20',
previewAccent: '#2563eb',
previewText: '#f4f4f5',
isRounded: false,
},
{
id: 'light',
name: 'MindSift Light (Tagmodus)',
subtitle: 'Helles Papier-Design & klare Kontraste',
file: 'src/themes/theme-light.css',
previewBg: '#f3f4f6',
previewPanel: '#ffffff',
previewAccent: '#2563eb',
previewText: '#111827',
isRounded: false,
},
{
id: 'high-contrast',
name: 'High Contrast (Scharf)',
subtitle: 'Tiefschwarz (#000) & weiße Konturen',
file: 'src/themes/theme-high-contrast.css',
previewBg: '#000000',
previewPanel: '#09090b',
previewAccent: '#38bdf8',
previewText: '#ffffff',
isRounded: false,
},
{
id: 'soft',
name: 'Soft Slate (Abgerundet)',
subtitle: 'Schiefer-Blau mit weichen Radien (10px)',
file: 'src/themes/theme-soft.css',
previewBg: '#0f172a',
previewPanel: '#1e293b',
previewAccent: '#3b82f6',
previewText: '#f8fafc',
isRounded: true,
},
];
export function getActiveTheme(): ThemeId {
try {
const saved = localStorage.getItem(THEME_STORAGE_KEY) as ThemeId | null;
if (saved && AVAILABLE_THEMES.some((t) => t.id === saved)) {
return saved;
}
} catch {
// ignore
}
return 'dark';
}
export function setActiveTheme(themeId: ThemeId): void {
try {
localStorage.setItem(THEME_STORAGE_KEY, themeId);
} catch {
// ignore
}
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-theme', themeId);
}
}
export function initThemeOnBoot(): ThemeId {
const active = getActiveTheme();
if (typeof document !== 'undefined') {
document.documentElement.setAttribute('data-theme', active);
}
return active;
}
+56
View File
@@ -0,0 +1,56 @@
/* ==========================================================================
MINDSIFT THEME: DARK MONOCHROME (Standard)
Alle Farben, Kontraste und Rundungen zentral in einer einzigen Datei.
========================================================================== */
:root,
:root[data-theme="dark"] {
/* --- 1. AKZENTFARBE --- */
--accent-primary: #2563eb;
--accent-hover: #3b82f6;
--accent-dim: rgba(37, 99, 235, 0.14);
--accent-border: rgba(59, 130, 246, 0.45);
--text-on-accent: #ffffff;
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE --- */
--bg-app: #121316;
--bg-panel: #1a1c20;
--bg-panel-elevated: #22252a;
--bg-panel-hover: #2a2e35;
--bg-input: #121316;
--bg-overlay: rgba(0, 0, 0, 0.72);
--bg-code: #0d0e11;
/* --- 3. TEXT & KONTRASTE --- */
--text-main: #f4f4f5;
--text-muted: #a1a1aa;
--text-faint: #71717a;
/* --- 4. RAHMEN, LINIEN & SCHATTEN --- */
--border-color: rgba(255, 255, 255, 0.09);
--border-subtle: rgba(255, 255, 255, 0.05);
--border-strong: rgba(255, 255, 255, 0.22);
--shadow-panel: 0 10px 30px rgba(0, 0, 0, 0.45);
--scrollbar-thumb: rgba(255, 255, 255, 0.14);
--scrollbar-thumb-hover: rgba(255, 255, 255, 0.26);
/* --- 5. STATUS- & SIGNALFARBEN --- */
--color-success: #22c55e;
--bg-success: rgba(34, 197, 94, 0.12);
--border-success: rgba(34, 197, 94, 0.38);
--color-danger: #ef4444;
--bg-danger: rgba(239, 68, 68, 0.12);
--border-danger: rgba(239, 68, 68, 0.38);
--color-warning: #f59e0b;
--bg-warning: rgba(245, 158, 11, 0.12);
--border-warning: rgba(245, 158, 11, 0.38);
/* --- 6. RUNDUNGEN (Geometrie) --- */
--radius-card: 0px;
--radius-btn: 2px;
--radius-input: 2px;
--radius-checkbox: 0px;
--radius-pill: 9999px;
}
@@ -0,0 +1,55 @@
/* ==========================================================================
MINDSIFT THEME: HIGH CONTRAST (Maximaler Kontrast & Schärfe)
Alle Farben, Kontraste und Rundungen zentral in einer einzigen Datei.
========================================================================== */
:root[data-theme="high-contrast"] {
/* --- 1. AKZENTFARBE --- */
--accent-primary: #38bdf8;
--accent-hover: #7dd3fc;
--accent-dim: rgba(56, 189, 248, 0.22);
--accent-border: #38bdf8;
--text-on-accent: #000000;
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE --- */
--bg-app: #000000;
--bg-panel: #09090b;
--bg-panel-elevated: #18181b;
--bg-panel-hover: #27272a;
--bg-input: #000000;
--bg-overlay: rgba(0, 0, 0, 0.88);
--bg-code: #000000;
/* --- 3. TEXT & KONTRASTE --- */
--text-main: #ffffff;
--text-muted: #e4e4e7;
--text-faint: #a1a1aa;
/* --- 4. RAHMEN, LINIEN & SCHATTEN --- */
--border-color: rgba(255, 255, 255, 0.38);
--border-subtle: rgba(255, 255, 255, 0.20);
--border-strong: #ffffff;
--shadow-panel: 0 0 0 1px #ffffff;
--scrollbar-thumb: rgba(255, 255, 255, 0.45);
--scrollbar-thumb-hover: #ffffff;
/* --- 5. STATUS- & SIGNALFARBEN --- */
--color-success: #4ade80;
--bg-success: rgba(74, 222, 128, 0.20);
--border-success: #4ade80;
--color-danger: #f87171;
--bg-danger: rgba(248, 113, 113, 0.20);
--border-danger: #f87171;
--color-warning: #fbbf24;
--bg-warning: rgba(251, 191, 36, 0.20);
--border-warning: #fbbf24;
/* --- 6. RUNDUNGEN (Geometrie: Komplett scharfkantig) --- */
--radius-card: 0px;
--radius-btn: 0px;
--radius-input: 0px;
--radius-checkbox: 0px;
--radius-pill: 0px;
}
+55
View File
@@ -0,0 +1,55 @@
/* ==========================================================================
MINDSIFT THEME: LIGHT (Tagmodus / Hell & Klar)
Alle Farben, Kontraste und Rundungen zentral in einer einzigen Datei.
========================================================================== */
:root[data-theme="light"] {
/* --- 1. AKZENTFARBE --- */
--accent-primary: #2563eb;
--accent-hover: #1d4ed8;
--accent-dim: rgba(37, 99, 235, 0.10);
--accent-border: rgba(37, 99, 235, 0.38);
--text-on-accent: #ffffff;
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE --- */
--bg-app: #f3f4f6;
--bg-panel: #ffffff;
--bg-panel-elevated: #ebedf0;
--bg-panel-hover: #e2e5ea;
--bg-input: #ffffff;
--bg-overlay: rgba(15, 23, 42, 0.45);
--bg-code: #f1f5f9;
/* --- 3. TEXT & KONTRASTE --- */
--text-main: #111827;
--text-muted: #4b5563;
--text-faint: #9ca3af;
/* --- 4. RAHMEN, LINIEN & SCHATTEN --- */
--border-color: rgba(17, 24, 39, 0.13);
--border-subtle: rgba(17, 24, 39, 0.06);
--border-strong: rgba(17, 24, 39, 0.28);
--shadow-panel: 0 8px 24px rgba(15, 23, 42, 0.08);
--scrollbar-thumb: rgba(17, 24, 39, 0.18);
--scrollbar-thumb-hover: rgba(17, 24, 39, 0.32);
/* --- 5. STATUS- & SIGNALFARBEN --- */
--color-success: #15803d;
--bg-success: rgba(22, 163, 74, 0.10);
--border-success: rgba(22, 163, 74, 0.35);
--color-danger: #b91c1c;
--bg-danger: rgba(220, 38, 38, 0.09);
--border-danger: rgba(220, 38, 38, 0.35);
--color-warning: #b45309;
--bg-warning: rgba(217, 119, 6, 0.10);
--border-warning: rgba(217, 119, 6, 0.35);
/* --- 6. RUNDUNGEN (Geometrie) --- */
--radius-card: 0px;
--radius-btn: 2px;
--radius-input: 2px;
--radius-checkbox: 0px;
--radius-pill: 9999px;
}
+56
View File
@@ -0,0 +1,56 @@
/* ==========================================================================
MINDSIFT THEME: SOFT SLATE (Abgerundete Geometrie & Sanftes Schiefer-Blau)
Demonstriert, wie über die Rundungs-Variablen in einer einzigen Datei
das gesamte App-Layout auf abgerundete Ecken umschaltet.
========================================================================== */
:root[data-theme="soft"] {
/* --- 1. AKZENTFARBE --- */
--accent-primary: #3b82f6;
--accent-hover: #60a5fa;
--accent-dim: rgba(59, 130, 246, 0.15);
--accent-border: rgba(96, 165, 250, 0.40);
--text-on-accent: #ffffff;
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE --- */
--bg-app: #0f172a;
--bg-panel: #1e293b;
--bg-panel-elevated: #293548;
--bg-panel-hover: #334155;
--bg-input: #0f172a;
--bg-overlay: rgba(2, 6, 23, 0.72);
--bg-code: #090d16;
/* --- 3. TEXT & KONTRASTE --- */
--text-main: #f8fafc;
--text-muted: #94a3b8;
--text-faint: #64748b;
/* --- 4. RAHMEN, LINIEN & SCHATTEN --- */
--border-color: rgba(148, 163, 184, 0.16);
--border-subtle: rgba(148, 163, 184, 0.08);
--border-strong: rgba(148, 163, 184, 0.32);
--shadow-panel: 0 12px 32px rgba(2, 6, 23, 0.40);
--scrollbar-thumb: rgba(148, 163, 184, 0.22);
--scrollbar-thumb-hover: rgba(148, 163, 184, 0.38);
/* --- 5. STATUS- & SIGNALFARBEN --- */
--color-success: #10b981;
--bg-success: rgba(16, 185, 129, 0.14);
--border-success: rgba(16, 185, 129, 0.38);
--color-danger: #f43f5e;
--bg-danger: rgba(244, 63, 94, 0.14);
--border-danger: rgba(244, 63, 94, 0.38);
--color-warning: #f59e0b;
--bg-warning: rgba(245, 158, 11, 0.14);
--border-warning: rgba(245, 158, 11, 0.38);
/* --- 6. RUNDUNGEN (Weich abgerundete Ecken überall) --- */
--radius-card: 10px;
--radius-btn: 6px;
--radius-input: 6px;
--radius-checkbox: 4px;
--radius-pill: 9999px;
}
+56
View File
@@ -0,0 +1,56 @@
/* ==========================================================================
MINDSIFT THEME-VORLAGE (TEMPLATE)
Kopiere diese Datei, benenne [data-theme="mein-theme"] um und passe
die 6 Gruppen nach deinen Wünschen an.
========================================================================== */
:root[data-theme="custom-template"] {
/* --- 1. AKZENTFARBE (Buttons, aktive Tabs, Fokus-Ränder, Badges) --- */
--accent-primary: #2563eb;
--accent-hover: #3b82f6;
--accent-dim: rgba(37, 99, 235, 0.14);
--accent-border: rgba(59, 130, 246, 0.45);
--text-on-accent: #ffffff;
/* --- 2. OBERFLÄCHEN & HINTERGRÜNDE --- */
--bg-app: #121316; /* Seiten-Hintergrund (Canvas) */
--bg-panel: #1a1c20; /* Karten, Header, Sidebar, Dropdowns */
--bg-panel-elevated: #22252a; /* Hervorgehobene Elemente & aktive Zeilen */
--bg-panel-hover: #2a2e35; /* Hover-Effekt auf Oberflächen */
--bg-input: #121316; /* Textfelder, Selects, Textareas */
--bg-overlay: rgba(0, 0, 0, 0.72); /* Hintergrund bei Modals/Dialogen */
--bg-code: #0d0e11; /* Code-Blöcke & Terminal-Boxen */
/* --- 3. TEXT & KONTRASTE --- */
--text-main: #f4f4f5; /* Haupttext & Überschriften */
--text-muted: #a1a1aa; /* Sekundärtext & Beschreibungen */
--text-faint: #71717a; /* Platzhalter & dezente Hinweise */
/* --- 4. RAHMEN, LINIEN & SCHATTEN --- */
--border-color: rgba(255, 255, 255, 0.09);
--border-subtle: rgba(255, 255, 255, 0.05);
--border-strong: rgba(255, 255, 255, 0.22);
--shadow-panel: 0 10px 30px rgba(0, 0, 0, 0.45);
--scrollbar-thumb: rgba(255, 255, 255, 0.14);
--scrollbar-thumb-hover: rgba(255, 255, 255, 0.26);
/* --- 5. STATUS- & SIGNALFARBEN --- */
--color-success: #22c55e;
--bg-success: rgba(34, 197, 94, 0.12);
--border-success: rgba(34, 197, 94, 0.38);
--color-danger: #ef4444;
--bg-danger: rgba(239, 68, 68, 0.12);
--border-danger: rgba(239, 68, 68, 0.38);
--color-warning: #f59e0b;
--bg-warning: rgba(245, 158, 11, 0.12);
--border-warning: rgba(245, 158, 11, 0.38);
/* --- 6. RUNDUNGEN (Geometrie) --- */
--radius-card: 0px; /* Karten, Panels, Container (0px = eckig) */
--radius-btn: 2px; /* Buttons & Badges */
--radius-input: 2px; /* Eingabefelder & Dropdowns */
--radius-checkbox: 0px; /* Aufgaben-Checkboxen */
--radius-pill: 9999px; /* Quick-Capture-Blase & Profil-Pill */
}
+33 -1
View File
@@ -17,6 +17,13 @@ export default {
hover: 'var(--bg-panel-hover)',
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'],
},
},