fix(web): minor UI refinements across header, pills, swipe, and settings

- Remove ThemeSwitcher from header (already accessible via settings)
- Increase pill padding and font size for better tap targets
- Guard non-cancelable touchmove preventDefault in SwipeAction
- Restyle settings page with grid-area layout and inline sign-out button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 14:59:58 +01:00
parent 3bb2ef2759
commit b3c30738bd
4 changed files with 147 additions and 117 deletions
@@ -1,6 +1,5 @@
<script> <script>
import ReportPicker from './ReportPicker.svelte'; import ReportPicker from './ReportPicker.svelte';
import ThemeSwitcher from './ThemeSwitcher.svelte';
/** /**
* @type {string} * @type {string}
@@ -45,7 +44,6 @@
</button> </button>
<div class="header-actions"> <div class="header-actions">
<ThemeSwitcher mode="cycle" />
<a href="/settings" class="settings-btn" aria-label="Settings"> <a href="/settings" class="settings-btn" aria-label="Settings">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
@@ -7,14 +7,14 @@
export let visible = false; export let visible = false;
const pills = [ const pills = [
{ label: 'Due', text: 'due:' }, { label: "Due", text: "due:" },
{ label: 'Pri', text: 'priority:' }, { label: "Pri", text: "priority:" },
{ label: 'Project', text: 'project:' }, { label: "Project", text: "project:" },
{ label: 'Tag', text: '+' }, { label: "Tag", text: "+" },
{ label: 'Recur', text: 'recur:' }, { label: "Recur", text: "recur:" },
{ label: 'Scheduled', text: 'scheduled:' }, { label: "Scheduled", text: "scheduled:" },
{ label: 'Wait', text: 'wait:' }, { label: "Wait", text: "wait:" },
{ label: 'Until', text: 'until:' } { label: "Until", text: "until:" },
]; ];
</script> </script>
@@ -41,11 +41,11 @@
} }
.pill { .pill {
padding: 0.25rem 0.625rem; padding: 0.375rem 0.75rem;
background-color: var(--bg-tertiary); background-color: var(--bg-tertiary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 1rem; border-radius: 1rem;
font-size: var(--font-size-xs); font-size: var(--font-size-s);
font-family: inherit; font-family: inherit;
color: var(--text-secondary); color: var(--text-secondary);
cursor: pointer; cursor: pointer;
@@ -51,7 +51,7 @@
} }
if (swiping) { if (swiping) {
e.preventDefault(); if (e.cancelable) e.preventDefault();
// Only allow right swipe // Only allow right swipe
offsetX = Math.max(0, deltaX); offsetX = Math.max(0, deltaX);
} }
+119 -87
View File
@@ -74,107 +74,112 @@
} }
</script> </script>
<div class="page"> <header class="settings-header">
<div class="container"> <a href="/" class="back-link" aria-label="Back to tasks">
<div class="page-header"> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<a href="/" class="back-link" aria-label="Back to tasks"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
<svg class="back-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"> </svg>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /> </a>
</svg> <h1>Settings</h1>
</a> {#if $authStore.isAuthenticated}
<h1>Settings</h1> <button class="signout-btn" on:click={logout} aria-label="Sign out">
</div> <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
</button>
{/if}
</header>
<div class="settings-content">
<section class="section">
<h2>Theme</h2>
<ThemeSwitcher mode="full" />
</section>
{#if $authStore.isAuthenticated}
<section class="section"> <section class="section">
<h2>Theme</h2> <h2>Account</h2>
<ThemeSwitcher mode="full" /> <div class="info-row">
<span class="label">Username:</span>
<span class="value">{$authStore.user?.username || 'Unknown'}</span>
</div>
{#if $authStore.user?.email}
<div class="info-row">
<span class="label">Email:</span>
<span class="value">{$authStore.user.email}</span>
</div>
{/if}
</section> </section>
{#if $authStore.isAuthenticated} <section class="section">
<section class="section"> <h2>Sync</h2>
<h2>Account</h2> <div class="info-row">
<span class="label">Status:</span>
<span class="value">{$syncStore.status}</span>
</div>
<div class="info-row">
<span class="label">Queue:</span>
<span class="value">{$syncStore.queueSize} changes</span>
</div>
{#if $syncStore.lastSync}
<div class="info-row"> <div class="info-row">
<span class="label">Username:</span> <span class="label">Last Sync:</span>
<span class="value">{$authStore.user?.username || 'Unknown'}</span> <span class="value">{new Date($syncStore.lastSync * 1000).toLocaleString()}</span>
</div> </div>
{#if $authStore.user?.email} {/if}
<div class="info-row">
<span class="label">Email:</span>
<span class="value">{$authStore.user.email}</span>
</div>
{/if}
</section>
<section class="section"> <Button on:click={triggerSync} disabled={$syncStore.status === 'syncing'}>
<h2>Sync</h2> {$syncStore.status === 'syncing' ? 'Syncing...' : 'Sync Now'}
<div class="info-row"> </Button>
<span class="label">Status:</span> </section>
<span class="value">{$syncStore.status}</span> {:else}
</div> <section class="section">
<div class="info-row"> <h2>API Key Authentication</h2>
<span class="label">Queue:</span> <p class="text-secondary mb-md">
<span class="value">{$syncStore.queueSize} changes</span> For testing, you can authenticate with an API key. Generate a key using:
</div> <code>opal server keygen --name "Web"</code>
{#if $syncStore.lastSync} </p>
<div class="info-row">
<span class="label">Last Sync:</span>
<span class="value">{new Date($syncStore.lastSync * 1000).toLocaleString()}</span>
</div>
{/if}
<Button on:click={triggerSync} disabled={$syncStore.status === 'syncing'}> <Input
{$syncStore.status === 'syncing' ? 'Syncing...' : 'Sync Now'} label="API Key"
</Button> type="password"
</section> placeholder="oak_..."
bind:value={apiKey}
{error}
/>
<section class="section"> <Button
<h2>Actions</h2> on:click={saveApiKey}
<Button variant="danger" on:click={logout}>Logout</Button> loading={saving}
</section> fullWidth
{:else} >
<section class="section"> Save API Key
<h2>API Key Authentication</h2> </Button>
<p class="text-secondary mb-md">
For testing, you can authenticate with an API key. Generate a key using: <div class="mt-lg text-center">
<code>opal server keygen --name "Web"</code> <p class="text-sm text-secondary">
Or <a href="/auth/login">login with OAuth</a>
</p> </p>
</div>
<Input </section>
label="API Key" {/if}
type="password"
placeholder="oak_..."
bind:value={apiKey}
{error}
/>
<Button
on:click={saveApiKey}
loading={saving}
fullWidth
>
Save API Key
</Button>
<div class="mt-lg text-center">
<p class="text-sm text-secondary">
Or <a href="/auth/login">login with OAuth</a>
</p>
</div>
</section>
{/if}
</div>
</div> </div>
<style> <style>
.page-header { .settings-header {
grid-area: header;
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-sm); gap: var(--spacing-sm);
padding-top: var(--spacing-lg); padding: var(--spacing-sm) var(--spacing-md);
margin-bottom: var(--spacing-md); background-color: var(--bg-primary);
border-bottom: 1px solid var(--border-color);
} }
.page-header h1 { .settings-header h1 {
flex: 1;
font-size: var(--font-size-lg);
font-weight: 600;
margin-bottom: 0; margin-bottom: 0;
} }
@@ -190,21 +195,48 @@
} }
.back-link:hover { .back-link:hover {
background-color: var(--bg-tertiary); background-color: var(--bg-secondary);
text-decoration: none; text-decoration: none;
} }
.back-icon { .signout-btn {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
background: none;
border: none;
border-radius: var(--border-radius);
color: var(--text-secondary);
cursor: pointer;
transition: background-color 0.2s;
}
.signout-btn:hover {
background-color: var(--bg-secondary);
color: var(--color-danger);
}
.icon {
width: 1.25rem; width: 1.25rem;
height: 1.25rem; height: 1.25rem;
} }
.section { .settings-content {
grid-area: content;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding: var(--spacing-md);
min-height: 0;
background-color: var(--bg-primary); background-color: var(--bg-primary);
}
.section {
background-color: var(--bg-secondary);
border-radius: var(--border-radius); border-radius: var(--border-radius);
padding: var(--spacing-lg); padding: var(--spacing-lg);
margin-bottom: var(--spacing-lg); margin-bottom: var(--spacing-lg);
box-shadow: var(--shadow-sm);
} }
.info-row { .info-row {