Check, format and test / build (push) Successful in 10m22s
Add a settings screen (gear button on the month-confirmation screen) to configure the expected shift times, the rounding step, the interface language, and the time clock address — replacing the old onboarding-only connection flow. Settings persist to the same preferences.toml as the reader address, under a single load_settings/ save_settings command pair. The review screen's "fix shifts" action now redistributes stacked punches by nearest configured shift time instead of blind position, and a stacked cell manually cleared down to one entry no longer leaves a dangling empty input. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
136 lines
2.6 KiB
Svelte
136 lines
2.6 KiB
Svelte
<script lang="ts">
|
|
import type { Snippet } from 'svelte';
|
|
|
|
let {
|
|
id,
|
|
label,
|
|
value = $bindable(),
|
|
type = 'text',
|
|
placeholder = '',
|
|
maxlength,
|
|
inputmode,
|
|
err = false,
|
|
suffix,
|
|
oninput,
|
|
icon
|
|
}: {
|
|
id: string;
|
|
label: string;
|
|
value: string | number;
|
|
type?: string;
|
|
placeholder?: string;
|
|
maxlength?: number;
|
|
inputmode?: 'text' | 'numeric' | 'decimal' | 'none' | 'tel' | 'search' | 'email' | 'url';
|
|
err?: boolean;
|
|
suffix?: string;
|
|
oninput?: (e: Event & { currentTarget: HTMLInputElement }) => void;
|
|
icon?: Snippet;
|
|
} = $props();
|
|
</script>
|
|
|
|
<div class="field" class:err>
|
|
<label for={id}>{label}</label>
|
|
{#if icon}{@render icon()}{/if}
|
|
<input
|
|
{id}
|
|
{type}
|
|
{placeholder}
|
|
{maxlength}
|
|
{inputmode}
|
|
bind:value
|
|
{oninput}
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
/>
|
|
{#if suffix}<span class="suffix">{suffix}</span>{/if}
|
|
</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.err,
|
|
.field.err:hover,
|
|
.field.err:focus-within {
|
|
border-color: #e5484d;
|
|
}
|
|
|
|
.field.err:focus-within {
|
|
box-shadow: 0 0 0 4px rgba(229, 72, 77, 0.18);
|
|
}
|
|
|
|
.field.err label {
|
|
color: #ff8a8f;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.suffix {
|
|
flex: none;
|
|
font-size: 14px;
|
|
color: var(--ox-text-subtle);
|
|
}
|
|
</style>
|