mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:21:10 +02:00
refactor(settings): unify section styling, remove Credits tab
- AiSettings: Tailwind → scoped CSS with theme tokens; tier cards as buttons with subtle primary-tinted active state; Tier 0 in matching card; pill toggles for behavior settings; improved body text readability - VaultSection: theme-token colors, SettingsSectionHeader with status badge action, unified button system, flat subsection structure with border dividers, collapsible encrypted-fields list (5 by default), threat-model disclosure moved above destructive actions, wizard step numbering; dropped stale dark-mode @media block - Settings ListView: overflow-x: hidden so chip-row scroll stays scoped; hashchange + workbench:navigate-anchor effect for live deep-links - Remove Credits tab (stub redirecting to /?app=credits) — full Credits module + dashboard widget already cover balance/purchases/transactions
This commit is contained in:
parent
7d120225dc
commit
677123091a
5 changed files with 1055 additions and 736 deletions
|
|
@ -3,13 +3,9 @@
|
|||
* AiSettings — Settings-page section for the tiered LLM orchestrator.
|
||||
*
|
||||
* Lets the user pick which @mana/shared-llm tiers Mana is allowed to
|
||||
* use. Each tier is a checkbox with an explanation of what it means
|
||||
* for privacy + speed + cost. The cloud tier is gated behind an
|
||||
* additional consent checkbox the first time it's enabled.
|
||||
*
|
||||
* Drops into the main settings page (/settings) as a self-contained
|
||||
* Card section, mirroring the existing PasskeyManager / SessionManager
|
||||
* pattern.
|
||||
* use. Each tier is a selectable row with an explanation of what it
|
||||
* means for privacy + speed + cost. The cloud tier is gated behind
|
||||
* an additional consent step the first time it's enabled.
|
||||
*/
|
||||
import { llmSettingsState, updateLlmSettings, tierLabel, type LlmTier } from '@mana/shared-llm';
|
||||
import {
|
||||
|
|
@ -30,7 +26,7 @@
|
|||
let loadingBrowser = $state(false);
|
||||
|
||||
function toggleTier(tier: LlmTier) {
|
||||
if (tier === 'none') return; // 'none' is implicit, not a real toggle
|
||||
if (tier === 'none') return;
|
||||
const current = settings.allowedTiers;
|
||||
const next = current.includes(tier) ? current.filter((t) => t !== tier) : [...current, tier];
|
||||
updateLlmSettings({ allowedTiers: next });
|
||||
|
|
@ -57,8 +53,6 @@
|
|||
updateLlmSettings({ showSourceInUi: value });
|
||||
}
|
||||
|
||||
// Tier card metadata — defined inline because it's specific to this UI
|
||||
// and changes alongside layout updates.
|
||||
type TierCard = {
|
||||
tier: LlmTier;
|
||||
icon: typeof Robot;
|
||||
|
|
@ -99,7 +93,7 @@
|
|||
bullets: [
|
||||
'Direkt aus dem Browser — keine Mana-Server-Zwischenstation',
|
||||
'Du zahlst beim Provider, wir sehen nichts davon',
|
||||
'Schluessel werden verschluesselt in deinem Vault gespeichert',
|
||||
'Schlüssel werden verschlüsselt in deinem Vault gespeichert',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -123,24 +117,28 @@
|
|||
const browserCacheReady = $derived(webgpuSupported && localLlmStatus.current.state === 'ready');
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<!-- Tier 0 explainer -->
|
||||
<div class="mb-4 rounded-xl border border-border bg-muted/20 p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<CheckCircle size={20} class="mt-0.5 shrink-0 text-emerald-500" weight="fill" />
|
||||
<div>
|
||||
<p class="font-medium">Lokal (ohne KI) — immer aktiv</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Mana funktioniert auch ganz ohne KI: Datum-Erkennung, Suche und einfache Klassifikation
|
||||
laufen über klassische Algorithmen. Manche Funktionen sind dann begrenzt, dafür ist alles
|
||||
100% offline und kostet nichts.
|
||||
</p>
|
||||
<div class="ai-settings">
|
||||
<!-- Tier 0 — always on -->
|
||||
<div class="tier-row tier0 enabled" aria-disabled="true">
|
||||
<div class="tier-icon">
|
||||
<CheckCircle size={20} weight="fill" />
|
||||
</div>
|
||||
<div class="tier-body">
|
||||
<div class="tier-title-line">
|
||||
<span class="tier-title">Lokal (ohne KI)</span>
|
||||
<span class="tier-badge tier-badge-always">immer aktiv</span>
|
||||
</div>
|
||||
<p class="tier-subtitle">Basis-Funktionen ohne jede KI</p>
|
||||
<p class="tier0-desc">
|
||||
Mana funktioniert auch ganz ohne KI: Datum-Erkennung, Suche und einfache Klassifikation
|
||||
laufen über klassische Algorithmen. Manche Funktionen sind dann begrenzt, dafür ist alles
|
||||
100% offline und kostet nichts.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tier 1-3 toggleable cards -->
|
||||
<div class="space-y-3">
|
||||
<!-- Tier 1–3 selectable rows -->
|
||||
<div class="tier-list">
|
||||
{#each tierCards as card}
|
||||
{@const enabled = isEnabled(card.tier)}
|
||||
{@const tierBlocked = card.tier === 'browser' && !webgpuSupported}
|
||||
|
|
@ -148,189 +146,518 @@
|
|||
type="button"
|
||||
onclick={() => !tierBlocked && toggleTier(card.tier)}
|
||||
disabled={tierBlocked}
|
||||
class="w-full rounded-xl border p-4 text-left transition-all {enabled
|
||||
? 'border-primary bg-primary/5 ring-1 ring-primary/30'
|
||||
: 'border-border bg-card hover:border-primary/40'} {tierBlocked
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: 'cursor-pointer'}"
|
||||
class="tier-row"
|
||||
class:enabled
|
||||
class:blocked={tierBlocked}
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg {enabled
|
||||
? 'bg-primary/20 text-primary'
|
||||
: 'bg-muted text-muted-foreground'}"
|
||||
>
|
||||
<card.icon size={20} />
|
||||
<div class="tier-icon">
|
||||
<card.icon size={20} />
|
||||
</div>
|
||||
<div class="tier-body">
|
||||
<div class="tier-title-line">
|
||||
<span class="tier-title">{card.title}</span>
|
||||
{#if enabled}
|
||||
<span class="tier-badge">aktiv</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-base font-semibold">{card.title}</div>
|
||||
{#if enabled}
|
||||
<span
|
||||
class="rounded-full bg-primary px-2 py-0.5 text-[10px] font-medium text-primary-foreground"
|
||||
>aktiv</span
|
||||
<p class="tier-subtitle">{card.subtitle}</p>
|
||||
<ul class="tier-bullets">
|
||||
{#each card.bullets as bullet}
|
||||
<li>{bullet}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if card.warning}
|
||||
<p class="tier-warning">
|
||||
<Warning size={12} weight="fill" />
|
||||
<span>{card.warning}</span>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if card.tier === 'browser' && enabled && webgpuSupported}
|
||||
<div class="tier-extra">
|
||||
{#if browserCacheReady}
|
||||
<span class="status-ok">✓ Modell geladen</span>
|
||||
{:else if localLlmStatus.current.state === 'downloading'}
|
||||
<span class="status-muted">
|
||||
Lade {defaultModelInfo.displayName} ({(
|
||||
localLlmStatus.current.progress * 100
|
||||
).toFixed(0)}%)…
|
||||
</span>
|
||||
{:else}
|
||||
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
loadBrowserModel();
|
||||
}}
|
||||
disabled={loadingBrowser}
|
||||
class="action-btn"
|
||||
>
|
||||
{loadingBrowser
|
||||
? 'Lade…'
|
||||
: `Modell laden (~${defaultModelInfo.downloadSizeMb} MB)`}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="mt-0.5 text-sm text-muted-foreground">{card.subtitle}</p>
|
||||
<ul class="mt-2 space-y-1 text-xs text-muted-foreground">
|
||||
{#each card.bullets as bullet}
|
||||
<li class="flex items-start gap-1.5">
|
||||
<span class="mt-0.5 text-primary">•</span>
|
||||
<span>{bullet}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if card.warning}
|
||||
<div
|
||||
class="mt-2 flex items-start gap-1.5 rounded-md bg-amber-500/10 p-2 text-xs text-amber-700 dark:text-amber-400"
|
||||
>
|
||||
<Warning size={12} weight="fill" class="mt-0.5 shrink-0" />
|
||||
<span>{card.warning}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Per-tier extras -->
|
||||
{#if card.tier === 'browser' && enabled && webgpuSupported}
|
||||
<div class="mt-3 flex items-center gap-2 text-xs">
|
||||
{#if browserCacheReady}
|
||||
<span class="text-emerald-500">✓ Modell geladen</span>
|
||||
{:else if localLlmStatus.current.state === 'downloading'}
|
||||
<span class="text-muted-foreground">
|
||||
Lade {defaultModelInfo.displayName} ({(
|
||||
localLlmStatus.current.progress * 100
|
||||
).toFixed(0)}%)…
|
||||
</span>
|
||||
{:else}
|
||||
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
loadBrowserModel();
|
||||
}}
|
||||
disabled={loadingBrowser}
|
||||
class="rounded-md bg-primary px-3 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50"
|
||||
>
|
||||
{loadingBrowser
|
||||
? 'Lade…'
|
||||
: `Modell laden (~${defaultModelInfo.downloadSizeMb} MB)`}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if card.tier === 'browser' && tierBlocked}
|
||||
<p class="tier-blocked-note">
|
||||
WebGPU nicht verfügbar in deinem Browser. Funktioniert in Chrome/Edge 113+ oder Safari
|
||||
18+.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if card.tier === 'browser' && tierBlocked}
|
||||
<p class="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
||||
WebGPU nicht verfügbar in deinem Browser. Funktioniert in Chrome/Edge 113+ oder
|
||||
Safari 18+.
|
||||
{#if card.tier === 'byok' && enabled}
|
||||
<div
|
||||
class="tier-extra"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
role="presentation"
|
||||
>
|
||||
<ByokKeysManager />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if card.tier === 'cloud' && enabled && !settings.cloudConsentGiven}
|
||||
<div
|
||||
class="tier-consent"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
role="presentation"
|
||||
>
|
||||
<p class="consent-title">Bestätigung erforderlich</p>
|
||||
<p class="consent-desc">
|
||||
Cloud-Anfragen senden deine Inhalte an Google. Bitte bestätige, dass du das
|
||||
verstanden hast und akzeptierst.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if card.tier === 'byok' && enabled}
|
||||
<div
|
||||
class="mt-3"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
role="presentation"
|
||||
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
setCloudConsent(true);
|
||||
}}
|
||||
class="consent-btn"
|
||||
>
|
||||
<ByokKeysManager />
|
||||
</div>
|
||||
{/if}
|
||||
Verstanden, Cloud aktivieren
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if card.tier === 'cloud' && enabled && !settings.cloudConsentGiven}
|
||||
<div
|
||||
class="mt-3 rounded-md border border-amber-500/30 bg-amber-500/5 p-3"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
role="presentation"
|
||||
{#if card.tier === 'cloud' && enabled && settings.cloudConsentGiven}
|
||||
<div
|
||||
class="tier-extra consent-ok"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
role="presentation"
|
||||
>
|
||||
<span class="status-ok">✓ Cloud-Zustimmung erteilt</span>
|
||||
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
setCloudConsent(false);
|
||||
}}
|
||||
class="link-btn"
|
||||
>
|
||||
<p class="text-xs font-medium text-amber-700 dark:text-amber-400">
|
||||
Bestätigung erforderlich
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
Cloud-Anfragen senden deine Inhalte an Google. Bitte bestätige, dass du das
|
||||
verstanden hast und akzeptierst.
|
||||
</p>
|
||||
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
setCloudConsent(true);
|
||||
}}
|
||||
class="mt-2 rounded-md bg-amber-600 px-3 py-1 text-xs font-medium text-white hover:bg-amber-700"
|
||||
>
|
||||
Verstanden, Cloud aktivieren
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if card.tier === 'cloud' && enabled && settings.cloudConsentGiven}
|
||||
<div class="mt-3 flex items-center justify-between gap-2 text-xs">
|
||||
<span class="text-emerald-500">✓ Cloud-Zustimmung erteilt</span>
|
||||
<!-- svelte-ignore node_invalid_placement_ssr -->
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
setCloudConsent(false);
|
||||
}}
|
||||
class="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Zurücknehmen
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Zurücknehmen
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Active tier chain summary -->
|
||||
<div class="mt-4 rounded-lg bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
||||
<span class="font-medium text-foreground">Aktuelle Reihenfolge: </span>
|
||||
{#if settings.allowedTiers.length === 0}
|
||||
Nur lokal (ohne KI) — die meisten KI-Funktionen sind begrenzt.
|
||||
{:else}
|
||||
{settings.allowedTiers.map((t) => tierLabel(t)).join(' → ')} → Lokal (Fallback)
|
||||
{/if}
|
||||
<div class="summary-row">
|
||||
<span class="summary-label">Aktuelle Reihenfolge:</span>
|
||||
<span class="summary-value">
|
||||
{#if settings.allowedTiers.length === 0}
|
||||
Nur lokal (ohne KI) — die meisten KI-Funktionen sind begrenzt.
|
||||
{:else}
|
||||
{settings.allowedTiers.map((t) => tierLabel(t)).join(' → ')} → Lokal (Fallback)
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Behavior toggles -->
|
||||
<div class="mt-6 space-y-3 border-t border-border pt-4">
|
||||
<label class="flex cursor-pointer items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.fallbackToRulesOnError}
|
||||
onchange={(e) => setFallback((e.currentTarget as HTMLInputElement).checked)}
|
||||
class="mt-0.5 h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-sm font-medium">Bei Fehler auf "Lokal" zurückfallen</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
<div class="rows">
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Bei Fehler auf „Lokal" zurückfallen</p>
|
||||
<p class="row-desc">
|
||||
Wenn die gewählte KI-Schicht eine Anfrage nicht beantworten kann, versucht Mana es mit der
|
||||
lokalen Variante (sofern verfügbar). Aus: zeigt stattdessen einen Fehler an.
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle"
|
||||
class:on={settings.fallbackToRulesOnError}
|
||||
onclick={() => setFallback(!settings.fallbackToRulesOnError)}
|
||||
aria-label="Fallback auf Lokal"
|
||||
>
|
||||
<span class="toggle-knob"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label class="flex cursor-pointer items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.showSourceInUi}
|
||||
onchange={(e) => setShowSource((e.currentTarget as HTMLInputElement).checked)}
|
||||
class="mt-0.5 h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-sm font-medium">Quelle bei jedem KI-Resultat anzeigen</div>
|
||||
<div class="text-xs text-muted-foreground">
|
||||
Zeigt unter jeder KI-generierten Antwort eine kleine Markierung wie "Auf deinem Gerät"
|
||||
oder "via Google Gemini" — damit du immer siehst, wo deine Daten gerade verarbeitet
|
||||
<div class="row">
|
||||
<div class="row-info">
|
||||
<p class="row-title">Quelle bei jedem KI-Resultat anzeigen</p>
|
||||
<p class="row-desc">
|
||||
Zeigt unter jeder KI-generierten Antwort eine kleine Markierung wie „Auf deinem Gerät"
|
||||
oder „via Google Gemini" — damit du immer siehst, wo deine Daten gerade verarbeitet
|
||||
wurden.
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle"
|
||||
class:on={settings.showSourceInUi}
|
||||
onclick={() => setShowSource(!settings.showSourceInUi)}
|
||||
aria-label="Quelle anzeigen"
|
||||
>
|
||||
<span class="toggle-knob"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.ai-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
/* ── Tier 0 — always-active (same card shape, non-interactive) ───── */
|
||||
.tier-row.tier0 {
|
||||
cursor: default;
|
||||
border-color: hsl(142 71% 45% / 0.35);
|
||||
background: hsl(142 71% 45% / 0.05);
|
||||
box-shadow: inset 0 0 0 1px hsl(142 71% 45% / 0.1);
|
||||
}
|
||||
.tier-row.tier0:hover {
|
||||
border-color: hsl(142 71% 45% / 0.35);
|
||||
background: hsl(142 71% 45% / 0.05);
|
||||
}
|
||||
.tier-row.tier0 .tier-icon {
|
||||
background: hsl(142 71% 45% / 0.15);
|
||||
color: hsl(142 71% 45%);
|
||||
}
|
||||
.tier-row.tier0 .tier-badge-always {
|
||||
background: hsl(142 71% 45%);
|
||||
color: white;
|
||||
}
|
||||
.tier0-desc {
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── Tier selection list ────────────────────────────────────────── */
|
||||
.tier-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tier-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.875rem;
|
||||
width: 100%;
|
||||
padding: 0.875rem 1rem;
|
||||
background: hsl(var(--color-card));
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 0.875rem;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.15s,
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
}
|
||||
.tier-row:not(.blocked):hover {
|
||||
border-color: hsl(var(--color-border-strong, var(--color-border)));
|
||||
background: hsl(var(--color-surface-hover, var(--color-muted)) / 0.4);
|
||||
}
|
||||
.tier-row.enabled {
|
||||
border-color: hsl(var(--color-primary) / 0.4);
|
||||
background: hsl(var(--color-primary) / 0.04);
|
||||
box-shadow: inset 0 0 0 1px hsl(var(--color-primary) / 0.15);
|
||||
}
|
||||
.tier-row.blocked {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tier-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-radius: 0.625rem;
|
||||
background: hsl(var(--color-muted));
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s;
|
||||
}
|
||||
.tier-row.enabled .tier-icon {
|
||||
background: hsl(var(--color-primary) / 0.15);
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.tier-body {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tier-title-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tier-title {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.tier-badge {
|
||||
padding: 0.0625rem 0.4375rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 500;
|
||||
border-radius: 9999px;
|
||||
background: hsl(var(--color-primary));
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
}
|
||||
|
||||
.tier-subtitle {
|
||||
margin: 0.1875rem 0 0;
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tier-bullets {
|
||||
margin: 0.625rem 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3125rem;
|
||||
}
|
||||
|
||||
.tier-bullets li {
|
||||
position: relative;
|
||||
padding-left: 0.9375rem;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-foreground) / 0.78);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.tier-bullets li::before {
|
||||
content: '•';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.tier-warning {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.375rem;
|
||||
margin: 0.625rem 0 0;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
color: hsl(35 90% 45%);
|
||||
}
|
||||
.tier-warning :global(svg) {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.tier-extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.tier-extra.consent-ok {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.status-ok {
|
||||
color: hsl(142 71% 45%);
|
||||
}
|
||||
.status-muted {
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.tier-blocked-note {
|
||||
margin: 0.5rem 0 0;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(35 90% 45%);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--color-primary));
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
cursor: pointer;
|
||||
}
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
cursor: pointer;
|
||||
}
|
||||
.link-btn:hover {
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
/* ── Cloud consent inline block ─────────────────────────────────── */
|
||||
.tier-consent {
|
||||
margin-top: 0.625rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
border-left: 3px solid hsl(35 90% 55%);
|
||||
background: hsl(35 90% 55% / 0.06);
|
||||
border-radius: 0 0.375rem 0.375rem 0;
|
||||
}
|
||||
.consent-title {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(35 90% 40%);
|
||||
}
|
||||
.consent-desc {
|
||||
margin: 0.25rem 0 0;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
line-height: 1.55;
|
||||
}
|
||||
.consent-btn {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(35 90% 50%);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.consent-btn:hover {
|
||||
background: hsl(35 90% 45%);
|
||||
}
|
||||
|
||||
/* ── Summary row ────────────────────────────────────────────────── */
|
||||
.summary-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
.summary-label {
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
.summary-value {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ── Behavior toggle rows (matches GeneralSection) ──────────────── */
|
||||
.rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid hsl(var(--color-border));
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid hsl(var(--color-border));
|
||||
}
|
||||
|
||||
.row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.row-info {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.row-title {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.row-desc {
|
||||
margin: 0.1875rem 0 0;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
position: relative;
|
||||
width: 2.75rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 9999px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
background: hsl(var(--color-muted));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle.on {
|
||||
background: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.toggle-knob {
|
||||
position: absolute;
|
||||
top: 0.125rem;
|
||||
left: 0.125rem;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
transition: transform 0.2s;
|
||||
box-shadow: 0 1px 3px hsl(0 0% 0% / 0.15);
|
||||
}
|
||||
|
||||
.toggle.on .toggle-knob {
|
||||
transform: translateX(1.25rem);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
* updates both the navigation and the search results.
|
||||
*/
|
||||
import type { Component } from 'svelte';
|
||||
import { Gear, Robot, ShieldCheck, CurrencyCircleDollar, Cloud } from '@mana/shared-icons';
|
||||
import { Gear, Robot, ShieldCheck, Cloud } from '@mana/shared-icons';
|
||||
|
||||
export type CategoryId = 'general' | 'ai' | 'security' | 'credits' | 'data';
|
||||
export type CategoryId = 'general' | 'ai' | 'security' | 'data';
|
||||
|
||||
export interface Category {
|
||||
id: CategoryId;
|
||||
|
|
@ -39,13 +39,6 @@ export const categories: Category[] = [
|
|||
icon: ShieldCheck,
|
||||
anchors: ['passkeys', 'sessions', 'two-factor', 'vault', 'security-log'],
|
||||
},
|
||||
{
|
||||
id: 'credits',
|
||||
label: 'Credits',
|
||||
description: 'Guthaben & Transaktionen',
|
||||
icon: CurrencyCircleDollar,
|
||||
anchors: ['credits'],
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
label: 'Daten & Sync',
|
||||
|
|
@ -143,21 +136,6 @@ export const searchIndex: SearchEntry[] = [
|
|||
anchor: 'security-log',
|
||||
},
|
||||
|
||||
// Credits
|
||||
{
|
||||
label: 'Credits-Guthaben',
|
||||
keywords: ['balance', 'geld'],
|
||||
category: 'credits',
|
||||
anchor: 'credits',
|
||||
},
|
||||
{
|
||||
label: 'Credits kaufen',
|
||||
keywords: ['buy', 'pakete', 'kaufen'],
|
||||
category: 'credits',
|
||||
anchor: 'credits',
|
||||
},
|
||||
{ label: 'Transaktionen', keywords: ['history'], category: 'credits', anchor: 'credits' },
|
||||
|
||||
// Data
|
||||
{
|
||||
label: 'Cloud Sync',
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { CurrencyCircleDollar } from '@mana/shared-icons';
|
||||
import { creditsService } from '$lib/api/credits';
|
||||
import type { CreditBalance } from '$lib/api/credits';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import SettingsPanel from '../SettingsPanel.svelte';
|
||||
import SettingsSectionHeader from '../SettingsSectionHeader.svelte';
|
||||
|
||||
let creditBalance = $state<CreditBalance | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
if (!authStore.isAuthenticated) return;
|
||||
try {
|
||||
creditBalance = await creditsService.getBalance();
|
||||
} catch (e) {
|
||||
console.error('CreditsSection load failed:', e);
|
||||
}
|
||||
});
|
||||
|
||||
function formatCredits(amount: number): string {
|
||||
return amount.toLocaleString('de-DE');
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsPanel id="credits">
|
||||
<SettingsSectionHeader
|
||||
icon={CurrencyCircleDollar}
|
||||
title="Credits"
|
||||
description="Dein Guthaben für Mana Apps"
|
||||
tone="yellow"
|
||||
>
|
||||
{#snippet action()}
|
||||
<a href="/?app=credits" class="text-sm text-primary hover:underline">Alle Details</a>
|
||||
{/snippet}
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-3">
|
||||
<div class="rounded-lg bg-surface-hover p-4 text-center">
|
||||
<p class="text-sm text-muted-foreground">Verfügbar</p>
|
||||
<p class="text-2xl font-bold text-primary">
|
||||
{creditBalance ? formatCredits(creditBalance.balance) : '...'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-surface-hover p-4 text-center">
|
||||
<p class="text-sm text-muted-foreground">Gratis heute</p>
|
||||
<p class="text-2xl font-bold text-green-600 dark:text-green-400">
|
||||
{creditBalance
|
||||
? `${creditBalance.freeCreditsRemaining}/${creditBalance.dailyFreeCredits}`
|
||||
: '...'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-surface-hover p-4 text-center">
|
||||
<p class="text-sm text-muted-foreground">Gesamt verbraucht</p>
|
||||
<p class="text-2xl font-bold">
|
||||
{creditBalance ? formatCredits(creditBalance.totalSpent) : '...'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex gap-2">
|
||||
<a
|
||||
href="/?app=credits&tab=packages"
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
Credits kaufen
|
||||
</a>
|
||||
<a
|
||||
href="/?app=credits&tab=transactions"
|
||||
class="inline-flex items-center gap-2 rounded-lg border border-border px-4 py-2 text-sm font-medium transition-colors hover:bg-surface-hover"
|
||||
>
|
||||
Transaktionen
|
||||
</a>
|
||||
</div>
|
||||
</SettingsPanel>
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -15,7 +15,6 @@
|
|||
import GeneralSection from '$lib/components/settings/sections/GeneralSection.svelte';
|
||||
import AiSection from '$lib/components/settings/sections/AiSection.svelte';
|
||||
import SecuritySection from '$lib/components/settings/sections/SecuritySection.svelte';
|
||||
import CreditsSection from '$lib/components/settings/sections/CreditsSection.svelte';
|
||||
import DataSection from '$lib/components/settings/sections/DataSection.svelte';
|
||||
|
||||
let activeCategory = $state<CategoryId>('general');
|
||||
|
|
@ -76,8 +75,6 @@
|
|||
<AiSection />
|
||||
{:else if activeCategory === 'security'}
|
||||
<SecuritySection />
|
||||
{:else if activeCategory === 'credits'}
|
||||
<CreditsSection />
|
||||
{:else if activeCategory === 'data'}
|
||||
<DataSection />
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue