feat(ui): icon nav tabs + elevation for the Settings modal

Recording/Hardware/Storage/Calendar/Sync/Privacy nav tabs get matching
icons; the active tab now uses the accent color instead of an inverted
fg/bg swap. Check/X/AlertTriangle icons replace ✓/✗/✕/⚠ throughout
(connection test result, close button, third-party-cloud warning
banner, no-egress confirmation). The modal itself gets proper
elevation (shadow + larger radius) instead of a flat bordered box.
This commit is contained in:
iamdoubz committed 2026-07-02 12:57:36 -05:00
1 parent ecf38d5313
commit df82b0636d
1 file changed
+101 -34
+101 -34
View File
@@ -9,6 +9,17 @@
import { open } from "@tauri-apps/plugin-dialog"; import { open } from "@tauri-apps/plugin-dialog";
import type { BackendId, SyncKind, SyncTargetConfig } from "../api"; import type { BackendId, SyncKind, SyncTargetConfig } from "../api";
import { trapFocus } from "../actions/trapFocus"; import { trapFocus } from "../actions/trapFocus";
import {
X,
Check,
AlertTriangle,
Mic,
Cpu,
HardDrive,
CalendarDays,
UploadCloud,
ShieldCheck,
} from "@lucide/svelte";
let { onClose }: { onClose: () => void } = $props(); let { onClose }: { onClose: () => void } = $props();
@@ -49,7 +60,7 @@
return Math.round((p.received / p.total) * 100); return Math.round((p.received / p.total) * 100);
} }
let showConsent = $state(false); let showConsent = $state(false);
let testResult = $state<string | null>(null); let testResult = $state<{ ok: boolean; message: string } | null>(null);
onMount(() => { onMount(() => {
settings.load(); settings.load();
@@ -113,7 +124,7 @@
} }
async function testConnection() { async function testConnection() {
const r = await settings.test(form); const r = await settings.test(form);
testResult = (r.ok ? "✓ " : "✗ ") + r.message; testResult = { ok: r.ok, message: r.message };
} }
async function addTarget() { async function addTarget() {
await settings.addTarget(form); await settings.addTarget(form);
@@ -138,24 +149,28 @@
<header> <header>
<strong>Settings</strong> <strong>Settings</strong>
<nav> <nav>
<button class:active={section === "recording"} onclick={() => (section = "recording")} <button class:active={section === "recording"} onclick={() => (section = "recording")}>
>Recording</button <Mic size={14} aria-hidden="true" /> Recording
> </button>
<button class:active={section === "hardware"} onclick={() => (section = "hardware")} <button class:active={section === "hardware"} onclick={() => (section = "hardware")}>
>Hardware</button <Cpu size={14} aria-hidden="true" /> Hardware
> </button>
<button class:active={section === "storage"} onclick={() => (section = "storage")} <button class:active={section === "storage"} onclick={() => (section = "storage")}>
>Storage</button <HardDrive size={14} aria-hidden="true" /> Storage
> </button>
<button class:active={section === "calendar"} onclick={() => (section = "calendar")} <button class:active={section === "calendar"} onclick={() => (section = "calendar")}>
>Calendar</button <CalendarDays size={14} aria-hidden="true" /> Calendar
> </button>
<button class:active={section === "sync"} onclick={() => (section = "sync")}>Sync</button> <button class:active={section === "sync"} onclick={() => (section = "sync")}>
<button class:active={section === "privacy"} onclick={() => (section = "privacy")} <UploadCloud size={14} aria-hidden="true" /> Sync
>Privacy</button </button>
> <button class:active={section === "privacy"} onclick={() => (section = "privacy")}>
<ShieldCheck size={14} aria-hidden="true" /> Privacy
</button>
</nav> </nav>
<button class="close" onclick={onClose} aria-label="Close">✕</button> <button class="close" onclick={onClose} aria-label="Close" title="Close (Esc)">
<X size={18} aria-hidden="true" />
</button>
</header> </header>
{#if settings.backendStub && section === "sync"} {#if settings.backendStub && section === "sync"}
@@ -449,11 +464,20 @@
<button class="primary" onclick={addTarget} disabled={!form.name || !form.base_url} <button class="primary" onclick={addTarget} disabled={!form.name || !form.base_url}
>Add target</button >Add target</button
> >
{#if testResult}<span class="test">{testResult}</span>{/if} {#if testResult}
<span class="test" class:ok={testResult.ok} class:fail={!testResult.ok}>
{#if testResult.ok}<Check size={14} aria-hidden="true" />{:else}<X
size={14}
aria-hidden="true"
/>{/if}
{testResult.message}
</span>
{/if}
</div> </div>
{:else} {:else}
<div class="banner"> <div class="banner">
⚠ {form.kind} is a third-party cloud — uploading sends your data off your device to {form.kind}. <AlertTriangle size={14} aria-hidden="true" />
{form.kind} is a third-party cloud — uploading sends your data off your device to {form.kind}.
</div> </div>
<div class="grid"> <div class="grid">
<label class="wide">Name<input bind:value={form.name} placeholder={form.kind} /></label> <label class="wide">Name<input bind:value={form.name} placeholder={form.kind} /></label>
@@ -494,7 +518,10 @@
<h4>Egress allowlist</h4> <h4>Egress allowlist</h4>
{#if settings.privacy.allowlistedHosts.length === 0} {#if settings.privacy.allowlistedHosts.length === 0}
<p class="muted confirm">✓ No hosts are allowlisted — WA makes no content egress.</p> <p class="muted confirm">
<Check size={14} aria-hidden="true" />
No hosts are allowlisted — WA makes no content egress.
</p>
{:else} {:else}
<ul class="hosts"> <ul class="hosts">
{#each settings.privacy.allowlistedHosts as host (host)} {#each settings.privacy.allowlistedHosts as host (host)}
@@ -532,25 +559,26 @@
.overlay { .overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(0, 0, 0, 0.45); background: rgba(10, 12, 16, 0.5);
display: grid; display: grid;
place-items: center; place-items: center;
z-index: 50; z-index: 50;
} }
.panel { .panel {
background: var(--bg); background: var(--bg-elevated);
color: var(--fg); color: var(--fg);
width: min(720px, 92vw); width: min(720px, 92vw);
max-height: 88vh; max-height: 88vh;
overflow: auto; overflow: auto;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 10px; border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
padding: 0 1.25rem 1.25rem; padding: 0 1.25rem 1.25rem;
} }
header { header {
position: sticky; position: sticky;
top: 0; top: 0;
background: var(--bg); background: var(--bg-elevated);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
@@ -563,25 +591,44 @@
} }
nav button, nav button,
.kinds button { .kinds button {
display: inline-flex;
align-items: center;
gap: 0.35rem;
background: transparent; background: transparent;
border: 1px solid var(--border); border: 1px solid var(--border);
color: var(--fg); color: var(--fg);
padding: 0.3rem 0.7rem; padding: 0.35rem 0.7rem;
border-radius: 6px; border-radius: var(--radius-sm);
font-size: 0.85rem;
cursor: pointer; cursor: pointer;
transition: background-color 120ms ease-out;
}
nav button:hover,
.kinds button:hover {
background: var(--bg-hover);
} }
nav button.active, nav button.active,
.kinds button.active { .kinds button.active {
background: var(--fg); background: var(--accent);
color: var(--bg); color: var(--accent-fg);
border-color: transparent;
} }
.close { .close {
display: grid;
place-items: center;
margin-left: auto; margin-left: auto;
width: 30px;
height: 30px;
background: transparent; background: transparent;
border: none; border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--muted); color: var(--muted);
cursor: pointer; cursor: pointer;
font-size: 1rem; transition: background-color 120ms ease-out;
}
.close:hover {
background: var(--bg-hover);
color: var(--fg);
} }
h3 { h3 {
margin: 1rem 0 0.5rem; margin: 1rem 0 0.5rem;
@@ -687,13 +734,21 @@
gap: 0.3rem; gap: 0.3rem;
} }
.banner { .banner {
background: color-mix(in srgb, var(--danger) 14%, var(--bg)); display: flex;
align-items: flex-start;
gap: 0.4rem;
background: color-mix(in srgb, var(--warning) 14%, var(--bg));
border: 1px solid var(--border); border: 1px solid var(--border);
padding: 0.5rem 0.7rem; padding: 0.5rem 0.7rem;
border-radius: 6px; border-radius: var(--radius-sm);
font-size: 0.85rem; font-size: 0.85rem;
margin: 0.5rem 0; margin: 0.5rem 0;
} }
.banner :global(svg) {
flex-shrink: 0;
margin-top: 0.15rem;
color: var(--warning);
}
ul.targets, ul.targets,
ul.models, ul.models,
ul.events { ul.events {
@@ -764,10 +819,22 @@
color: var(--danger); color: var(--danger);
} }
.test { .test {
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.82rem; font-size: 0.82rem;
color: var(--muted); color: var(--muted);
} }
.test.ok {
color: var(--success);
}
.test.fail {
color: var(--danger);
}
.confirm { .confirm {
display: flex;
align-items: center;
gap: 0.35rem;
color: var(--success); color: var(--success);
} }
ul.hosts { ul.hosts {