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:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user