feat(oxibadge): add onboarding flow with i18n and reader preferences
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:
2026-10-07 11:08:02 +02:00
parent 268a917a69
commit 99772b9e4a
19 changed files with 821 additions and 29 deletions
+93
View File
@@ -0,0 +1,93 @@
<script lang="ts">
let {
label,
type = 'button',
faded = false,
busy = false,
disabled = false,
onclick
}: {
label: string;
type?: 'button' | 'submit';
faded?: boolean;
busy?: boolean;
disabled?: boolean;
onclick?: () => void;
} = $props();
</script>
<button {type} class="btn" class:faded class:busy {disabled} {onclick}>
<span>{label}</span>
{#if busy}
<span class="spinner"></span>
{:else}
<svg width="18" height="18" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true"
><path d="M5 12h14M13 6l6 6-6 6" /></svg
>
{/if}
</button>
<style>
.btn {
all: unset;
box-sizing: border-box;
height: var(--ox-control-h);
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
border-radius: var(--ox-radius);
background: var(--ox-accent);
color: var(--ox-on-accent);
font-family: var(--ox-font-body);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition:
filter 0.15s,
transform 0.1s,
opacity 0.2s;
}
.btn:not(:disabled):hover {
filter: brightness(1.08);
}
.btn:not(:disabled):active {
transform: translateY(1px);
}
.btn:focus-visible {
outline: 2px solid #ffffff;
outline-offset: 3px;
}
.btn:disabled {
cursor: not-allowed;
}
.btn.faded {
opacity: 0.35;
}
.btn.busy {
opacity: 0.7;
cursor: progress;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(26, 7, 0, 0.35);
border-top-color: var(--ox-on-accent);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>