Use shared form controls and enforce UI patterns (Refs #110)
CI / checks (push) Successful in 4m51s

This commit is contained in:
2026-10-10 00:19:48 -04:00
parent 8d1bb69591
commit cc9c10226e
54 changed files with 1431 additions and 1449 deletions
+22 -31
View File
@@ -1,4 +1,7 @@
<script module lang="ts">
import Select from '$ui/components/primitives/Select.svelte';
import Input from '$ui/components/primitives/Input.svelte';
import Field from '$ui/components/primitives/Field.svelte';
const serverTitleCollator = new Intl.Collator(undefined, { numeric: true });
</script>
@@ -443,12 +446,8 @@
{/if}
<div class="toolbar">
<div class="search-row">
<label class="search"
><span class="sr-only">Search servers, maps, or players</span><input
type="search"
bind:value={query}
placeholder="Search fleet…"
/></label
<Field class="search" hiddenLabel label="Search servers, maps, or players">
<Input type="search" bind:value={query} placeholder="Search fleet…" /></Field
>
<button
class="control"
@@ -459,27 +458,27 @@
>
</div>
{#if viewOptionsOpen}<div id="fleet-view-options" class="view-options">
<label
>Game<select bind:value={game}
<Field label="Game">
<Select bind:value={game}
><option value="">All games</option><option value="t5"
>Black Ops I</option
><option value="t6">Black Ops II</option></select
></label
><option value="t6">Black Ops II</option></Select
></Field
>
<label
>Group by<select bind:value={grouping}
<Field label="Group by">
<Select bind:value={grouping}
><option value="none">None</option><option value="map">Map</option
><option value="game">Game</option></select
></label
><option value="game">Game</option></Select
></Field
>
<label
>Sort by<select bind:value={sort}
<Field label="Sort by">
<Select bind:value={sort}
><option value="duration">Longest match</option><option value="players"
>Most players</option
><option value="round">Highest round</option><option value="name"
>Lobby name</option
></select
></label
></Select
></Field
>
<div class="view-toggle" role="group" aria-label="Display mode">
<button
@@ -698,8 +697,7 @@
font-size: var(--text-xs);
}
button,
input,
select {
input {
font: inherit;
}
button {
@@ -750,15 +748,11 @@
gap: var(--space-3);
align-items: center;
}
.search {
.search-row :global(.search) {
flex: 1;
min-width: 0;
}
.search input {
width: 100%;
}
input,
select {
input {
color: var(--text-primary);
background: var(--surface-sunken);
border: 1px solid var(--border-control);
@@ -790,12 +784,9 @@
background: var(--surface);
border-radius: var(--radius-md);
}
.view-options label {
display: grid;
gap: var(--space-2);
.view-options :global(.field) {
flex: 1 1 130px;
color: var(--text-secondary);
font-size: var(--text-xs);
}
.chips,
.view-toggle {
+38 -54
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import Select from '$ui/components/primitives/Select.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import Input from '$ui/components/primitives/Input.svelte';
import { onMount } from 'svelte';
import { timeFormat, timeZone, timeZonePersistenceError } from '$ui/stores/time-zone.js';
import { availableTimeZones, DEFAULT_TIME_ZONE } from '$lib/time-zone.js';
@@ -105,53 +108,57 @@
</p>{/if}
<form onsubmit={changePassword} aria-busy={busy}>
<fieldset disabled={busy || signingOut}>
<input
type="text"
class="password-username"
value={user.username}
autocomplete="username"
aria-label="Username"
readonly
tabindex="-1"
/>
<label for="current-password"
>{user.mustChangePassword ? 'Temporary password' : 'Current password'}<input
<Field label="Username" hiddenLabel class="password-username"
><Input
type="text"
value={user.username}
autocomplete="username"
aria-label="Username"
readonly
tabindex={-1}
/></Field
>
<Field
forId="current-password"
label={user.mustChangePassword ? 'Temporary password' : 'Current password'}
>
<Input
id="current-password"
name="currentPassword"
type="password"
autocomplete="current-password"
required
maxlength="128"
maxlength={128}
bind:value={currentPassword}
/></label
/></Field
>
<label for="new-password"
>New password<input
<Field forId="new-password" label="New password">
<Input
id="new-password"
name="newPassword"
type="password"
autocomplete="new-password"
required
minlength="8"
maxlength="128"
minlength={8}
maxlength={128}
bind:value={newPassword}
aria-describedby="new-password-help"
/></label
/></Field
>
<p id="new-password-help" class="hint">
Use 8–128 characters and a password you do not use elsewhere.
</p>
<label for="confirm-password"
>Confirm new password<input
<Field forId="confirm-password" label="Confirm new password">
<Input
id="confirm-password"
name="confirmPassword"
type="password"
autocomplete="new-password"
required
minlength="8"
maxlength="128"
minlength={8}
maxlength={128}
bind:value={confirmPassword}
/></label
/></Field
>
<div>
<Button
@@ -169,16 +176,15 @@
</section>
<section class="panel" aria-labelledby="display-heading">
<h2 id="display-heading">Display preferences</h2>
<label class="timezone"
>Timezone
<select bind:value={$timeZone} aria-describedby="timezone-help">
<Field class="timezone" label="Timezone">
<Select bind:value={$timeZone} aria-describedby="timezone-help">
{#each zones as zone}<option value={zone}
>{zone.replaceAll('_', ' ')}{zone === DEFAULT_TIME_ZONE
? ' (default)'
: ''}</option
>{/each}
</select>
</label>
</Select>
</Field>
<p id="timezone-help">
Dates and times throughout the app use this timezone, including daylight saving
changes. Saved automatically in this browser.
@@ -193,23 +199,14 @@
</section>
<style>
.timezone {
.panel :global(.timezone) {
display: grid;
gap: 8px;
margin-top: 16px;
font-size: var(--text-sm);
max-width: 420px;
}
.timezone select {
width: 100%;
padding: 10px 12px;
font: inherit;
color: var(--text-primary);
background: var(--surface-sunken);
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
}
.timezone ~ p {
.panel :global(.timezone ~ p) {
font-size: var(--text-sm);
color: var(--text-secondary);
}
@@ -263,21 +260,8 @@
gap: 18px;
min-width: 0;
}
label {
display: grid;
gap: 8px;
font-size: var(--text-sm);
}
input {
width: 100%;
padding: 12px;
color: var(--text-primary);
background: var(--surface-sunken);
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
font: inherit;
}
.password-username {
.panel :global(.password-username) {
position: absolute;
width: 1px;
height: 1px;
+28 -40
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import Input from '$ui/components/primitives/Input.svelte';
import Select from '$ui/components/primitives/Select.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import { onMount } from 'svelte';
import { page } from '$app/state';
import { confirmAction } from '$ui/confirm.js';
@@ -215,8 +218,8 @@
>
<fieldset disabled={busy}>
<div class="fields">
<label
>Game<select
<Field label="Game">
<Select
bind:value={settings.game}
disabled={!!editing}
onchange={() => {
@@ -225,60 +228,60 @@
}}
><option value="t5">Black Ops I</option><option value="t6"
>Black Ops II</option
></select
></label
></Select
></Field
>
<label
>Scope<select bind:value={settings.mapSlug} disabled={!!editing}
<Field label="Scope">
<Select bind:value={settings.mapSlug} disabled={!!editing}
><option value="*">Every map in this game</option
>{#each zombieMaps(settings.game) as map}<option value={map.slug}
>{map.friendly}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
<label
>Autoscaling<select bind:value={settings.mode}
<Field label="Autoscaling">
<Select bind:value={settings.mode}
><option value="off">Off</option
>{#if report.removalAvailable}<option value="create-destroy"
>On</option
>{/if}</select
></label
>{/if}</Select
></Field
>
<label
>Minimum running lobbies<input
<Field label="Minimum running lobbies">
<Input
type="number"
min="1"
max="20"
required
bind:value={settings.minimum}
/></label
/></Field
>
<label
>Maximum lobbies<input
<Field label="Maximum lobbies">
<Input
type="number"
min="1"
max="20"
required
bind:value={settings.maximum}
/></label
/></Field
>
<label
>Empty spare target<input
<Field label="Empty spare target">
<Input
type="number"
min="1"
max="20"
required
bind:value={settings.spares}
/></label
/></Field
>
<label
>Empty duration in minutes<input
<Field label="Empty duration in minutes">
<Input
type="number"
min="1"
max="1440"
required
bind:value={settings.emptyMinutes}
/></label
/></Field
>
</div>
{#if settings.mapSlug === '*'}<p>
@@ -356,22 +359,7 @@
gap: 16px;
margin-bottom: 20px;
}
label {
display: flex;
flex-direction: column;
gap: 8px;
font-size: var(--text-sm);
}
input,
select {
padding: 10px;
min-width: 0;
color: var(--text-primary);
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
font: inherit;
}
fieldset {
border: 0;
padding: 0;
+14 -26
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import Select from '$ui/components/primitives/Select.svelte';
import Input from '$ui/components/primitives/Input.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import RelativeTime from '$ui/components/RelativeTime.svelte';
import { page } from '$app/stores';
import { contextHref } from '$lib/navigation-context.js';
@@ -158,8 +161,8 @@
ariaLabel="Beta views"
/>
</div>
<label class="search"
>Search<input
<Field class="search" label="Search">
<Input
type="search"
bind:value={search}
oninput={() => {
@@ -167,11 +170,11 @@
saveContext();
}}
placeholder="Name, beta number, file, or server"
maxlength="200"
/></label
maxlength={200}
/></Field
>
<label
>Status<select
<Field label="Status">
<Select
bind:value={status}
onchange={() => {
pageNumber = 1;
@@ -187,7 +190,7 @@
{:else}<option value="promoted">Promoted</option><option value="rolled-back"
>Rolled back</option
>{/if}
</select></label
</Select></Field
>
{#if betas.length && (search || status)}<Button
variant="ghost"
@@ -320,27 +323,12 @@
align-self: flex-end;
max-width: 100%;
}
label {
display: grid;
gap: 6px;
font-size: var(--text-xs);
color: var(--text-secondary);
}
.search {
.toolbar :global(.search) {
flex: 1;
min-width: 240px;
}
input,
select {
padding: 10px 12px;
min-height: 40px;
background: var(--surface-sunken);
color: var(--text-primary);
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
font: inherit;
font-size: var(--text-sm);
}
.table-scroll {
overflow-x: auto;
}
@@ -444,7 +432,7 @@
td {
padding: 12px 8px;
}
.search {
.toolbar :global(.search) {
flex-basis: 100%;
}
.pagination {
+37 -33
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import Select from '$ui/components/primitives/Select.svelte';
import Input from '$ui/components/primitives/Input.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import { countLabel } from '$ui/plural.js';
import { page } from '$app/stores';
import { contextHref, safeReturnHref } from '$lib/navigation-context.js';
@@ -667,29 +670,30 @@
<form onsubmit={openBeta} class="beta-form">
<h2 bind:this={stepHeading} tabindex="-1">{steps[step]}</h2>
{#if step === 0}
<label
>Beta label (optional)<input
<Field label="Beta label (optional)">
<Input
bind:value={betaLabel}
maxlength="120"
maxlength={120}
placeholder="For example, Town balance update"
/><span class="muted"
>Shown in the beta list so others can identify this test.</span
></label
></Field
>
<div class="picks">
{#each picks as pick, index (pick.key)}<section class="pick-row">
<label
>File {index + 1}<select
<Field
>{#snippet label()}File {index + 1}{/snippet}
<Select
bind:value={pick.scriptId}
onchange={() => onPickFile(index)}
><option value={null}>Choose a file</option
>{#each eligibleFor(index) as file}<option value={file.id}
>[{file.kind}] {file.name}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
<label
>Exact version<select
<Field label="Exact version">
<Select
bind:value={pick.versionId}
disabled={pick.scriptId === null ||
loadingVersions.has(pick.key)}
@@ -702,8 +706,8 @@
0,
8
)} · {version.state.replaceAll('-', ' ')}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
<Button
ariaLabel={`Remove file ${index + 1}`}
@@ -742,8 +746,8 @@
At launch, Electric Cherry chooses compatible servers with the fewest
current players. Every requested instance must pass the test plan.
</p>
<label class="instance-count"
>Instances per game<input
<Field class="instance-count" label="Instances per game">
<Input
type="number"
min="1"
max="1000"
@@ -756,7 +760,7 @@
: event.currentTarget.valueAsNumber)}
/><span class="muted"
>The same number applies to every game in this beta.</span
></label
></Field
>
<p class="muted">
{applicableGames
@@ -812,14 +816,14 @@
</fieldset>
{#if selectionMode === 'manual' && candidateServers.length}
<div class="filters">
<label
>Find servers<input
<Field label="Find servers">
<Input
type="search"
bind:value={search}
placeholder="Name, map, lobby…"
/></label
><label
>Game<select bind:value={serverGame}
/></Field
><Field label="Game">
<Select bind:value={serverGame}
><option value="">All games</option
>{#each [...new Set(fleetServers
.map((s) => s.game)
@@ -829,16 +833,16 @@
: game === 't6'
? 'Black Ops II'
: game}</option
>{/each}</select
></label
><label
>Availability<select bind:value={availability}
>{/each}</Select
></Field
><Field label="Availability">
<Select bind:value={availability}
><option value="">All servers</option><option value="empty"
>Confirmed empty</option
><option value="occupied">Occupied</option><option
value="unknown">Unknown / stale / offline</option
></select
></label
></Select
></Field
>
</div>
<div class="actions">
@@ -999,8 +1003,7 @@
</div>
<style>
.beta-form input:not([type='checkbox']):not([type='radio']),
.beta-form select {
.beta-form input:not([type='checkbox']):not([type='radio']) {
border-color: var(--border-control);
}
.page,
@@ -1081,8 +1084,7 @@
color: var(--text-secondary);
min-width: 0;
}
input:not([type='checkbox']):not([type='radio']),
select {
input:not([type='checkbox']):not([type='radio']) {
width: 100%;
min-height: 40px;
padding: 9px 12px;
@@ -1108,7 +1110,7 @@
display: grid;
gap: 16px;
}
.instance-count {
.automatic-selection :global(.instance-count) {
max-width: 400px;
}
.advanced-selection {
@@ -1148,7 +1150,8 @@
.filters {
align-items: end;
}
.filters label {
.filters :global(.field) {
flex: 1 1 180px;
}
.draft-status,
@@ -1217,7 +1220,8 @@
.pick-row {
grid-template-columns: minmax(0, 1fr) auto;
}
.pick-row label:first-child {
.pick-row > :global(.field:first-child) {
grid-column: 1/-1;
}
.steps {
+19 -32
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import Select from '$ui/components/primitives/Select.svelte';
import Input from '$ui/components/primitives/Input.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import RelativeTime from '$ui/components/RelativeTime.svelte';
import { contextHref } from '$lib/navigation-context.js';
import { timeFormat } from '$ui/stores/time-zone.js';
@@ -102,31 +105,31 @@
<div class="page">
<SectionHeader title="Campaigns" />
<div class="toolbar">
<label class="search"
>Search<input
<Field class="search" label="Search">
<Input
type="search"
bind:value={query}
oninput={() => saveContext(true)}
placeholder="Script, campaign, or server"
maxlength="100"
/></label
maxlength={100}
/></Field
>
<label
>Status<select bind:value={status} onchange={() => saveContext(true)}>
<Field label="Status">
<Select bind:value={status} onchange={() => saveContext(true)}>
<option value="">All statuses</option><option value="progress">In progress</option
><option value="attention">Needs attention</option><option value="complete"
>Complete</option
><option value="rolled-back">Rolled back</option><option value="inactive"
>Inactive</option
>
</select></label
</Select></Field
>
<label
>Type<select bind:value={action} onchange={() => saveContext(true)}>
<Field label="Type">
<Select bind:value={action} onchange={() => saveContext(true)}>
<option value="">All types</option><option value="promote">Promote</option><option
value="rollback">Rollback</option
><option value="drift">Maintenance repair</option>
</select></label
</Select></Field
>
{#if filtered}<Button variant="ghost" onclick={clearFilters}>Clear filters</Button>{/if}
</div>
@@ -254,29 +257,12 @@
flex-wrap: wrap;
gap: 12px;
}
label {
display: grid;
gap: 6px;
font-size: var(--text-xs);
color: var(--text-secondary);
}
.search {
.toolbar :global(.search) {
flex: 1;
min-width: 220px;
}
input,
select {
padding: 10px 12px;
min-height: 40px;
background: var(--surface-sunken);
color: var(--text-primary);
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
font: inherit;
font-size: var(--text-sm);
}
input:focus-visible,
select:focus-visible,
.timestamp:focus-visible {
outline: 2px solid var(--accent-primary);
outline-offset: 2px;
@@ -438,10 +424,11 @@
td {
padding: 12px 8px;
}
.search {
.toolbar :global(.search) {
flex-basis: 100%;
}
.toolbar > label:not(.search) {
.toolbar > :global(.field:not(.search)) {
flex: 1;
}
}
+8 -8
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import Select from '$ui/components/primitives/Select.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import RelativeTime from '$ui/components/RelativeTime.svelte';
import { gameTitle } from '$ui/server-labels.js';
import BackLink from '$ui/components/BackLink.svelte';
@@ -716,15 +718,15 @@
downloaded automatically.
</p>
<div class="download">
<label
>Host<select bind:value={targets[item.id]}
<Field label="Host">
<Select bind:value={targets[item.id]}
><option value="">Select host</option>{#each onlineHosts as host}<option
value={host.id}
>{host.name} · {formatGameBytes(
Number(host.inventory.storageAvailable ?? 0)
)} free</option
>{/each}</select
></label
>{/each}</Select
></Field
><Button
size="sm"
disabled={busy || !targets[item.id]}
@@ -783,8 +785,7 @@
gap: 6px;
font-size: var(--text-sm);
}
input,
select {
input {
width: 100%;
min-width: 0;
box-sizing: border-box;
@@ -794,8 +795,7 @@
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
}
input:disabled,
select:disabled {
input:disabled {
opacity: 0.7;
}
input[type='file'] {
+28 -32
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import Field from '$ui/components/primitives/Field.svelte';
import Select from '$ui/components/primitives/Select.svelte';
import Input from '$ui/components/primitives/Input.svelte';
import { onMount, untrack } from 'svelte';
import { page } from '$app/stores';
import { pollVisible } from '$ui/poll.js';
@@ -109,21 +112,26 @@
>{/if}
</div>
{#if filtersVisible}<div class="filters">
<input
type="search"
aria-label="Search hosts"
placeholder="Search hosts…"
bind:value={search}
/>
<select aria-label="Filter hosts by status" bind:value={status}
><option value="">All statuses</option><option value="attention"
>Needs attention</option
><option value="offline">Offline</option><option value="preparing"
>Preparing</option
><option value="draining">Draining</option><option value="disabled"
>Disabled</option
><option value="deleting">Deleting</option><option value="ready">Ready</option
></select
<Field label="Search hosts" hiddenLabel class="search"
><Input
type="search"
aria-label="Search hosts"
placeholder="Search hosts…"
bind:value={search}
/></Field
>
<Field label="Filter hosts by status" hiddenLabel
><Select aria-label="Filter hosts by status" bind:value={status}
><option value="">All statuses</option><option value="attention"
>Needs attention</option
><option value="offline">Offline</option><option value="preparing"
>Preparing</option
><option value="draining">Draining</option><option value="disabled"
>Disabled</option
><option value="deleting">Deleting</option><option value="ready"
>Ready</option
></Select
></Field
>
{#if search || status}<Button
variant="ghost"
@@ -170,27 +178,15 @@
font: inherit;
padding: 4px;
}
input,
select {
box-sizing: border-box;
padding: 10px 12px;
color: var(--text-primary);
background: var(--surface-sunken);
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
font: var(--text-sm) var(--font-body);
}
input {
flex: 1 1 240px;
min-width: 0;
max-width: 440px;
}
input:focus-visible,
select:focus-visible,
button:focus-visible {
outline: 2px solid var(--accent-primary);
outline-offset: 3px;
}
.filters > :global(.search) {
flex: 1 1 240px;
max-width: 440px;
}
.empty {
padding: 24px;
text-align: center;
+21 -42
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import Input from '$ui/components/primitives/Input.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import { timeFormat } from '$ui/stores/time-zone.js';
import RelativeTime from '$ui/components/RelativeTime.svelte';
import Breadcrumbs from '$ui/components/primitives/Breadcrumbs.svelte';
@@ -468,11 +470,8 @@
}}
>
<fieldset disabled={busy || cleaning}>
<label
>Plutainer image<input
required
bind:value={runtimeImage}
/></label
<Field label="Plutainer image">
<Input required bind:value={runtimeImage} /></Field
><Button type="submit" disabled={busy || !host.connected}
>Pull runtime image</Button
>
@@ -509,50 +508,43 @@
<h2>Host configuration</h2>
{#if canManage}<form onsubmit={save}>
<fieldset disabled={busy || cleaning}>
<label
>Friendly name<input
required
maxlength="200"
bind:value={name}
/></label
<Field label="Friendly name">
<Input required maxlength={200} bind:value={name} /></Field
>
<div class="fields">
<label
>First game port<input
<Field label="First game port">
<Input
type="number"
min="1"
max="65535"
required
bind:value={portMin}
/></label
><label
>Last game port<input
/></Field
><Field label="Last game port">
<Input
type="number"
min={portMin}
max="65535"
required
bind:value={portMax}
/></label
/></Field
>
</div>
<label
>Local bind address<input
required
bind:value={bindAddress}
/></label
><label
>Public / NAT address<input
<Field label="Local bind address">
<Input required bind:value={bindAddress} /></Field
><Field label="Public / NAT address">
<Input
bind:value={publicAddress}
placeholder="Optional public hostname or address"
/></label
><label
>Minimum free game storage (GiB)<input
/></Field
><Field label="Minimum free game storage (GiB)">
<Input
type="number"
min="0"
step="0.1"
required
bind:value={minimumFreeGB}
/></label
/></Field
><Button type="submit" disabled={busy}>Save settings</Button>
</fieldset>
</form>{:else}<dl>
@@ -745,20 +737,7 @@
padding: 0;
margin: 0;
}
label {
display: grid;
gap: 8px;
font-size: var(--text-sm);
}
input {
width: 100%;
min-width: 0;
padding: 10px;
color: var(--text-primary);
background: var(--surface-sunken);
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
}
.fields > * {
flex: 1;
min-width: 120px;
+30 -33
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import Field from '$ui/components/primitives/Field.svelte';
import Input from '$ui/components/primitives/Input.svelte';
import Select from '$ui/components/primitives/Select.svelte';
import RelativeTime from '$ui/components/RelativeTime.svelte';
let { data: initial }: { data: import('./$types').PageData } = $props();
import { onMount, untrack } from 'svelte';
@@ -262,9 +265,8 @@
{#snippet filterFields(suffix: string)}
<div class="filters">
<div class="field">
<label for={`leaderboard-game${suffix}`}>Game</label>
<select
<Field label="Game" forId={`leaderboard-game${suffix}`}>
<Select
id={`leaderboard-game${suffix}`}
value={game}
onchange={(event) =>
@@ -273,11 +275,10 @@
<option value="">All games</option>{#each games as item (item.key)}<option
value={item.key}>{item.label}</option
>{/each}
</select>
</div>
<div class="field">
<label for={`leaderboard-map${suffix}`}>Map</label>
<select
</Select>
</Field>
<Field label="Map" forId={`leaderboard-map${suffix}`}>
<Select
id={`leaderboard-map${suffix}`}
value={map}
onchange={(event) => updateFilters({ map: event.currentTarget.value })}
@@ -291,11 +292,10 @@
? ` · ${item.game === 't5' ? 'BO1' : 'BO2'}`
: ''}</option
>{/each}
</select>
</div>
<div class="field">
<label for={`leaderboard-players${suffix}`}>Players</label>
<select
</Select>
</Field>
<Field label="Players" forId={`leaderboard-players${suffix}`}>
<Select
id={`leaderboard-players${suffix}`}
value={players}
onchange={(event) => updateFilters({ players: event.currentTarget.value })}
@@ -304,14 +304,13 @@
<option value="">All squad sizes</option>{#each squadSizes as size}<option
value={size.key}>{size.label}</option
>{/each}
</select><span id={`players-help${suffix}`} class="sr-only"
</Select><span id={`players-help${suffix}`} class="sr-only"
>Players recorded at the start of each match. Matches with unknown starting
counts appear only under All squad sizes.</span
>
</div>
<div class="field">
<label for={`leaderboard-metric${suffix}`}>Rank by</label>
<select
</Field>
<Field label="Rank by" forId={`leaderboard-metric${suffix}`}>
<Select
id={`leaderboard-metric${suffix}`}
value={metric}
onchange={(event) => updateFilters({ metric: event.currentTarget.value })}
@@ -319,8 +318,8 @@
{#each leaderboardMetrics as item (item.key)}<option value={item.key}
>{item.label}</option
>{/each}
</select>
</div>
</Select>
</Field>
<form
class="search"
onsubmit={(event) => {
@@ -330,15 +329,16 @@
}}
role="search"
>
<label for={`leaderboard-search${suffix}`}>Find a player</label>
<input
id={`leaderboard-search${suffix}`}
type="search"
bind:value={search}
maxlength="100"
placeholder="Name or GUID"
aria-describedby={`search-help${suffix}`}
/><span id={`search-help${suffix}`} class="sr-only"
<Field label="Find a player" forId={`leaderboard-search${suffix}`}>
<Input
id={`leaderboard-search${suffix}`}
type="search"
bind:value={search}
maxlength={100}
placeholder="Name or GUID"
aria-describedby={`search-help${suffix}`}
/></Field
><span id={`search-help${suffix}`} class="sr-only"
>Press Enter to search. Player ranks are preserved.</span
>
</form>
@@ -700,7 +700,6 @@
.search {
grid-column: 1 / -1;
}
.field,
.search {
display: flex;
flex-direction: column;
@@ -712,12 +711,10 @@
font-size: var(--text-xs);
}
input,
select,
button {
font: inherit;
}
input,
select {
input {
min-height: 42px;
width: 100%;
padding: 9px 12px;
@@ -1,4 +1,6 @@
<script lang="ts">
import Select from '$ui/components/primitives/Select.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import RelativeTime from '$ui/components/RelativeTime.svelte';
import { playerCompletionLabel } from '$ui/player-match.js';
import { onMount } from 'svelte';
@@ -389,17 +391,17 @@
description="Choose the leaderboard ranking and match history. Lifetime records stay unchanged."
>
<div class="scope-controls" aria-busy={filtering}>
<label
>Game<select
<Field label="Game">
<Select
value={scope.game}
onchange={(event) => void updateScope({ game: event.currentTarget.value, map: '' })}
><option value="">All games</option><option value="t5">Black Ops I</option><option
value="t6">Black Ops II</option
></select
></label
></Select
></Field
>
<label
>Map<select
<Field label="Map">
<Select
value={scope.map}
onchange={(event) => void updateScope({ map: event.currentTarget.value })}
><option value="">All maps</option
@@ -408,26 +410,26 @@
>{/if}{#each filteredMaps as map (`${map.game}:${map.key}`)}<option
value={map.key}
>{map.name}{!scope.game ? ` · ${gameName(map.game)}` : ''}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
<label
>Players<select
<Field label="Players">
<Select
value={scope.players ?? ''}
onchange={(event) => void updateScope({ players: event.currentTarget.value })}
><option value="">All squad sizes</option><option value="1">Solo</option><option
value="2">Duo</option
><option value="3">Trio</option><option value="4">Four players</option></select
></label
><option value="3">Trio</option><option value="4">Four players</option></Select
></Field
>
<label
>Rank by<select
<Field label="Rank by">
<Select
value={scope.metric}
onchange={(event) => void updateScope({ metric: event.currentTarget.value })}
>{#each leaderboardMetrics as metric}<option value={metric.key}
>{metric.label}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
</div>
{#snippet actions()}<Button onclick={() => (filterOpen = false)}>Done</Button>{/snippet}
@@ -608,15 +610,7 @@
display: grid;
gap: var(--space-4);
}
.scope-controls label {
display: grid;
gap: var(--space-2);
color: var(--text-secondary);
font-size: var(--text-sm);
}
.scope-controls select {
width: 100%;
}
.mobile-records {
display: none;
}
+11 -22
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import Input from '$ui/components/primitives/Input.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import { page } from '$app/stores';
import Button from '$ui/components/primitives/Button.svelte';
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
@@ -51,28 +53,28 @@
{#if error}<p class="error" role="alert">{error}</p>{/if}
<form onsubmit={login} aria-busy={busy}>
<fieldset disabled={busy}>
<label for="username"
>Username<input
<Field forId="username" label="Username">
<Input
id="username"
name="username"
autocomplete="username"
autocapitalize="none"
spellcheck="false"
required
maxlength="64"
maxlength={64}
bind:value={username}
/></label
/></Field
>
<label for="password"
>Password<input
<Field forId="password" label="Password">
<Input
id="password"
name="password"
type="password"
autocomplete="current-password"
required
maxlength="128"
maxlength={128}
bind:value={password}
/></label
/></Field
>
<Button type="submit" variant="primary" loading={busy} disabled={busy}>Sign in</Button>
</fieldset>
@@ -102,20 +104,7 @@
padding: 0;
min-width: 0;
}
label {
display: grid;
gap: 8px;
font-size: var(--text-sm);
}
input {
width: 100%;
padding: 12px;
background: var(--surface-sunken);
color: var(--text-primary);
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
font: inherit;
}
.help,
.setup-link {
color: var(--text-secondary);
+18 -19
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import Select from '$ui/components/primitives/Select.svelte';
import Input from '$ui/components/primitives/Input.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import RelativeTime from '$ui/components/RelativeTime.svelte';
import ActivityLog from '$ui/components/ActivityLog.svelte';
import BackLink from '$ui/components/BackLink.svelte';
@@ -381,12 +384,9 @@
<label class="toggle"
><input type="checkbox" bind:checked={scheduleEnabled} /> Daily fleet restart</label
>
<label
>Queue time ({$timeZone})<input
type="time"
bind:value={scheduleTime}
required
/></label
<Field
>{#snippet label()}Queue time ({$timeZone}){/snippet}
<Input type="time" bind:value={scheduleTime} required /></Field
>
<Button type="submit" disabled={busy}>Save schedule</Button>
</form>
@@ -490,30 +490,30 @@
{/if}
{/if}
<div class="filters">
<label
>Capability<select bind:value={kind} onchange={() => saveContext()}
<Field label="Capability">
<Select bind:value={kind} onchange={() => saveContext()}
><option value="all">All capabilities</option><option value="health"
>Health recovery</option
><option value="drift">Configuration drift</option><option
value="updates">Plutonium updates</option
><option value="schedule">Scheduled restarts</option></select
></label
><option value="schedule">Scheduled restarts</option></Select
></Field
>
<label
>Server<input
<Field label="Server">
<Input
bind:value={server}
oninput={() => saveContext()}
placeholder="Filter servers"
/></label
/></Field
>
{#if view === 'activity'}<label
>Status<select bind:value={status} onchange={() => saveContext()}
{#if view === 'activity'}<Field label="Status">
<Select bind:value={status} onchange={() => saveContext()}
><option value="all">All activity</option><option value="active"
>Active</option
><option value="attention">Needs attention</option><option
value="done">Finished</option
></select
></label
></Select
></Field
>{/if}
</div>
{#if view === 'overview' && ['all', 'updates'].includes(kind)}
@@ -808,8 +808,7 @@
display: flex;
align-items: center;
}
input:not([type='checkbox']),
select {
input:not([type='checkbox']) {
color: inherit;
background: var(--surface-sunken);
padding: 9px;
+29 -23
View File
@@ -303,31 +303,37 @@
{flash}<Button size="sm" onclick={load}>Retry</Button>
</div>{/if}
{#if loading || plugins.length || flash}<div class="library-toolbar">
<input
class="search"
type="search"
aria-label="Search plugins"
bind:value={search}
placeholder="Search names or files…"
/>
<select aria-label="Filter plugins by game" bind:value={game}
><option value="">All games</option><option value="t5">Black Ops I</option><option
value="t6">Black Ops II</option
></select
<Field label="Search plugins" hiddenLabel class="search"
><Input
type="search"
aria-label="Search plugins"
bind:value={search}
placeholder="Search names or files…"
/></Field
>
<select aria-label="Filter plugins by status" bind:value={status}
><option value="">All statuses</option><option value="untested"
>Needs testing</option
><option value="testing">Beta active</option><option value="test-failed"
>Test failed</option
><option value="tested">Test passed</option><option value="promoted"
>Promoted</option
><option value="empty">No versions</option></select
<Field label="Filter plugins by game" hiddenLabel
><Select aria-label="Filter plugins by game" bind:value={game}
><option value="">All games</option><option value="t5">Black Ops I</option
><option value="t6">Black Ops II</option></Select
></Field
>
<select aria-label="Sort plugins" bind:value={sort}
><option value="name">Name A–Z</option><option value="recent"
>Recently uploaded</option
></select
<Field label="Filter plugins by status" hiddenLabel
><Select aria-label="Filter plugins by status" bind:value={status}
><option value="">All statuses</option><option value="untested"
>Needs testing</option
><option value="testing">Beta active</option><option value="test-failed"
>Test failed</option
><option value="tested">Test passed</option><option value="promoted"
>Promoted</option
><option value="empty">No versions</option></Select
></Field
>
<Field label="Sort plugins" hiddenLabel
><Select aria-label="Sort plugins" bind:value={sort}
><option value="name">Name A–Z</option><option value="recent"
>Recently uploaded</option
></Select
></Field
>
</div>{/if}
{#if game}<p class="filter-hint">
+23 -35
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import Select from '$ui/components/primitives/Select.svelte';
import Input from '$ui/components/primitives/Input.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import BackLink from '$ui/components/BackLink.svelte';
import { timeFormat } from '$ui/stores/time-zone.js';
import { onMount, untrack } from 'svelte';
@@ -302,14 +305,14 @@
panelId="deployment-panel"
/>
</div>
<label class="search"
>Server<input
<Field class="search" label="Server">
<Input
type="search"
bind:value={search}
oninput={() => saveContext(true)}
placeholder="Search server or lobby"
maxlength="100"
/></label
maxlength={100}
/></Field
>
<div class="desktop-filters">
{@render filterFields()}
@@ -322,15 +325,15 @@
</div>
</div>
{#snippet filterFields()}
<label
>Operation<select bind:value={action} onchange={() => saveContext(true)}
<Field label="Operation">
<Select bind:value={action} onchange={() => saveContext(true)}
><option value="all">All operations</option
>{#each provisioningActions as value}<option {value}>{actionLabels[value]}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
<label
>Status<select bind:value={historyState} onchange={() => saveContext(true)}
<Field label="Status">
<Select bind:value={historyState} onchange={() => saveContext(true)}
><option value="all">All statuses</option><option value="pending"
>Waiting / queued</option
><option value="running">In progress</option><option value="awaiting-health"
@@ -341,16 +344,16 @@
>Interrupted</option
><option value="warning">Warnings</option><option value="cancelled"
>Cancelled</option
></select
></label
></Select
></Field
>
<label
>Source<select bind:value={source} onchange={() => saveContext(true)}
<Field label="Source">
<Select bind:value={source} onchange={() => saveContext(true)}
><option value="all">All sources</option
>{#each provisioningListSources as value}<option {value}
>{provisioningSourceLabels[value]}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
{/snippet}
<Dialog
@@ -636,27 +639,12 @@
display: grid;
gap: 16px;
}
label {
display: grid;
gap: 6px;
font-size: var(--text-xs);
color: var(--text-secondary);
}
.search {
.toolbar :global(.search) {
flex: 1;
min-width: 220px;
}
input,
select {
padding: 10px 12px;
min-height: 40px;
background: var(--surface-sunken);
color: var(--text-primary);
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
font: inherit;
font-size: var(--text-sm);
}
.queue-heading,
.queue-tools,
.pagination {
@@ -789,7 +777,7 @@
table {
min-width: 820px;
}
.search {
.toolbar :global(.search) {
flex-basis: 100%;
}
th,
+22 -22
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import Select from '$ui/components/primitives/Select.svelte';
import Input from '$ui/components/primitives/Input.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import { page } from '$app/stores';
import { contextHref } from '$lib/navigation-context.js';
import { onMount } from 'svelte';
@@ -213,41 +216,39 @@
</SectionHeader>
<section class="library" aria-label="Script library">
{#if loading || scripts.length || error}<div class="library-toolbar">
<label class="search"
><span class="sr-only">Search scripts</span><input
<Field class="search" hiddenLabel label="Search scripts">
<Input
type="search"
bind:value={search}
placeholder="Search names, files, or maps…"
/></label
/></Field
>
<label
><span class="sr-only">Filter by game</span><select
bind:value={game}
onchange={() => (map = '')}
<Field hiddenLabel label="Filter by game">
<Select bind:value={game} onchange={() => (map = '')}
><option value="">All games</option><option value="t5">Black Ops I</option
><option value="t6">Black Ops II</option></select
></label
><option value="t6">Black Ops II</option></Select
></Field
>
<label
><span class="sr-only">Filter by map</span><select bind:value={map}
<Field hiddenLabel label="Filter by map">
<Select bind:value={map}
><option value="">All maps</option>{#each maps as m}<option value={m}
>{mapLabel(m)}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
<label
><span class="sr-only">Filter by status</span><select bind:value={status}
<Field hiddenLabel label="Filter by status">
<Select bind:value={status}
>{#each tabs as tab}<option value={tab.value}
>{tab.value === 'all' ? 'All statuses' : tab.label}</option
>{/each}<option value="empty">No versions</option></select
></label
>{/each}<option value="empty">No versions</option></Select
></Field
>
<label
><span class="sr-only">Sort scripts</span><select bind:value={sort}
<Field hiddenLabel label="Sort scripts">
<Select bind:value={sort}
><option value="attention">Needs attention first</option><option
value="recent">Recently uploaded</option
><option value="name">Name A–Z</option></select
></label
><option value="name">Name A–Z</option></Select
></Field
>
</div>
<div class="list-heading">
@@ -408,7 +409,6 @@
}
button:focus-visible,
input:focus-visible,
select:focus-visible,
a:focus-visible {
outline: 2px solid var(--accent-primary);
outline-offset: 3px;
+33 -38
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import Select from '$ui/components/primitives/Select.svelte';
import RelativeTime from '$ui/components/RelativeTime.svelte';
import { onMount } from 'svelte';
import { page } from '$app/stores';
@@ -173,27 +174,31 @@
{/each}
</div>{/if}
<div class="ui-toolbar">
<input
type="search"
aria-label="Search keys"
bind:value={search}
oninput={(event) => {
search = event.currentTarget.value;
limit = 25;
saveSearch();
}}
placeholder="Search labels or assigned servers…"
/>
<select
aria-label="Filter keys by game"
bind:value={gameFilter}
onchange={() => {
limit = 25;
saveSearch();
}}
><option value="">All games</option><option value="t5">Black Ops I</option><option
value="t6">Black Ops II</option
></select
<Field label="Search keys" hiddenLabel class="search"
><Input
type="search"
aria-label="Search keys"
bind:value={search}
oninput={(event) => {
search = event.currentTarget.value;
limit = 25;
saveSearch();
}}
placeholder="Search labels or assigned servers…"
/></Field
>
<Field label="Filter keys by game" hiddenLabel
><Select
aria-label="Filter keys by game"
bind:value={gameFilter}
onchange={() => {
limit = 25;
saveSearch();
}}
><option value="">All games</option><option value="t5">Black Ops I</option><option
value="t6">Black Ops II</option
></Select
></Field
>
</div>
{#if error && !open}<div role="alert" class="ui-error">
@@ -250,11 +255,11 @@
<Dialog bind:open title="Add key" {busy}>
{#if error}<p role="alert" class="error">{error}</p>{/if}
<form onsubmit={save}>
<label
>Game<select bind:value={game} disabled={busy}
<Field label="Game">
<Select bind:value={game} disabled={busy}
><option value="t5">Black Ops I</option><option value="t6">Black Ops II</option
></select
></label
></Select
></Field
>
<Field
label="Label (optional)"
@@ -319,6 +324,7 @@
display: block;
margin-top: 6px;
}
.assignment {
margin-top: 8px;
}
@@ -331,22 +337,11 @@
p {
color: var(--text-secondary);
}
form,
label {
form {
display: grid;
gap: var(--space-4);
}
label {
gap: 8px;
font-size: var(--text-base);
}
select {
padding: 10px;
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
background: var(--surface-sunken);
color: inherit;
}
.actions {
display: flex;
gap: var(--space-2);
+38 -37
View File
@@ -1,4 +1,7 @@
<script lang="ts">
import Select from '$ui/components/primitives/Select.svelte';
import Input from '$ui/components/primitives/Input.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import RelativeTime from '$ui/components/RelativeTime.svelte';
import { onMount } from 'svelte';
import { page } from '$app/stores';
@@ -361,58 +364,54 @@
<form onsubmit={create} aria-busy={busy}>
<fieldset disabled={busy}>
{#if formError}<p class="error" role="alert">{formError}</p>{/if}
<label for="new-username"
>Username<input
<Field forId="new-username" label="Username">
<Input
id="new-username"
name="username"
required
minlength="3"
maxlength="64"
minlength={3}
maxlength={64}
pattern={'[a-zA-Z0-9][a-zA-Z0-9_.\\-]{2,63}'}
autocomplete="off"
autocapitalize="none"
spellcheck="false"
bind:value={username}
aria-describedby="new-username-help"
/></label
/></Field
>
<p class="hint" id="new-username-help">
3–64 characters: letters, numbers, periods, underscores, and hyphens. Start with a
letter or number.
</p>
<label for="new-name"
>Display name<input
<Field forId="new-name" label="Display name">
<Input
id="new-name"
name="displayName"
required
maxlength="100"
maxlength={100}
autocomplete="off"
bind:value={displayName}
/></label
/></Field
>
<label for="new-role"
>Role<select
id="new-role"
name="role"
bind:value={role}
aria-describedby="new-role-help"
<Field forId="new-role" label="Role">
<Select id="new-role" name="role" bind:value={role} aria-describedby="new-role-help"
>{#each roles as item}<option value={item.value}>{item.label}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
<p class="hint" id="new-role-help">{roleDescription}</p>
<label for="temporary-password"
>Temporary password<input
<Field forId="temporary-password" label="Temporary password">
<Input
id="temporary-password"
name="password"
type={showPassword ? 'text' : 'password'}
required
minlength="8"
maxlength="128"
minlength={8}
maxlength={128}
autocomplete="new-password"
bind:value={password}
aria-describedby="temporary-help"
/></label
/></Field
>
<p class="hint" id="temporary-help">
8–128 characters. Save it before adding the user; it will not be displayed again.
@@ -445,25 +444,25 @@
<form onsubmit={save} aria-busy={busy}>
<fieldset disabled={busy}>
{#if formError}<p class="error" role="alert">{formError}</p>{/if}
<label for="edit-name"
>Display name<input
<Field forId="edit-name" label="Display name">
<Input
id="edit-name"
name="displayName"
required
maxlength="100"
maxlength={100}
autocomplete="off"
bind:value={displayName}
/></label
/></Field
>
<label for="edit-role"
>Role<select
<Field forId="edit-role" label="Role">
<Select
id="edit-role"
name="role"
bind:value={role}
aria-describedby="edit-role-help"
>{#each roles as item}<option value={item.value}>{item.label}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
<p class="hint" id="edit-role-help">{roleDescription}</p>
<label class="checkbox"
@@ -476,17 +475,20 @@
Use <a href="/account">My account</a> to change your own password. Changes to your
access can sign you out.
</p>
{:else}<label for="reset-password"
>New temporary password <span class="optional">(optional)</span><input
{:else}<Field forId="reset-password"
>{#snippet label()}New temporary password <span class="optional"
>(optional)</span
>{/snippet}
<Input
id="reset-password"
name="password"
type={showPassword ? 'text' : 'password'}
minlength="8"
maxlength="128"
minlength={8}
maxlength={128}
autocomplete="new-password"
bind:value={password}
aria-describedby="reset-help"
/></label
/></Field
>
<p class="hint" id="reset-help">
Leave blank to keep the current password. A reset requires the user to choose a
@@ -663,8 +665,7 @@
gap: 8px;
font-size: var(--text-sm);
}
input:not([type='checkbox']),
select {
input:not([type='checkbox']) {
width: 100%;
padding: 11px;
color: var(--text-primary);
+26 -37
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import Input from '$ui/components/primitives/Input.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import Button from '$ui/components/primitives/Button.svelte';
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
let token = $state('');
@@ -54,78 +56,78 @@
{#if error}<p class="error" role="alert">{error}</p>{/if}
<form onsubmit={setup} aria-busy={busy}>
<fieldset disabled={busy}>
<label for="setup-token"
>Setup token<input
<Field forId="setup-token" label="Setup token">
<Input
id="setup-token"
name="token"
type="password"
required
minlength="32"
minlength={32}
autocomplete="off"
spellcheck="false"
bind:value={token}
aria-describedby="token-help"
/></label
/></Field
>
<p id="token-help" class="hint">
Provided through the controller’s <code>EC_SETUP_TOKEN</code> setting.
</p>
<label for="setup-username"
>Username<input
<Field forId="setup-username" label="Username">
<Input
id="setup-username"
name="username"
required
minlength="3"
maxlength="64"
minlength={3}
maxlength={64}
pattern={'[a-zA-Z0-9][a-zA-Z0-9_.\\-]{2,63}'}
autocomplete="username"
autocapitalize="none"
spellcheck="false"
bind:value={username}
aria-describedby="username-help"
/></label
/></Field
>
<p id="username-help" class="hint">
3–64 characters. Start with a letter or number; periods, underscores, and hyphens
are allowed.
</p>
<label for="setup-name"
>Display name<input
<Field forId="setup-name" label="Display name">
<Input
id="setup-name"
name="displayName"
required
maxlength="100"
maxlength={100}
autocomplete="name"
bind:value={displayName}
/></label
/></Field
>
<label for="setup-password"
>Password<input
<Field forId="setup-password" label="Password">
<Input
id="setup-password"
name="password"
type="password"
required
minlength="8"
maxlength="128"
minlength={8}
maxlength={128}
autocomplete="new-password"
bind:value={password}
aria-describedby="password-help"
/></label
/></Field
>
<p id="password-help" class="hint">
Use 8–128 characters. A long, unique passphrase works well.
</p>
<label for="setup-confirm"
>Confirm password<input
<Field forId="setup-confirm" label="Confirm password">
<Input
id="setup-confirm"
name="confirmPassword"
type="password"
required
minlength="8"
maxlength="128"
minlength={8}
maxlength={128}
autocomplete="new-password"
bind:value={confirmPassword}
/></label
/></Field
>
<Button type="submit" variant="primary" disabled={busy} loading={busy}
>Create administrator</Button
@@ -153,20 +155,7 @@
padding: 0;
min-width: 0;
}
label {
display: grid;
gap: 8px;
font-size: var(--text-sm);
}
input {
width: 100%;
padding: 12px;
background: var(--surface-sunken);
color: var(--text-primary);
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
font: inherit;
}
.help,
.hint {
font-size: var(--text-sm);