feat(oxibadge): add onboarding flow with i18n and reader preferences
Check, format and test / build (push) Successful in 9m59s
Check, format and test / build (push) Successful in 9m59s
Adds the first real screen: a setup form for the device's host/port, tested against the real device (3 retries) and persisted to preferences.toml before showing a placeholder "done" screen. - Rust: test_connection/save_preferences/load_preferences commands; preferences stored as [[reader]] in TOML so more readers can be added later without a format change. - Svelte: split the monolithic page into reusable components (Screen, Column, Field, Button, ErrorBanner, Hint) under src/lib/components, sharing design tokens from app.css. - Automatic it/en i18n via svelte-i18n, locale detected from navigator.language. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import Column from './Column.svelte';
|
||||
import Field from './Field.svelte';
|
||||
import Button from './Button.svelte';
|
||||
import ErrorBanner from './ErrorBanner.svelte';
|
||||
import Hint from './Hint.svelte';
|
||||
|
||||
let { onconnected }: { onconnected: (cfg: { host: string; port: number }) => void } = $props();
|
||||
|
||||
type Phase = 'idle' | 'testing' | 'err';
|
||||
|
||||
let host = $state('');
|
||||
let port = $state(5005);
|
||||
let phase = $state<Phase>('idle');
|
||||
let errorCode = $state('');
|
||||
|
||||
const trimmedHost = $derived(host.trim());
|
||||
const empty = $derived(trimmedHost === '');
|
||||
const disabled = $derived(empty || phase === 'testing');
|
||||
const btnLabel = $derived(
|
||||
phase === 'testing'
|
||||
? $_('onboarding.button.testing')
|
||||
: phase === 'err'
|
||||
? $_('onboarding.button.retry')
|
||||
: $_('onboarding.button.idle')
|
||||
);
|
||||
|
||||
function resetPhase() {
|
||||
phase = 'idle';
|
||||
errorCode = '';
|
||||
}
|
||||
|
||||
async function testAndSave() {
|
||||
if (disabled) return;
|
||||
phase = 'testing';
|
||||
errorCode = '';
|
||||
try {
|
||||
await invoke('test_connection', { host: trimmedHost, port });
|
||||
await invoke('save_preferences', { host: trimmedHost, port });
|
||||
onconnected({ host: trimmedHost, port });
|
||||
} catch (err) {
|
||||
errorCode = String(err);
|
||||
phase = 'err';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Column>
|
||||
<header class="intro">
|
||||
<h1>{$_('onboarding.title')}</h1>
|
||||
<p>{$_('onboarding.subtitle')}</p>
|
||||
</header>
|
||||
|
||||
<form
|
||||
class="form"
|
||||
onsubmit={(event) => {
|
||||
event.preventDefault();
|
||||
testAndSave();
|
||||
}}
|
||||
>
|
||||
<div class="row">
|
||||
<Field
|
||||
id="ox-host"
|
||||
label={$_('onboarding.host_label')}
|
||||
bind:value={host}
|
||||
placeholder="192.168.1.100"
|
||||
oninput={resetPhase}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="3" y="4" width="18" height="12" rx="2" /><path d="M8 20h8M12 16v4" />
|
||||
</svg>
|
||||
{/snippet}
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
id="ox-port"
|
||||
label={$_('onboarding.port_label')}
|
||||
bind:value={port}
|
||||
type="number"
|
||||
placeholder="5005"
|
||||
oninput={resetPhase}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" label={btnLabel} faded={empty} busy={phase === 'testing'} {disabled} />
|
||||
|
||||
{#if phase === 'err'}
|
||||
<ErrorBanner title={$_('onboarding.error_title')} message={$_(`onboarding.errors.${errorCode}`)} />
|
||||
{/if}
|
||||
|
||||
<Hint text={$_('onboarding.hint')} />
|
||||
</form>
|
||||
</Column>
|
||||
|
||||
<style>
|
||||
.intro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-family: var(--ox-font-display);
|
||||
font-weight: 700;
|
||||
font-size: 48px;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--ox-text-strong);
|
||||
}
|
||||
|
||||
.intro p {
|
||||
margin: 0;
|
||||
max-width: 340px;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
color: var(--ox-text-muted);
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 108px;
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user