claude-desktop/src/lib/components/SettingsPanel.svelte
Eddy ad9833fcb8
Some checks failed
Build AppImage / build (push) Has been cancelled
feat: Phase 8+9 — Inline Tool-Karten + komplettes UI-Redesign auf Cursor/Zed-Niveau [appimage]
Phase 8 (VS-Code-Look Chatbereich):
- Linksbuendige Messages mit Avatar-Spalte, Hover-Actions
- Inline Tool-Karten (Read/Edit/Bash/Generic) in Assistant-Messages
- Edit-Karten zeigen Diff direkt mit Accept/Reject
- Tool-Calls werden via events.ts an letzte Assistant-Message gebunden
- Smart-Sticky-Scroll (Auto-Scroll stoppt wenn User selbst scrollt)
- OOM-Bug durch MutationObserver mit subtree:true behoben

Phase 9 (Komplettes UI-Redesign):
- Design-System in app.css: 4 Graustufen, 1 Akzent (#007acc), 4 Status-Farben,
  5 Schriftgroessen (11/12/13/14/16), 4-Punkt-Spacing, 2 Radius-Werte
- vscode.css als Aliase auf das neue System
- UI-Library src/lib/ui/: Button, Card, Icon, Badge, StatusDot, Tooltip, Drawer, Tabs
- Lucide-svelte fuer SVG-Icons (ersetzt Emojis im Chrome)
- StatusBar (22px) ersetzt ueberfuellten Footer mit 6+ Stats
- Titlebar entruempelt: ✱-Logo + Stop + Schulungsmodus + Version
- 2-spaltiges Layout (Sidebar 240px + Hauptbereich) statt 4-Pane-Zerstueckelung
- ToolDrawer: 13 Panels in 4 Gruppen (Aktivitaet/Speicher/Werkzeuge/Einstellungen),
  jede Gruppe mit internen Tabs, Esc schliesst
- Cmd+K global oeffnet QuickActions als zentrale Navigation
- StatusDot-Komponente ersetzt Emoji-Status (🟢🟡🔴) in AgentView
- Hardgecodete Farben (#ef4444, #22c55e, #eab308 ...) in 9 Komponenten durch
  CSS-Variablen ersetzt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:27:09 +02:00

897 lines
23 KiB
Svelte
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/core';
import { currentModel, agentMode, type AgentMode } from '$lib/stores/app';
import { updateCheckManual } from '$lib/stores/updateTrigger';
// === Typen ===
interface ModelInfo { id: string; name: string; description: string; }
interface SlashCommand { name: string; description: string; category: string; source: string; }
interface HookInfo { event: string; name: string; enabled: boolean; description: string; }
interface Permission {
id: string;
pattern: string;
tool: string | null;
path_pattern: string | null;
permission_type: string;
action: string;
}
// === Zustand ===
let searchQuery = $state('');
let activeCategory = $state('general');
let loading = $state(true);
// Settings-Daten
let availableModels: ModelInfo[] = $state([]);
let selectedModel = $state('');
let saving = $state(false);
let appVersion = $state('');
// Commands & Skills
let slashCommands: SlashCommand[] = $state([]);
// Hooks
let hooks: HookInfo[] = $state([]);
// Permissions
let permissions: Permission[] = $state([]);
// Alle Settings aus DB
let allSettings: Record<string, string> = $state({});
// === Kategorien ===
const categories = [
{ id: 'general', label: 'Allgemein', icon: '⚙️' },
{ id: 'model', label: 'Modell', icon: '🤖' },
{ id: 'commands', label: 'Commands', icon: '⌨️' },
{ id: 'hooks', label: 'Hooks', icon: '🪝' },
{ id: 'permissions', label: 'Berechtigungen', icon: '🛡️' },
{ id: 'about', label: 'Über', icon: '' },
];
// Modell-Icons und Preise
const modelIcons: Record<string, string> = { haiku: '🐦', sonnet: '📝', opus: '🎭' };
const modelPrices: Record<string, { input: number; output: number }> = {
haiku: { input: 0.25, output: 1.25 },
sonnet: { input: 3, output: 15 },
opus: { input: 15, output: 75 },
};
// Agent-Modi
const agentModes: { id: AgentMode; name: string; icon: string; desc: string }[] = [
{ id: 'solo', name: 'Solo', icon: '🎯', desc: 'Main Agent macht alles selbst' },
{ id: 'handlanger', name: 'Handlanger', icon: '👷', desc: 'Main denkt, Sub-Agents führen aus' },
{ id: 'experten', name: 'Experten', icon: '🧠', desc: 'Jeder Agent denkt selbst' },
{ id: 'auto', name: 'Auto', icon: '🔄', desc: 'Automatisch basierend auf Aufgabe' },
];
// === Gefilterte Einträge ===
let filteredCommands = $derived(
slashCommands.filter(c =>
!searchQuery || c.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.description.toLowerCase().includes(searchQuery.toLowerCase())
)
);
let filteredHooks = $derived(
hooks.filter(h =>
!searchQuery || h.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
h.event.toLowerCase().includes(searchQuery.toLowerCase())
)
);
// Suchfilter: Wenn Suche aktiv, zeige alle Kategorien die Treffer haben
let hasSearchResults = $derived(() => {
if (!searchQuery) return true;
const q = searchQuery.toLowerCase();
// Prüfe ob irgendwas matcht
return filteredCommands.length > 0 ||
filteredHooks.length > 0 ||
agentModes.some(m => m.name.toLowerCase().includes(q) || m.desc.toLowerCase().includes(q)) ||
availableModels.some(m => m.name.toLowerCase().includes(q));
});
// === Laden ===
onMount(async () => {
await loadAll();
});
async function loadAll() {
loading = true;
try {
const [models, model, mode, version, cmds, allHooks, perms, settings] = await Promise.all([
invoke<ModelInfo[]>('get_available_models'),
invoke<string>('get_current_model'),
invoke<string>('get_agent_mode'),
invoke<string>('get_current_version'),
invoke<SlashCommand[]>('get_slash_commands'),
invoke<HookInfo[]>('list_hooks'),
invoke<Permission[]>('get_permissions').catch(() => []),
invoke<Record<string, string>>('get_all_settings').catch(() => ({})),
]);
availableModels = models;
selectedModel = model;
$currentModel = model;
$agentMode = mode as AgentMode;
appVersion = version;
slashCommands = cmds;
hooks = allHooks;
permissions = perms;
allSettings = settings;
} catch (err) {
console.error('Settings laden fehlgeschlagen:', err);
}
loading = false;
}
// === Aktionen ===
async function changeModel(modelId: string) {
if (modelId === selectedModel) return;
saving = true;
try {
await invoke('set_model', { model: modelId });
selectedModel = modelId;
$currentModel = modelId;
} catch (err) {
console.error('Modell-Wechsel fehlgeschlagen:', err);
}
saving = false;
}
async function changeMode(modeId: AgentMode) {
if (modeId === $agentMode) return;
try {
await invoke('set_agent_mode', { mode: modeId });
$agentMode = modeId;
} catch (err) {
console.error('Modus-Wechsel fehlgeschlagen:', err);
}
}
async function toggleHook(hook: HookInfo) {
try {
await invoke('set_hook_enabled', { hookName: hook.name, enabled: !hook.enabled });
hooks = hooks.map(h => h.name === hook.name ? { ...h, enabled: !h.enabled } : h);
} catch (err) {
console.error('Hook-Toggle fehlgeschlagen:', err);
}
}
function triggerUpdateCheck() {
updateCheckManual.set(true);
}
function formatPrice(price: number): string {
return `$${price.toFixed(2)}`;
}
function getCategoryIcon(cat: string): string {
switch(cat) {
case 'builtin': return '📦';
case 'custom': return '📝';
case 'skill': return '⚡';
default: return '📄';
}
}
function getCategoryLabel(cat: string): string {
switch(cat) {
case 'builtin': return 'Eingebaut';
case 'custom': return 'Custom';
case 'skill': return 'Skill';
default: return cat;
}
}
</script>
<div class="settings-panel">
<!-- Suchfeld -->
<div class="search-bar">
<span class="search-icon">🔍</span>
<input
type="text"
class="search-input"
placeholder="Einstellungen durchsuchen..."
bind:value={searchQuery}
/>
{#if searchQuery}
<button class="search-clear" onclick={() => searchQuery = ''}>✕</button>
{/if}
</div>
<div class="settings-layout">
<!-- Kategorie-Sidebar -->
{#if !searchQuery}
<nav class="category-nav">
{#each categories as cat}
<button
class="category-btn"
class:active={activeCategory === cat.id}
onclick={() => activeCategory = cat.id}
>
<span class="cat-icon">{cat.icon}</span>
<span class="cat-label">{cat.label}</span>
</button>
{/each}
</nav>
{/if}
<!-- Content -->
<div class="settings-content" class:full-width={!!searchQuery}>
{#if loading}
<div class="loading">Lade Einstellungen...</div>
{:else}
<!-- ALLGEMEIN -->
{#if activeCategory === 'general' || searchQuery}
{#if !searchQuery || agentModes.some(m => m.name.toLowerCase().includes(searchQuery.toLowerCase()))}
<section class="section">
<h3>🤝 Agent-Modus</h3>
<div class="mode-grid">
{#each agentModes as mode}
<button
class="mode-card"
class:selected={$agentMode === mode.id}
onclick={() => changeMode(mode.id)}
>
<span class="mode-icon">{mode.icon}</span>
<div class="mode-text">
<span class="mode-name">{mode.name}</span>
{#if $agentMode === mode.id}
<span class="active-badge">Aktiv</span>
{/if}
</div>
<span class="mode-desc">{mode.desc}</span>
</button>
{/each}
</div>
</section>
{/if}
{#if !searchQuery}
<section class="section">
<h3>🎨 Darstellung</h3>
<div class="setting-row">
<div class="setting-info">
<span class="setting-key">Theme</span>
<span class="setting-desc">Farbschema der Anwendung</span>
</div>
<span class="setting-val">AWL Dark</span>
</div>
</section>
{/if}
{/if}
<!-- MODELL -->
{#if activeCategory === 'model' || (searchQuery && availableModels.some(m => m.name.toLowerCase().includes(searchQuery.toLowerCase())))}
<section class="section">
<h3>🤖 Claude-Modell</h3>
<div class="model-list">
{#each availableModels as model}
<button
class="model-card"
class:selected={selectedModel === model.id}
onclick={() => changeModel(model.id)}
disabled={saving}
>
<div class="model-head">
<span class="model-icon">{modelIcons[model.id] || '🤖'}</span>
<span class="model-name">{model.name}</span>
{#if selectedModel === model.id}
<span class="active-badge">Aktiv</span>
{/if}
</div>
<span class="model-desc">{model.description}</span>
{#if modelPrices[model.id]}
<div class="model-price">
<span>In: {formatPrice(modelPrices[model.id].input)}/1M</span>
<span>Out: {formatPrice(modelPrices[model.id].output)}/1M</span>
</div>
{/if}
</button>
{/each}
</div>
</section>
{/if}
<!-- COMMANDS & SKILLS -->
{#if activeCategory === 'commands' || (searchQuery && filteredCommands.length > 0)}
<section class="section">
<h3>⌨️ Commands & Skills</h3>
<p class="section-sub">Slash-Commands aus ~/.claude/commands/ und Skills aus ~/.claude/skills/</p>
<div class="command-list">
{#each filteredCommands as cmd}
<div class="command-row">
<div class="command-info">
<span class="command-name">/{cmd.name}</span>
<span class="command-badge" class:builtin={cmd.category === 'builtin'} class:skill={cmd.category === 'skill'} class:custom={cmd.category === 'custom'}>
{getCategoryIcon(cmd.category)} {getCategoryLabel(cmd.category)}
</span>
</div>
<span class="command-desc">{cmd.description}</span>
{#if cmd.source !== 'builtin'}
<span class="command-source" title={cmd.source}>
{cmd.source.split('/').pop()}
</span>
{/if}
</div>
{/each}
</div>
{#if filteredCommands.length === 0}
<div class="empty-state">Keine Commands gefunden</div>
{/if}
<div class="section-footer">
<span class="count">{slashCommands.filter(c => c.category === 'builtin').length} Eingebaut</span>
<span class="count">{slashCommands.filter(c => c.category === 'custom').length} Custom</span>
<span class="count">{slashCommands.filter(c => c.category === 'skill').length} Skills</span>
</div>
</section>
{/if}
<!-- HOOKS -->
{#if activeCategory === 'hooks' || (searchQuery && filteredHooks.length > 0)}
<section class="section">
<h3>🪝 Hooks</h3>
<p class="section-sub">Event-basierte Automatisierungen</p>
<div class="hook-list">
{#each filteredHooks as hook}
<div class="hook-row">
<div class="hook-info">
<button
class="hook-toggle"
class:enabled={hook.enabled}
onclick={() => toggleHook(hook)}
title={hook.enabled ? 'Deaktivieren' : 'Aktivieren'}
>
{hook.enabled ? '●' : '○'}
</button>
<div class="hook-text">
<span class="hook-name">{hook.name}</span>
<span class="hook-event">{hook.event}</span>
</div>
</div>
{#if hook.description}
<span class="hook-desc">{hook.description}</span>
{/if}
</div>
{/each}
</div>
{#if filteredHooks.length === 0}
<div class="empty-state">Keine Hooks gefunden</div>
{/if}
</section>
{/if}
<!-- BERECHTIGUNGEN -->
{#if activeCategory === 'permissions' || (searchQuery && 'berechtigungen permissions'.includes(searchQuery.toLowerCase()))}
<section class="section">
<h3>🛡️ Berechtigungen</h3>
<p class="section-sub">Guard-Rails für Tool-Zugriffe und Aktionen</p>
{#if permissions.length > 0}
<div class="perm-list">
{#each permissions as perm}
<div class="perm-row">
<span class="perm-pattern">{perm.pattern}</span>
{#if perm.tool}
<span class="perm-tool">{perm.tool}</span>
{/if}
<span class="perm-status" class:allowed={perm.action === 'Allow'} class:denied={perm.action !== 'Allow'}>
{perm.action === 'Allow' ? '✓ Erlaubt' : '✗ ' + perm.action}
</span>
</div>
{/each}
</div>
{:else}
<div class="empty-state">
Keine expliziten Berechtigungen gesetzt.<br>
<span class="hint">Berechtigungen werden bei der ersten Tool-Nutzung vergeben.</span>
</div>
{/if}
</section>
{/if}
<!-- ÜBER -->
{#if activeCategory === 'about' || (searchQuery && 'version update über about'.includes(searchQuery.toLowerCase()))}
<section class="section">
<h3> Über Claude Desktop</h3>
<div class="about-grid">
<div class="setting-row">
<div class="setting-info">
<span class="setting-key">Version</span>
</div>
<span class="setting-val">{appVersion || '—'}</span>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-key">Framework</span>
</div>
<span class="setting-val">Tauri 2.0 + SvelteKit 5</span>
</div>
<div class="setting-row">
<div class="setting-info">
<span class="setting-key">Theme</span>
</div>
<span class="setting-val">AWL Dark</span>
</div>
</div>
<div class="update-section">
<button class="update-btn" onclick={triggerUpdateCheck}>
🔄 Nach Updates suchen
</button>
{#if appVersion === 'dev'}
<p class="dev-hint">
Entwicklungs-Build — Auto-Update deaktiviert.
</p>
{/if}
</div>
<!-- Alle DB-Settings (Debug) -->
{#if Object.keys(allSettings).length > 0}
<details class="debug-settings">
<summary>Alle Settings ({Object.keys(allSettings).length})</summary>
<div class="settings-dump">
{#each Object.entries(allSettings) as [key, value]}
<div class="dump-row">
<span class="dump-key">{key}</span>
<span class="dump-val">{value}</span>
</div>
{/each}
</div>
</details>
{/if}
</section>
{/if}
{/if}
</div>
</div>
</div>
<style>
.settings-panel {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
/* Suchfeld */
.search-bar {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--bg-tertiary);
}
.search-icon {
font-size: 0.8rem;
opacity: 0.5;
}
.search-input {
flex: 1;
background: var(--bg-primary);
border: 1px solid var(--bg-tertiary);
border-radius: var(--radius-sm);
padding: 5px 8px;
font-size: 0.8rem;
color: var(--text-primary);
outline: none;
transition: border-color 0.15s;
}
.search-input:focus {
border-color: var(--accent);
}
.search-input::placeholder {
color: var(--text-secondary);
opacity: 0.6;
}
.search-clear {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 0.75rem;
padding: 2px 4px;
}
.search-clear:hover {
color: var(--text-primary);
}
/* Layout */
.settings-layout {
display: flex;
flex: 1;
overflow: hidden;
}
/* Kategorie-Nav */
.category-nav {
display: flex;
flex-direction: column;
width: 110px;
flex-shrink: 0;
background: var(--bg-secondary);
border-right: 1px solid var(--bg-tertiary);
padding: 4px;
gap: 2px;
overflow-y: auto;
}
.category-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
background: transparent;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
color: var(--text-secondary);
font-size: 0.72rem;
text-align: left;
transition: all 0.12s;
}
.category-btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.category-btn.active {
background: rgba(233, 69, 96, 0.12);
color: var(--accent);
}
.cat-icon { font-size: 0.85rem; }
.cat-label { white-space: nowrap; }
/* Content */
.settings-content {
flex: 1;
overflow-y: auto;
padding: var(--spacing-md);
}
.settings-content.full-width {
/* Kein Sidebar bei Suche */
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
font-size: 0.85rem;
}
/* Sektionen */
.section {
margin-bottom: var(--spacing-lg);
}
.section h3 {
font-size: 0.85rem;
font-weight: 600;
margin-bottom: 4px;
}
.section-sub {
font-size: 0.7rem;
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
}
/* Agent-Modi Grid */
.mode-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
}
.mode-card {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px;
background: var(--bg-secondary);
border: 2px solid var(--bg-tertiary);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.15s;
text-align: left;
}
.mode-card:hover { border-color: var(--text-secondary); }
.mode-card.selected { border-color: var(--accent); background: rgba(233, 69, 96, 0.08); }
.mode-icon { font-size: 1rem; }
.mode-text { display: flex; align-items: center; gap: 4px; }
.mode-name { font-weight: 600; font-size: 0.8rem; }
.mode-desc { font-size: 0.65rem; color: var(--text-secondary); line-height: 1.3; }
/* Modell-Liste */
.model-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.model-card {
text-align: left;
padding: 10px;
background: var(--bg-secondary);
border: 2px solid var(--bg-tertiary);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.15s;
display: flex;
flex-direction: column;
gap: 3px;
}
.model-card:hover:not(:disabled) { border-color: var(--text-secondary); }
.model-card.selected { border-color: var(--accent); background: rgba(233, 69, 96, 0.08); }
.model-card:disabled { opacity: 0.5; cursor: wait; }
.model-head { display: flex; align-items: center; gap: 6px; }
.model-icon { font-size: 1rem; }
.model-name { font-weight: 600; font-size: 0.85rem; flex: 1; }
.model-desc { font-size: 0.72rem; color: var(--text-secondary); }
.model-price {
display: flex;
gap: var(--spacing-md);
font-size: 0.65rem;
color: var(--text-secondary);
font-family: var(--font-mono);
}
/* Active-Badge (wiederverwendbar) */
.active-badge {
font-size: 0.6rem;
padding: 1px 5px;
background: var(--accent);
color: white;
border-radius: var(--radius-sm);
font-weight: 500;
}
/* Setting-Row */
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid var(--bg-tertiary);
}
.setting-info { display: flex; flex-direction: column; gap: 1px; }
.setting-key { font-size: 0.8rem; }
.setting-desc { font-size: 0.65rem; color: var(--text-secondary); }
.setting-val { font-size: 0.8rem; color: var(--text-secondary); font-family: var(--font-mono); }
/* Commands */
.command-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.command-row {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px 8px;
background: var(--bg-secondary);
border-radius: var(--radius-sm);
transition: background 0.1s;
}
.command-row:hover { background: var(--bg-tertiary); }
.command-info {
display: flex;
align-items: center;
gap: 8px;
}
.command-name {
font-family: var(--font-mono);
font-size: 0.8rem;
font-weight: 600;
color: var(--accent);
}
.command-badge {
font-size: 0.6rem;
padding: 1px 5px;
border-radius: var(--radius-sm);
opacity: 0.8;
}
.command-badge.builtin { background: rgba(99, 102, 241, 0.15); color: #818cf8; }
.command-badge.skill { background: rgba(234, 179, 8, 0.15); color: var(--status-warning); }
.command-badge.custom { background: rgba(34, 197, 94, 0.15); color: var(--status-success); }
.command-desc { font-size: 0.7rem; color: var(--text-secondary); }
.command-source { font-size: 0.6rem; color: var(--text-secondary); opacity: 0.5; font-family: var(--font-mono); }
.section-footer {
display: flex;
gap: var(--spacing-md);
margin-top: var(--spacing-sm);
padding-top: var(--spacing-sm);
border-top: 1px solid var(--bg-tertiary);
}
.count {
font-size: 0.65rem;
color: var(--text-secondary);
}
/* Hooks */
.hook-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.hook-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
background: var(--bg-secondary);
border-radius: var(--radius-sm);
}
.hook-info {
display: flex;
align-items: center;
gap: 8px;
}
.hook-toggle {
background: none;
border: none;
font-size: 1rem;
cursor: pointer;
color: var(--text-secondary);
padding: 0;
line-height: 1;
transition: color 0.15s;
}
.hook-toggle.enabled { color: var(--status-success); }
.hook-text { display: flex; flex-direction: column; gap: 1px; }
.hook-name { font-size: 0.78rem; font-weight: 500; }
.hook-event { font-size: 0.62rem; color: var(--text-secondary); font-family: var(--font-mono); }
.hook-desc {
font-size: 0.62rem;
color: var(--text-secondary);
opacity: 0.7;
max-width: 140px;
text-align: right;
line-height: 1.2;
}
/* Permissions */
.perm-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.perm-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
background: var(--bg-secondary);
border-radius: var(--radius-sm);
font-size: 0.75rem;
}
.perm-pattern { font-weight: 500; flex: 1; font-family: var(--font-mono); font-size: 0.72rem; }
.perm-tool { color: var(--text-secondary); font-family: var(--font-mono); font-size: 0.65rem; padding: 1px 4px; background: var(--bg-tertiary); border-radius: 2px; }
.perm-status { font-size: 0.65rem; font-weight: 500; }
.perm-status.allowed { color: var(--status-success); }
.perm-status.denied { color: var(--status-error); }
/* Empty State */
.empty-state {
text-align: center;
padding: var(--spacing-md);
color: var(--text-secondary);
font-size: 0.8rem;
}
.empty-state .hint {
font-size: 0.7rem;
opacity: 0.7;
}
/* About / Updates */
.about-grid {
margin-bottom: var(--spacing-md);
}
.update-section {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.update-btn {
align-self: flex-start;
padding: 6px 14px;
background: var(--accent);
color: white;
border: none;
border-radius: var(--radius-md);
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.update-btn:hover { filter: brightness(1.1); }
.dev-hint {
margin: 0;
padding: 6px 10px;
font-size: 0.7rem;
color: var(--text-secondary);
background: rgba(234, 179, 8, 0.08);
border: 1px solid rgba(234, 179, 8, 0.2);
border-radius: var(--radius-sm);
}
/* Debug Settings */
.debug-settings {
margin-top: var(--spacing-md);
font-size: 0.72rem;
}
.debug-settings summary {
cursor: pointer;
color: var(--text-secondary);
padding: 4px 0;
}
.settings-dump {
margin-top: 4px;
background: var(--bg-secondary);
border-radius: var(--radius-sm);
padding: 6px;
}
.dump-row {
display: flex;
justify-content: space-between;
padding: 2px 4px;
border-bottom: 1px solid var(--bg-tertiary);
}
.dump-key { font-family: var(--font-mono); color: var(--accent); }
.dump-val { font-family: var(--font-mono); color: var(--text-secondary); }
</style>