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
+96
View File
@@ -0,0 +1,96 @@
<script lang="ts">
import type { Snippet } from 'svelte';
let {
id,
label,
value = $bindable(),
type = 'text',
placeholder = '',
oninput,
icon
}: {
id: string;
label: string;
value: string | number;
type?: string;
placeholder?: string;
oninput?: () => void;
icon?: Snippet;
} = $props();
</script>
<div class="field">
<label for={id}>{label}</label>
{#if icon}{@render icon()}{/if}
<input {id} {type} {placeholder} bind:value {oninput} autocomplete="off" spellcheck="false" />
</div>
<style>
.field {
position: relative;
height: var(--ox-control-h);
box-sizing: border-box;
display: flex;
align-items: center;
gap: 10px;
padding: 0 16px;
background: var(--ox-bg);
border: 1px solid var(--ox-border);
border-radius: var(--ox-radius);
transition:
border-color 0.15s,
box-shadow 0.15s;
}
.field:hover {
border-color: var(--ox-border-hover);
}
.field:focus-within {
border-color: var(--ox-accent);
box-shadow: 0 0 0 4px var(--ox-accent-ring);
}
.field label {
position: absolute;
top: -9px;
left: 12px;
padding: 0 6px;
background: var(--ox-bg);
font-size: 12px;
line-height: 16px;
font-weight: 500;
color: var(--ox-label);
transition: color 0.15s;
}
.field:focus-within label {
color: var(--ox-accent-soft);
}
.field :global(svg) {
color: #6e6e6e;
}
.field input {
all: unset;
box-sizing: border-box;
min-width: 0;
width: 100%;
font-family: var(--ox-font-mono);
font-size: 16px;
color: var(--ox-text);
caret-color: var(--ox-accent);
}
.field input::placeholder {
color: var(--ox-placeholder);
}
.field input[type='number']::-webkit-outer-spin-button,
.field input[type='number']::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
</style>