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
+39 -61
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 { untrack } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
@@ -187,59 +190,59 @@
<h2 class="sr-only" id={headingId}>Activity history</h2>
{#snippet filterFields(suffix: string)}
<div class="filters">
<label for={`activity-type${suffix}`}
>Type<select
<Field forId={`activity-type${suffix}`} label="Type">
<Select
id={`activity-type${suffix}`}
value={filters.type ?? ''}
onchange={(event) => update({ type: event.currentTarget.value })}
><option value="">All types</option>{#each typeOptions as [value, label]}<option
{value}>{label}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
<label for={`activity-state${suffix}`}
>State<select
<Field forId={`activity-state${suffix}`} label="State">
<Select
id={`activity-state${suffix}`}
value={filters.state ?? ''}
onchange={(event) => update({ state: event.currentTarget.value })}
><option value="">All states</option
>{#each Object.entries(activityStates) as [value, label]}<option {value}
>{label}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
{#if source === undefined}<label for={`activity-source${suffix}`}
>Source<select
{#if source === undefined}<Field forId={`activity-source${suffix}`} label="Source">
<Select
id={`activity-source${suffix}`}
value={filters.source ?? ''}
onchange={(event) => update({ source: event.currentTarget.value })}
><option value="">All sources</option
>{#each sourceOptions as [value, label]}<option {value}>{label}</option
>{/each}</select
></label
>{/each}</Select
></Field
>{/if}
{#if server === undefined}<label for={`activity-server${suffix}`}
>Server<select
{#if server === undefined}<Field forId={`activity-server${suffix}`} label="Server">
<Select
id={`activity-server${suffix}`}
value={filters.server ?? ''}
onchange={(event) => update({ server: event.currentTarget.value })}
><option value="">All servers</option>{#each serverOptions as name}<option
value={name}>{serverTitle(name)}</option
>{/each}</select
></label
>{/each}</Select
></Field
>{/if}
<label for={`activity-host${suffix}`}
>Host<select
<Field forId={`activity-host${suffix}`} label="Host">
<Select
id={`activity-host${suffix}`}
value={filters.host ?? ''}
onchange={(event) => update({ host: event.currentTarget.value })}
><option value="">All hosts</option>{#each hostOptions as [value, label]}<option
{value}>{label}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
<label for={`activity-days${suffix}`}
>Date range<select
<Field forId={`activity-days${suffix}`} label="Date range">
<Select
id={`activity-days${suffix}`}
value={String(filters.days ?? 0)}
onchange={(event) => update({ days: event.currentTarget.value })}
@@ -249,8 +252,8 @@
><option value="90">Last 90 days</option
>{#if filters.days && ![1, 7, 30, 90].includes(filters.days)}<option
value={String(filters.days)}>Last {filters.days} days</option
>{/if}</select
></label
>{/if}</Select
></Field
>
<form
class="search"
@@ -260,14 +263,14 @@
submitSearch();
}}
>
<label for={`activity-search${suffix}`}
>Search activity<input
<Field forId={`activity-search${suffix}`} label="Search activity">
<Input
id={`activity-search${suffix}`}
type="search"
bind:value={search}
maxlength="200"
maxlength={200}
placeholder="Server, host, or activity…"
/></label
/></Field
><Button type="submit" size="sm">Search</Button>
</form>
</div>
@@ -377,15 +380,15 @@
? `${data.offset + 1}–${data.offset + data.rows.length} of ${data.total}`
: 'No activities on this page'}</span
>
<label
>Per page<select
<Field label="Per page">
<Select
aria-label="Activities per page"
value={data.limit}
onchange={(event) => update({ limit: event.currentTarget.value })}
>{#each [...new Set( [25, 50, 100, data.limit] )].sort((a, b) => a - b) as size}<option
value={size}>{size}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
{#if data.total > data.limit || data.offset > 0}<nav aria-label="Activity pages">
<Button
@@ -418,25 +421,7 @@
align-items: end;
gap: var(--space-3);
}
label {
display: grid;
gap: 6px;
color: var(--text-secondary);
font-size: var(--text-xs);
min-width: 0;
}
select,
input {
width: 100%;
min-height: 40px;
padding: 8px 10px;
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
background: var(--surface-sunken);
color: var(--text-primary);
font: inherit;
font-size: var(--text-sm);
}
.search {
grid-column: span 2;
display: flex;
@@ -444,7 +429,8 @@
gap: var(--space-2);
min-width: 0;
}
.search label {
.search :global(.field) {
flex: 1;
}
.context,
@@ -549,15 +535,7 @@
.pagination {
justify-content: space-between;
}
.pagination label {
display: flex;
align-items: center;
gap: var(--space-2);
}
.pagination select {
width: auto;
min-height: 36px;
}
.pagination nav {
display: flex;
gap: var(--space-2);
+13 -17
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 Dialog from './primitives/Dialog.svelte';
import Button from './primitives/Button.svelte';
@@ -89,34 +91,34 @@
</p>
<form onsubmit={add}>
<fieldset disabled={busy}>
<label
>Friendly name<input
<Field label="Friendly name">
<Input
required
maxlength="200"
maxlength={200}
bind:value={name}
placeholder="Game host 1"
/></label
/></Field
>
<details>
<summary>Advanced settings</summary>
<div class="ports">
<label
>First game port<input
<Field label="First game port">
<Input
type="number"
required
min="1"
max="65535"
bind:value={portMin}
/></label
/></Field
>
<label
>Last game port<input
<Field label="Last game port">
<Input
type="number"
required
min={portMin}
max="65535"
bind:value={portMax}
/></label
/></Field
>
</div>
</details>
@@ -150,12 +152,7 @@
.muted {
font-size: var(--text-xs);
}
label {
display: grid;
gap: 8px;
font-size: var(--text-sm);
}
input,
textarea {
box-sizing: border-box;
width: 100%;
@@ -186,7 +183,6 @@
flex-wrap: wrap;
gap: 12px;
}
input:focus-visible,
textarea:focus-visible,
summary:focus-visible {
outline: 2px solid var(--accent-primary);
+57 -69
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, tick } from 'svelte';
import {
builtInPlans,
@@ -192,9 +195,8 @@
{/each}
</div>
{#if savedPresets.length}
<label class="saved-picker"
>Or use a saved preset
<select
<Field class="saved-picker" label="Or use a saved preset">
<Select
value={savedPresets.some((p) => p.name === matchingPreset?.name)
? matchingPreset?.name
: ''}
@@ -203,8 +205,8 @@
<option value="" disabled>Choose a saved plan…</option>
{#each savedPresets as preset}<option value={preset.name}>{preset.name}</option
>{/each}
</select>
</label>
</Select>
</Field>
{/if}
{/if}
{#if message}<div class="feedback" role="status" aria-live="polite">{message}</div>{/if}
@@ -274,22 +276,21 @@
<summary>Edit requirement <span>{requirementLabels[r.kind]}</span></summary>
<div class="editor-fields">
<div class="fields">
<label
>Description<input
<Field label="Description">
<Input
id={`plan-${r.id}-label`}
aria-invalid={!!fieldError(`${r.id}-label`)}
aria-describedby={`error-${r.id}-label`}
bind:value={r.label}
maxlength="200"
maxlength={200}
required
/><span class="error" id={`error-${r.id}-label`}
>{fieldError(`${r.id}-label`) ?? ''}</span
></label
></Field
>
{#if r.kind !== 'manual'}
<label
>{targetLabel(r.kind)}
<input
<Field label={targetLabel(r.kind)}>
<Input
id={`plan-${r.id}-target`}
aria-invalid={!!fieldError(`${r.id}-target`)}
aria-describedby={`error-${r.id}-target`}
@@ -318,11 +319,11 @@
<span class="error" id={`error-${r.id}-target`}
>{fieldError(`${r.id}-target`) ?? ''}</span
>
</label>
</Field>
{/if}
{#if ['round', 'matches', 'populated-minutes', 'player-hours', 'sustained-population'].includes(r.kind)}
<label
>Minimum players<input
<Field label="Minimum players">
<Input
id={`plan-${r.id}-minPlayers`}
aria-invalid={!!fieldError(`${r.id}-minPlayers`)}
aria-describedby={`error-${r.id}-minPlayers`}
@@ -333,12 +334,12 @@
required
/><span class="error" id={`error-${r.id}-minPlayers`}
>{fieldError(`${r.id}-minPlayers`) ?? ''}</span
></label
></Field
>
{/if}
{#if r.kind === 'matches'}
<label
>Minimum round in the same game<input
<Field label="Minimum round in the same game">
<Input
id={`plan-${r.id}-minRound`}
aria-invalid={!!fieldError(`${r.id}-minRound`)}
aria-describedby={`error-${r.id}-minRound`}
@@ -349,12 +350,12 @@
required
/><span class="error" id={`error-${r.id}-minRound`}
>{fieldError(`${r.id}-minRound`) ?? ''}</span
></label
></Field
>
{/if}
{#if r.kind === 'matches'}
<label
>Minimum match duration (minutes)<input
<Field label="Minimum match duration (minutes)">
<Input
id={`plan-${r.id}-minMinutes`}
aria-invalid={!!fieldError(`${r.id}-minMinutes`)}
aria-describedby={`error-${r.id}-minMinutes`}
@@ -365,7 +366,7 @@
required
/><span class="error" id={`error-${r.id}-minMinutes`}
>{fieldError(`${r.id}-minMinutes`) ?? ''}</span
></label
></Field
>
{/if}
</div>
@@ -402,12 +403,12 @@
{/each}
</div>
<div class="add-row">
<label
>Add another check<select bind:value={addKind}
<Field label="Add another check">
<Select bind:value={addKind}
>{#each requirementKinds as kind}<option value={kind}
>{requirementLabels[kind]}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
<button
type="button"
@@ -434,8 +435,8 @@
Servers step.
</p>
{:else}
<label
>Qualifying servers<select bind:value={plan.scope}>
<Field label="Qualifying servers">
<Select bind:value={plan.scope}>
<option value="all">Every selected server</option><option
value="quorum">At least N selected servers</option
>
@@ -445,10 +446,10 @@
<option value="map-game"
>At least one per selected game/map combination</option
>
</select></label
</Select></Field
>
{#if plan.scope === 'quorum'}<label
>Required servers<input
{#if plan.scope === 'quorum'}<Field label="Required servers">
<Input
id="plan-quorum"
aria-invalid={!!fieldError('quorum')}
aria-describedby="error-quorum"
@@ -461,11 +462,11 @@
/><span class="error" id="error-quorum"
>{fieldError('quorum') ??
`${serverCount ?? 0} servers selected`}</span
></label
></Field
>{/if}
{/if}
<label
>Servers per wave (0 = all at once)<input
<Field label="Servers per wave (0 = all at once)">
<Input
id="plan-waveSize"
aria-invalid={!!fieldError('waveSize')}
aria-describedby="error-waveSize"
@@ -476,7 +477,7 @@
required
/><span class="error" id="error-waveSize"
>{fieldError('waveSize') ?? ''}</span
></label
></Field
>
</div>
<p>
@@ -496,8 +497,8 @@
>
<div class="policy-body">
<div class="fields">
<label
>Testing window (hours from opening)<input
<Field label="Testing window (hours from opening)">
<Input
id="plan-timeoutHours"
aria-invalid={!!fieldError('timeoutHours')}
aria-describedby="error-timeoutHours"
@@ -509,20 +510,20 @@
required
/><span class="error" id="error-timeoutHours"
>{fieldError('timeoutHours') ?? ''}</span
></label
></Field
>
<label
>On test failure<select bind:value={plan.failureAction}
<Field label="On test failure">
<Select bind:value={plan.failureAction}
><option value="fail">Mark failed and hold</option><option value="pause"
>Pause for investigation</option
><option value="rollback">Roll back automatically</option></select
></label
><option value="rollback">Roll back automatically</option></Select
></Field
>
<label
>Rollback activation<select bind:value={plan.rollbackRestart}
<Field label="Rollback activation">
<Select bind:value={plan.rollbackRestart}
><option value="when-empty">Restore and restart when empty</option
><option value="manual">Hold changes for manual resume</option></select
></label
><option value="manual">Hold changes for manual resume</option></Select
></Field
>
</div>
<p>
@@ -541,12 +542,12 @@
><span class="edit-hint">Save</span></summary
>
<div class="policy-body fields">
<label
>Preset name<input
<Field label="Preset name">
<Input
bind:value={presetName}
maxlength="80"
maxlength={80}
placeholder="My gameplay check"
/></label
/></Field
>
<button
type="button"
@@ -808,19 +809,15 @@
gap: 12px;
align-items: end;
}
.add-row label {
.builder .add-row > :global(.field) {
flex: 1 1 220px;
}
label {
display: grid;
gap: 7px;
.builder :global(.field) {
flex: 1 1 180px;
min-width: 0;
font-size: var(--text-xs);
color: var(--text-secondary);
}
input,
select,
button {
color: var(--text-primary);
background: var(--surface-sunken);
@@ -830,14 +827,7 @@
min-width: 0;
font: inherit;
}
input,
select {
width: 100%;
font-size: var(--text-sm);
min-height: 40px;
box-sizing: border-box;
border-color: var(--border-control);
}
button {
cursor: pointer;
}
@@ -846,9 +836,7 @@
cursor: default;
}
button:focus-visible,
summary:focus-visible,
input:focus-visible,
select:focus-visible {
summary:focus-visible {
outline: 2px solid var(--accent-primary-hover);
outline-offset: 3px;
}
+21 -13
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 { serverTitle } from '$ui/server-labels.js';
import RelativeTime from '$ui/components/RelativeTime.svelte';
import { timeFormat } from '$ui/stores/time-zone.js';
@@ -413,27 +416,27 @@
current progress uses only the current attempt.
</p>
<div class="filters">
<label
>Server<select bind:value={evidenceServer}
<Field label="Server">
<Select bind:value={evidenceServer}
><option value="">All servers</option
>{#each [...new Set(test.evidence.map((event) => event.server))] as server}<option
value={server}>{serverTitle(server)}</option
>{/each}</select
></label
><label
>Event type<select bind:value={evidenceKind}
>{/each}</Select
></Field
><Field label="Event type">
<Select bind:value={evidenceKind}
><option value="">All events</option><option value="gameplay"
>Gameplay reports</option
><option value="health">Health observations</option><option
value="manual-sign-off">Tester sign-offs</option
></select
></label
><label
>Search evidence<input
></Select
></Field
><Field label="Search evidence">
<Input
type="search"
bind:value={evidenceQuery}
placeholder="Round, map, tester, observation…"
/></label
/></Field
>
</div>
{#each filteredEvidence.slice(0, evidenceLimit) as event (event.id)}<article
@@ -683,14 +686,15 @@
.signoff label {
flex: 1 1 240px;
}
.signoff :global(.field) {
flex: 1 1 240px;
}
label {
display: grid;
gap: 6px;
font-size: var(--text-sm);
color: var(--text-secondary);
}
input,
select,
textarea {
min-height: 40px;
padding: 8px 10px;
@@ -714,6 +718,10 @@
flex: 1 1 180px;
min-width: 0;
}
.filters :global(.field) {
flex: 1 1 180px;
min-width: 0;
}
.evidence {
display: grid;
gap: 8px;
+28 -39
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 { isDialogBackdrop } from '$ui/dialog.js';
import Button from './primitives/Button.svelte';
import type { BlueprintSummary } from '$lib/deployment-plan.js';
@@ -184,21 +187,21 @@
}}
>
<div class="identity">
<label
>Collection name<input
<Field label="Collection name">
<Input
bind:value={name}
maxlength="80"
maxlength={80}
placeholder="Friday Zombies"
required
disabled={busy}
/></label
><label
>Description (optional)<input
/></Field
><Field label="Description (optional)">
<Input
bind:value={description}
maxlength="500"
maxlength={500}
placeholder="Our weekend lineup"
disabled={busy}
/></label
/></Field
>
</div>
<div class="lineup">
@@ -211,8 +214,8 @@
{#each rows as row, index (row.key)}
<div class="map-row">
<span class="number">{String(index + 1).padStart(2, '0')}</span>
<label
>Game<select
<Field label="Game">
<Select
aria-label={`Game ${index + 1}`}
bind:value={row.game}
disabled={busy}
@@ -222,11 +225,11 @@
?.slug ?? '')}
><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
aria-label={`Map ${index + 1}`}
aria-invalid={duplicate(row) || undefined}
aria-describedby={duplicate(row)
@@ -242,13 +245,13 @@
other.game === row.game &&
other.mapSlug === map.slug
)}>{map.friendly}</option
>{/each}</select
>{/each}</Select
>{#if duplicate(row)}<span class="error" id={`map-error-${row.key}`}
>Already selected. Choose another map.</span
>{/if}</label
>{/if}</Field
>
<label
>Target lobbies<input
<Field label="Target lobbies">
<Input
aria-label={`Lobbies ${index + 1}`}
aria-invalid={!(
Number.isInteger(row.count) &&
@@ -263,7 +266,7 @@
bind:value={row.count}
required
disabled={busy}
/><span id={`count-hint-${row.key}`}>1–15</span></label
/><span id={`count-hint-${row.key}`}>1–15</span></Field
>
<button
type="button"
@@ -371,24 +374,7 @@
grid-template-columns: 1fr 1fr;
gap: 16px;
}
label {
display: grid;
gap: 7px;
font-size: var(--text-xs);
min-width: 0;
}
input,
select {
width: 100%;
min-width: 0;
box-sizing: border-box;
padding: 11px;
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
background: var(--surface-sunken);
color: inherit;
font: inherit;
}
.lineup {
border-block: 1px solid var(--border-subtle);
padding: 20px 0;
@@ -457,8 +443,11 @@
display: none;
font-variant-numeric: tabular-nums;
}
.map-row label:first-of-type,
.map-row label:nth-of-type(2) {
.map-row > :global(.field:first-of-type) {
grid-column: 1 / -1;
}
.map-row > :global(.field:nth-of-type(2)) {
grid-column: 1 / -1;
}
.actions {
+11 -20
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 type { CapacitySamplePage } from '$lib/hosts.js';
import Button from './primitives/Button.svelte';
@@ -59,18 +61,18 @@
measurements from other hosts used by fleet allowances.
</p>
<div class="controls">
<label
>Source<select bind:value={scope} onchange={() => (page = 1)}
<Field label="Source">
<Select bind:value={scope} onchange={() => (page = 1)}
><option value="host">This host</option><option value="fleet"
>Entire fleet</option
></select
></label
></Select
></Field
>
<label
>Phase<select bind:value={phase} onchange={() => (page = 1)}
<Field label="Phase">
<Select bind:value={phase} onchange={() => (page = 1)}
><option value="all">All phases</option><option value="startup">Starting</option
><option value="running">Running</option></select
></label
><option value="running">Running</option></Select
></Field
>
<Button
size="sm"
@@ -182,18 +184,7 @@
nav {
align-items: center;
}
label {
display: grid;
gap: 6px;
font-size: var(--text-sm);
}
select {
padding: 8px;
color: var(--text-primary);
background: var(--surface-sunken);
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
}
.table-wrap {
max-width: 100%;
overflow: auto;
+21 -39
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import Field from '$ui/components/primitives/Field.svelte';
import Input from '$ui/components/primitives/Input.svelte';
import { tick } from 'svelte';
import { goto } from '$app/navigation';
import type { UserRole } from '$lib/auth.js';
@@ -154,23 +156,24 @@
description="Jump to a page, server, host, player, or match."
>
<div class="palette">
<label class="sr-only" for={`${id}-search`}>Search Electric Cherry</label>
<input
bind:this={input}
bind:value={query}
id={`${id}-search`}
type="search"
maxlength="100"
placeholder="Search Electric Cherry…"
role="combobox"
aria-autocomplete="list"
aria-expanded={open}
aria-controls={`${id}-results`}
aria-activedescendant={items.length ? `${id}-option-${activeIndex}` : undefined}
aria-describedby={`${id}-hint`}
autocomplete="off"
onkeydown={keydown}
/>
<Field label="Search Electric Cherry" forId={`${id}-search`} hiddenLabel>
<Input
bind:element={input}
bind:value={query}
id={`${id}-search`}
type="search"
maxlength={100}
placeholder="Search Electric Cherry…"
role="combobox"
aria-autocomplete="list"
aria-expanded={open}
aria-controls={`${id}-results`}
aria-activedescendant={items.length ? `${id}-option-${activeIndex}` : undefined}
aria-describedby={`${id}-hint`}
autocomplete="off"
onkeydown={keydown}
/></Field
>
<p class="hint" id={`${id}-hint`}>
Try a lobby, player name, or #match number. <span>↑ ↓ to choose · Enter to open</span>
</p>
@@ -228,17 +231,7 @@
gap: var(--space-3);
min-width: 0;
}
input {
width: 100%;
min-height: 44px;
padding: var(--space-3);
font: inherit;
font-size: var(--text-base);
color: var(--text-primary);
background: var(--surface-sunken);
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
}
.hint,
.status,
.error {
@@ -296,15 +289,4 @@
.error {
color: var(--state-warning);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
</style>
+16 -12
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 { confirmAction } from '$ui/confirm.js';
import { beforeNavigate, goto } from '$app/navigation';
@@ -230,8 +233,8 @@
{#if dirty}<span class="draft-tag">Unsaved draft</span>{/if}
</div>
<div class="version-toolbar">
<label
>Starting version<select
<Field label="Starting version">
<Select
aria-label="Starting version"
value={selected}
onchange={changeVersion}
@@ -240,8 +243,8 @@
>v{version.version}{version.id === currentVersionId
? ' · promoted'
: ''}{version.id === versions[0]?.id ? ' · latest' : ''}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
<label class="file-button"
>Import .cfg<input
@@ -274,12 +277,12 @@
overlay={kind === 'config'}
disabled={saving || runningBetaId !== null}
/>
<label class="notes-label"
>Version notes<input
<Field class="notes-label" label="Version notes">
<Input
bind:value={notes}
placeholder="What changed, and why?"
disabled={saving || runningBetaId !== null}
/></label
/></Field
>
<div class="actions ui-sticky-actions">
<span class="muted"
@@ -355,13 +358,15 @@
.version-toolbar > label:first-child {
flex: 1;
}
.version-toolbar > :global(.field:first-child) {
flex: 1;
}
label {
display: grid;
gap: 8px;
color: var(--text-secondary);
font-size: var(--text-xs);
}
select,
input:not([type='file']) {
width: 100%;
box-sizing: border-box;
@@ -373,7 +378,6 @@
font: var(--text-sm) var(--font-body);
}
input:focus-visible,
select:focus-visible,
.file-button:focus-within {
outline: 2px solid var(--accent-primary);
outline-offset: 2px;
@@ -394,9 +398,10 @@
width: 100%;
cursor: pointer;
}
.notes-label {
.editor-panel :global(.notes-label) {
margin-top: 20px;
}
.actions {
margin-top: 16px;
}
@@ -441,8 +446,7 @@
padding: 16px;
}
}
input:not([type='checkbox']):not([type='file']),
select {
input:not([type='checkbox']):not([type='file']) {
border-color: var(--border-control);
}
</style>
+19 -15
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 { serverTitle } from '$ui/server-labels.js';
import type { ConfigInspection, ConfigServer, ConfigSource } from '$lib/config-workbench.js';
@@ -191,8 +194,8 @@
? 'Your draft composed with the other promoted configs. Saving creates an untested version.'
: 'The intended file from promoted configs and the selected map. Live files and active betas may differ.'}
</p>
{#if !fixedServer}<label class="server-label"
>Server<select
{#if !fixedServer}<Field class="server-label" label="Server">
<Select
aria-label="Server"
bind:value={selected}
disabled={!!fixedServer || !eligible.length}
@@ -201,7 +204,7 @@
>{serverTitle(server.name)}</option
>{/each}
{#if !eligible.length}<option value="">No matching servers</option>{/if}
</select></label
</Select></Field
>{/if}
{#if error}<div class="error" role="alert">
{error}{inspection ? ' Showing the last completed preview.' : ''}
@@ -274,11 +277,13 @@
></textarea>
{:else}
<div class="filters">
<input
aria-label="Search effective settings"
bind:value={query}
placeholder="Search settings or sources…"
/><label class="check"
<Field label="Search effective settings" hiddenLabel class="search"
><Input
aria-label="Search effective settings"
bind:value={query}
placeholder="Search settings or sources…"
/></Field
><label class="check"
><input type="checkbox" bind:checked={onlyOverrides} /> Overrides ({overlaps})</label
>
</div>
@@ -379,13 +384,12 @@
font-size: var(--text-sm);
margin: 12px 0 20px;
}
.server-label {
.inspector :global(.server-label) {
display: grid;
gap: 8px;
font-size: var(--text-xs);
color: var(--text-secondary);
}
select,
input:not([type='checkbox']) {
width: 100%;
box-sizing: border-box;
@@ -396,7 +400,6 @@
padding: 10px 12px;
font: var(--text-sm) var(--font-body);
}
select:focus-visible,
input:focus-visible,
button:focus-visible,
summary:focus-visible,
@@ -480,12 +483,13 @@
color: var(--text-secondary);
margin-left: 6px;
}
.filters > :global(.search) {
flex: 1 1 180px;
}
.filters {
margin: 16px 0 8px;
}
.filters > input {
flex: 1 1 180px;
}
.check {
font-size: var(--text-xs);
color: var(--text-secondary);
@@ -532,6 +536,7 @@
color: var(--text-secondary);
margin-top: 6px;
}
.source-label::before {
content: '▸';
}
@@ -610,7 +615,6 @@
}
}
input:not([type='checkbox']):not([type='file']),
select,
textarea {
border-color: var(--border-control);
}
+50 -43
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 { page } from '$app/stores';
import { contextHref } from '$lib/navigation-context.js';
import { onMount } from 'svelte';
@@ -247,28 +250,35 @@
<ConfigInspector />
{:else}
{#if loading || artifacts.length || error}<div class="library-toolbar">
<input
type="search"
class="search"
aria-label="Search configs"
bind:value={query}
placeholder="Search configs, maps, or servers…"
/>
<select aria-label="Filter by game" bind:value={game}
><option value="">All games</option>{#each gameOptions as option}<option
value={option}>{gameTitle(option)}</option
>{/each}</select
<Field label="Search configs" hiddenLabel class="search"
><Input
type="search"
aria-label="Search configs"
bind:value={query}
placeholder="Search configs, maps, or servers…"
/></Field
>
<select aria-label="Filter by scope" bind:value={scope}
><option value="">All scopes</option><option value="game">Game</option
><option value="map">Map</option></select
<Field label="Filter by game" hiddenLabel
><Select aria-label="Filter by game" bind:value={game}
><option value="">All games</option>{#each gameOptions as option}<option
value={option}>{gameTitle(option)}</option
>{/each}</Select
></Field
>
<select aria-label="Filter by status" bind:value={status}
><option value="">All statuses</option><option value="pending"
>Awaiting promotion</option
><option value="promoted">Has promoted version</option><option value="beta"
>Active beta</option
><option value="failed">Test failed</option></select
<Field label="Filter by scope" hiddenLabel
><Select aria-label="Filter by scope" bind:value={scope}
><option value="">All scopes</option><option value="game">Game</option
><option value="map">Map</option></Select
></Field
>
<Field label="Filter by status" hiddenLabel
><Select aria-label="Filter by status" bind:value={status}
><option value="">All statuses</option><option value="pending"
>Awaiting promotion</option
><option value="promoted">Has promoted version</option><option
value="beta">Active beta</option
><option value="failed">Test failed</option></Select
></Field
>
{#if filtered.length && (query || game || scope || status)}<Button
size="sm"
@@ -390,15 +400,15 @@
>
</div>
<div class="form-grid">
<label
>Name<input
<Field label="Name">
<Input
bind:value={name}
placeholder={kind === 'config' ? 'No AFK timeout' : 'Black Ops I baseline'}
required
disabled={saving}
/></label
><label
>Game<select
/></Field
><Field label="Game">
<Select
aria-label="Game"
bind:value={scopeGame}
onchange={() => (scopeMap = '')}
@@ -406,16 +416,16 @@
disabled={saving}
><option value="" disabled>Choose a game</option
>{#each gameOptions as option}<option value={option}>{gameTitle(option)}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
{#if kind === 'config'}<label
>Apply to<select aria-label="Apply to" bind:value={scopeType} disabled={saving}
{#if kind === 'config'}<Field label="Apply to">
<Select aria-label="Apply to" bind:value={scopeType} disabled={saving}
><option value="game">Whole game</option><option value="map">One map</option
></select
></label
>{#if scopeType === 'map'}<label
>Map<select
></Select
></Field
>{#if scopeType === 'map'}<Field label="Map">
<Select
aria-label="Map"
bind:value={scopeMap}
required
@@ -423,8 +433,8 @@
><option value="" disabled>Choose a map</option
>{#each scopeOptions.mapsByGame[scopeGame] ?? [] as option}<option
value={option}>{option}</option
>{/each}</select
></label
>{/each}</Select
></Field
>{/if}{/if}
</div>
{#if baselineExists}<p class="warning">
@@ -442,12 +452,12 @@
/></label
>
<ConfigTextEditor bind:text overlay={kind === 'config'} disabled={saving} />
<label class="notes"
>Notes<input
<Field class="notes" label="Notes">
<Input
bind:value={notes}
placeholder="What is this config for?"
disabled={saving}
/></label
/></Field
>
{#if createError}<div class="error" role="alert">{createError}</div>{/if}
<div class="dialog-actions">
@@ -504,13 +514,11 @@
}
button:focus-visible,
input:focus-visible,
select:focus-visible,
a:focus-visible {
outline: 2px solid var(--accent-primary);
outline-offset: 3px;
}
input,
select {
input {
box-sizing: border-box;
min-width: 0;
background: var(--surface-sunken);
@@ -653,8 +661,7 @@
.loading {
padding: 24px 0;
}
input:not([type='checkbox']):not([type='file']),
select {
input:not([type='checkbox']):not([type='file']) {
border-color: var(--border-control);
}
</style>
+31 -37
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import Field from '$ui/components/primitives/Field.svelte';
import Input from '$ui/components/primitives/Input.svelte';
import {
addConfigSetting,
changeConfigValue,
@@ -69,44 +71,50 @@
></textarea>
{:else}
<div class="settings">
<input
class="search"
aria-label="Find a setting"
bind:value={search}
placeholder="Find a setting…"
/>
<Field label="Find a setting" hiddenLabel class="search"
><Input
aria-label="Find a setting"
bind:value={search}
placeholder="Find a setting…"
/></Field
>
<p class="hint">
Edit values and inline comments. Other commands and formatting stay in the raw file.
</p>
{#each visible as entry (entry.index)}
<label class="setting"
><span><code>{entry.key}</code><small>Line {entry.index + 1}</small></span>
<input
<Field class="setting"
>{#snippet label()}<span
><code>{entry.key}</code><small>Line {entry.index + 1}</small></span
>{/snippet}
<Input
mono
value={entry.value}
{disabled}
aria-label={`${entry.key}, line ${entry.index + 1}`}
oninput={(e) =>
(text = changeConfigValue(text, entry.index, e.currentTarget.value))}
/>
</label>
</Field>
{:else}<p class="hint">
{entries.length ? 'No matching settings.' : 'Add your first setting below.'}
</p>{/each}
<div class="add-setting">
<label
>Setting name<input
<Field label="Setting name">
<Input
mono
bind:value={newKey}
onkeydown={addOnEnter}
placeholder="g_inactivity"
{disabled}
/></label
><label
>Value<input
/></Field
><Field label="Value">
<Input
mono
bind:value={newValue}
onkeydown={addOnEnter}
placeholder="0"
{disabled}
/></label
/></Field
><Button
size="sm"
onclick={addSetting}
@@ -170,7 +178,6 @@
color: var(--text-secondary);
}
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
outline: 2px solid var(--accent-primary);
outline-offset: -2px;
@@ -189,8 +196,7 @@
font: var(--text-sm)/1.8 var(--font-mono);
tab-size: 4;
}
textarea:disabled,
input:disabled {
textarea:disabled {
opacity: 0.65;
}
.settings {
@@ -198,17 +204,8 @@
max-height: 520px;
overflow: auto;
}
input {
box-sizing: border-box;
background: var(--surface);
color: var(--text-primary);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
padding: 9px 12px;
font: var(--text-sm) var(--font-mono);
width: 100%;
}
.setting {
.settings :global(.setting) {
display: grid;
grid-template-columns: minmax(140px, 1fr) minmax(0, 1.4fr);
align-items: center;
@@ -216,7 +213,7 @@
padding: 12px 0;
border-top: 1px solid var(--border-subtle);
}
.setting span {
.settings :global(.setting span) {
min-width: 0;
overflow-wrap: anywhere;
}
@@ -228,11 +225,9 @@
border-top: 1px solid var(--border-strong);
padding-top: 16px;
}
.add-setting label {
.add-setting :global(.field) {
flex: 1 1 140px;
display: grid;
gap: 6px;
font-size: var(--text-sm);
}
code {
font: var(--text-sm) var(--font-mono);
@@ -259,7 +254,7 @@
margin: 6px 0;
}
@media (max-width: 600px) {
.setting {
.settings :global(.setting) {
grid-template-columns: 1fr;
gap: 8px;
}
@@ -267,7 +262,6 @@
display: none;
}
}
input:not([type='checkbox']):not([type='file']),
textarea {
border-color: var(--border-control);
}
+10 -16
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 { onMount, onDestroy, tick } from 'svelte';
import { goto } from '$app/navigation';
import Dialog from './primitives/Dialog.svelte';
@@ -246,9 +248,9 @@
<p class="muted" role="status">Loading maps…</p>
{:else}
<div class="fields">
<label
>Game<select
bind:this={gameSelect}
<Field label="Game">
<Select
bind:element={gameSelect}
bind:value={game}
disabled={launching || unconfirmedLaunch}
onchange={(event) => {
@@ -260,16 +262,16 @@
<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
bind:value={mapSlug}
disabled={launching || unconfirmedLaunch}
onchange={selectionChanged}
>
{#each maps as map}<option value={map.slug}>{map.friendly}</option>{/each}
</select></label
</Select></Field
>
</div>
@@ -414,15 +416,7 @@
grid-template-columns: 1fr 1.3fr;
gap: 14px;
}
label {
display: grid;
gap: 8px;
color: var(--text-secondary);
font-size: var(--text-sm);
}
select {
width: 100%;
}
.summary {
display: grid;
gap: 8px;
@@ -1,4 +1,6 @@
<script lang="ts">
import Select from '$ui/components/primitives/Select.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import { untrack } from 'svelte';
import { currentRound, freshServer, type FleetServer } from '$lib/fleet-overview.js';
import { actionLabels } from '$lib/provisioning.js';
@@ -130,10 +132,10 @@
wide
>
<div class="fields">
<label class="action"
>Action<select bind:value={action} disabled={busy} onchange={chooseAction}>
<Field class="action" label="Action">
<Select bind:value={action} disabled={busy} onchange={chooseAction}>
{#each fleetActions as value}<option {value}>{actionLabels[value]}</option>{/each}
</select></label
</Select></Field
>
<p>{descriptions[action]}</p>
{#if error}<p class="ui-error" role="alert">{error}</p>{/if}
@@ -236,23 +238,14 @@
<style>
.fields,
.action {
.fields :global(.action) {
display: grid;
gap: var(--space-4);
}
.action {
.fields :global(.action) {
gap: var(--space-2);
}
select {
width: 100%;
min-height: 40px;
padding: 10px;
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--text-primary);
font: inherit;
}
p,
ul {
margin: 0;
+9 -17
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 { untrack } from 'svelte';
import { goto } from '$app/navigation';
import { currentRound, freshServer, type FleetServer } from '$lib/fleet-overview.js';
@@ -145,20 +147,16 @@
<Dialog bind:open {title} {busy}>
<div class="fields">
{#if !initialServer}
<label
>Server <select
bind:value={server}
disabled={busy || loading}
onchange={chooseServer}
>
<Field label="Server">
<Select bind:value={server} disabled={busy || loading} onchange={chooseServer}>
<option value="">{loading ? 'Loading servers…' : 'Choose a server'}</option>
{#each servers as item (item.name)}<option value={item.name}
>{serverTitle(item.name)}</option
>{/each}
</select></label
</Select></Field
>
<label
>Action <select
<Field label="Action">
<Select
bind:value={action}
disabled={busy || !server || loading}
onchange={() => {
@@ -172,7 +170,7 @@
>{/if}
{#each available as value}<option {value}>{actionLabels[value]}</option>{/each}
{#if server}<option value="destroy">Destroy server</option>{/if}
</select></label
</Select></Field
>
{/if}
{#if loading}<p role="status">Checking the latest server status…</p>
@@ -219,13 +217,7 @@
display: grid;
gap: 8px;
}
select {
padding: 10px;
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--text-primary);
}
p {
margin: 0;
line-height: 1.6;
+5 -15
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 { tick, untrack, onMount, onDestroy } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
@@ -193,12 +195,8 @@
retryDisabled={loading}
/>
<div class="log-tools">
<label class="log-search"
>Search loaded lines<input
type="search"
bind:value={query}
placeholder="Find text in this log"
/></label
<Field class="log-search" label="Search loaded lines">
<Input type="search" bind:value={query} placeholder="Find text in this log" /></Field
>
<details class="log-actions">
<summary>More</summary>
@@ -302,7 +300,7 @@
.log-tools {
align-items: end;
}
.log-search {
.log-tools :global(.log-search) {
display: grid;
gap: 6px;
flex: 1;
@@ -310,14 +308,6 @@
font-size: var(--text-xs);
color: var(--text-secondary);
}
.log-search input {
background: var(--surface-sunken);
color: var(--text-primary);
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
padding: 9px 12px;
font: inherit;
}
.copy-status {
margin: 0;
font-size: var(--text-xs);
+40 -46
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 RelativeTime from '$ui/components/RelativeTime.svelte';
import { untrack } from 'svelte';
import { page } from '$app/stores';
@@ -129,17 +132,17 @@
<section class="history" aria-label="Match history">
{#snippet filterFields(suffix: string)}
<div class="filters">
{#if server === undefined}<label for={`match-game${suffix}`}
>Game<select
{#if server === undefined}<Field forId={`match-game${suffix}`} label="Game">
<Select
id={`match-game${suffix}`}
value={filters.game}
onchange={(event) => update({ 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
><option value="t6">Black Ops II</option></Select
></Field
>{/if}
<label for={`match-map${suffix}`}
>Map<select
<Field forId={`match-map${suffix}`} label="Map">
<Select
id={`match-map${suffix}`}
value={filters.map}
onchange={(event) => update({ map: event.currentTarget.value })}
@@ -153,7 +156,7 @@
? ` · ${map.game === 't5' ? 'BO1' : 'BO2'}`
: ''}</option
>{/each}
</select></label
</Select></Field
>
{#if server === undefined}<form
role="search"
@@ -163,32 +166,32 @@
if (suffix) filtersOpen = false;
}}
>
<label for={`match-search${suffix}`}
>Server<input
<Field forId={`match-search${suffix}`} label="Server">
<Input
id={`match-search${suffix}`}
type="search"
bind:value={search}
maxlength="100"
maxlength={100}
placeholder="Find a server…"
aria-describedby={`match-search-help${suffix}`}
/></label
/></Field
>
<span id={`match-search-help${suffix}`} class="sr-only"
>Press Enter to search by server name.</span
>
</form>{/if}
<label for={`match-days${suffix}`}
>Date range<select
<Field forId={`match-days${suffix}`} label="Date range">
<Select
id={`match-days${suffix}`}
value={String(filters.days)}
onchange={(event) => update({ days: event.currentTarget.value })}
><option value="0">All time</option><option value="7">Last 7 days</option
><option value="30">Last 30 days</option><option value="90">Last 90 days</option
></select
></label
></Select
></Field
>
<label for={`match-status${suffix}`}
>Status<select
<Field forId={`match-status${suffix}`} label="Status">
<Select
id={`match-status${suffix}`}
value={filters.status}
onchange={(event) => update({ status: event.currentTarget.value })}
@@ -196,28 +199,28 @@
>Completed</option
><option value="active">In progress</option><option value="interrupted"
>Interrupted</option
></select
></label
></Select
></Field
>
{#if suffix}
<label for={`match-sort${suffix}`}
>Sort by<select
<Field forId={`match-sort${suffix}`} label="Sort by">
<Select
id={`match-sort${suffix}`}
value={filters.sort}
onchange={(event) => update({ sort: event.currentTarget.value })}
>{#each sorts as item}<option value={item.key}>{item.label}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
<label for={`match-order${suffix}`}
>Order<select
<Field forId={`match-order${suffix}`} label="Order">
<Select
id={`match-order${suffix}`}
value={filters.direction}
onchange={(event) => update({ direction: event.currentTarget.value })}
><option value="desc">Highest or newest first</option><option value="asc"
>Lowest or oldest first</option
></select
></label
></Select
></Field
>
{/if}
</div>
@@ -460,30 +463,19 @@
flex-wrap: wrap;
gap: var(--space-3);
}
label,
form {
flex: 1;
min-width: 145px;
}
label {
display: flex;
.filters :global(.field) {
flex: 1;
min-width: 145px;
}
.filters :global(.field) {
flex-direction: column;
gap: 6px;
color: var(--text-secondary);
font-size: var(--text-xs);
}
input,
select {
width: 100%;
min-height: 40px;
padding: 8px 10px;
border: 1px solid var(--border-control);
border-radius: var(--radius-md);
background: var(--surface-sunken);
color: var(--text-primary);
font: inherit;
font-size: var(--text-sm);
}
summary {
cursor: pointer;
color: var(--text-secondary);
@@ -640,10 +632,12 @@
display: grid;
grid-template-columns: minmax(0, 1fr);
}
.filters > label,
.filters > form {
min-width: 0;
}
.filters > :global(.field) {
min-width: 0;
}
.context {
gap: var(--space-1) var(--space-2);
}
@@ -1,4 +1,6 @@
<script lang="ts">
import Input from '$ui/components/primitives/Input.svelte';
import Field from '$ui/components/primitives/Field.svelte';
import { gameTitle } from '$ui/server-labels.js';
import RelativeTime from '$ui/components/RelativeTime.svelte';
import type { Host, NativeDeploymentSelection } from '$lib/hosts.js';
@@ -148,36 +150,36 @@
</script>
{#snippet runtimeFields()}
<label
>{compact ? 'Runtime image' : 'Plutainer runtime image'}<input
<Field label={compact ? 'Runtime image' : 'Plutainer runtime image'}>
<Input
required
bind:value={selection.image}
{disabled}
oninput={() => onchange?.()}
/></label
/></Field
>
{#if compact}<p>Custom images must be prepared on a host before it can run this server.</p>{/if}
{/snippet}
{#snippet networkFields()}
<label
>Local bind address<input
<Field label="Local bind address">
<Input
bind:value={selection.bindAddress}
placeholder="Host default"
{disabled}
oninput={() => onchange?.()}
/></label
/></Field
>
<label
>Public / NAT address<input
<Field label="Public / NAT address">
<Input
bind:value={selection.publicAddress}
placeholder="Host default"
{disabled}
oninput={() => onchange?.()}
/></label
/></Field
>
<label
>Public / NAT port<input
<Field label="Public / NAT port">
<Input
type="number"
min="1"
max="65535"
@@ -185,7 +187,7 @@
placeholder="Same as local port"
{disabled}
oninput={() => onchange?.()}
/></label
/></Field
>
<p>Public mapping does not change the local Docker port reservation.</p>
{/snippet}
@@ -317,21 +319,7 @@
font-size: var(--text-sm);
margin: 0;
}
label {
display: grid;
gap: 7px;
font-size: var(--text-sm);
}
input {
width: 100%;
box-sizing: border-box;
padding: 11px;
color: var(--text-primary);
background: var(--surface);
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
font: inherit;
}
p {
color: var(--text-secondary);
font-size: var(--text-xs);
@@ -360,7 +348,8 @@
cursor: pointer;
font-size: var(--text-sm);
}
details label {
details :global(.field) {
margin-top: 14px;
}
details p {
+13 -23
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 type { ActivityEvent, OperationStep } from '$lib/operation-flow.js';
import Button from './primitives/Button.svelte';
@@ -54,19 +57,19 @@
>
{#if events.length}
<div class="tools">
<label
>Search activity<input
<Field label="Search activity">
<Input
type="search"
bind:value={query}
oninput={() => (limit = 25)}
placeholder="Server, stage, or message"
/></label
><label
>Order<select bind:value={newestFirst}
/></Field
><Field label="Order">
<Select bind:value={newestFirst}
><option value={true}>Newest first</option><option value={false}
>Oldest first</option
></select
></label
></Select
></Field
>
</div>
<ol class="timeline">
@@ -133,25 +136,12 @@
gap: 12px;
margin-bottom: 20px;
}
label {
display: grid;
gap: 6px;
font-size: var(--text-xs);
color: var(--text-secondary);
}
label:first-child {
.tools > :global(.field:first-child) {
flex: 1;
min-width: 180px;
}
input,
select {
background: var(--surface-sunken);
color: var(--text-primary);
border: 1px solid var(--border-control);
padding: 9px 12px;
border-radius: var(--radius-sm);
font: inherit;
}
.time,
.muted {
color: var(--text-secondary);
+19 -40
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 { timeFormat } from '$ui/stores/time-zone.js';
import { onMount, onDestroy } from 'svelte';
import { goto } from '$app/navigation';
@@ -184,9 +187,8 @@
<p>Import a release below to track updates for this plugin.</p>
{/if}
<form onsubmit={preview} class="lookup">
<label
>Repository or release URL
<input
<Field label="Repository or release URL">
<Input
type="url"
bind:value={url}
oninput={() => {
@@ -197,7 +199,7 @@
placeholder="https://github.com/alicealys/t6-gsc-utils/releases/tag/v3.1.0"
disabled={busy || disabled}
/>
</label>
</Field>
<Button
type="submit"
loading={busy && !release}
@@ -228,29 +230,29 @@
</p>
{:else}
<div class="fields">
<label
>DLL asset<select bind:value={assetId} disabled={busy || disabled}>
<Field label="DLL asset">
<Select bind:value={assetId} disabled={busy || disabled}>
<option value="" disabled>Choose a DLL…</option>
{#each release.assets as asset}<option value={String(asset.id)}
>{asset.name} · {(asset.size / 1024).toFixed(1)} KB</option
>{/each}
</select></label
</Select></Field
>
{#if !pluginId}
<label
>Plugin name<input
<Field label="Plugin name">
<Input
bind:value={name}
disabled={busy}
placeholder="t6-gsc-utils"
/></label
/></Field
>
<label
>Deploy scope<select bind:value={game} disabled={busy}
<Field label="Deploy scope">
<Select bind:value={game} disabled={busy}
><option value="">Unscoped — select servers in a beta</option
><option value="t5">Black Ops I</option><option value="t6"
>Black Ops II</option
></select
></label
></Select
></Field
>
{/if}
</div>
@@ -334,30 +336,11 @@
gap: var(--space-3);
flex-wrap: wrap;
}
.lookup label {
.lookup :global(.field) {
flex: 1 1 300px;
}
label {
display: grid;
gap: var(--space-2);
font-size: var(--text-sm);
}
input,
select {
width: 100%;
min-width: 0;
padding: 10px 12px;
background: var(--surface-sunken);
border: 1px solid var(--border-strong);
border-radius: var(--radius-md);
color: var(--text-primary);
font: inherit;
}
input:focus-visible,
select:focus-visible {
outline: 2px solid var(--accent-primary);
outline-offset: 2px;
}
.fields {
display: grid;
gap: var(--space-3);
@@ -404,8 +387,4 @@
font: var(--text-xs) var(--font-mono);
margin-top: var(--space-2);
}
input:not([type='checkbox']):not([type='file']),
select {
border-color: var(--border-control);
}
</style>
@@ -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 { timestampISO } from '$lib/time-zone.js';
import { timeFormat } from '$ui/stores/time-zone.js';
import { onDestroy } from 'svelte';
@@ -125,23 +128,23 @@
void load();
}}
>
<label class="search"
>Search activity<input
<Field class="search" label="Search activity">
<Input
type="search"
bind:value={query}
placeholder="Find a stage or message"
/></label
/></Field
>
<label
>Order<select
<Field label="Order">
<Select
bind:value={order}
onchange={() => {
void load();
}}
><option value="newest">Newest first</option><option value="oldest"
>Oldest first</option
></select
></label
></Select
></Field
>
<Button type="submit" disabled={loading}>Search</Button>
</form>
@@ -270,31 +273,11 @@
align-items: end;
gap: var(--space-3);
}
.tools label {
display: grid;
gap: var(--space-2);
font-size: var(--text-xs);
color: var(--text-secondary);
}
.search {
.tools :global(.search) {
flex: 1 1 160px;
}
input,
select {
background: var(--surface-sunken);
color: var(--text-primary);
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
padding: 10px;
font: inherit;
min-height: 40px;
width: 100%;
}
input:focus-visible,
select:focus-visible {
outline: 2px solid var(--border-focus);
outline-offset: 2px;
}
.new-events {
display: flex;
align-items: center;
@@ -333,7 +316,7 @@
.tools {
align-items: stretch;
}
.tools .search {
.tools :global(.search) {
flex-basis: 100%;
}
}
+11 -18
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 type { FleetSettings } from '$lib/fleet-settings.js';
let { settings }: { settings: FleetSettings } = $props();
import Button from './primitives/Button.svelte';
@@ -141,12 +144,14 @@
retry requests for that campaign; unfinished recovery still prevents cleanup.
</p>
<div class="record">
<label
>Category<select bind:value={category}
>{#each categories as value}<option {value}>{value}</option>{/each}</select
></label
<Field label="Category">
<Select bind:value={category}
>{#each categories as value}<option {value}>{value}</option>{/each}</Select
></Field
>
<Field label="Record ID">
<Input bind:value={recordId} maxlength={200} /></Field
>
<label>Record ID<input bind:value={recordId} maxlength="200" /></label>
<Button disabled={busy || !recordId} onclick={() => control({ protected: true })}
>Protect record</Button
>
@@ -179,19 +184,7 @@
code {
overflow-wrap: anywhere;
}
label {
display: grid;
gap: 8px;
}
select,
input {
font: inherit;
color: var(--text-primary);
background: var(--surface-sunken);
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
padding: 8px;
}
.table-wrap {
overflow-x: auto;
margin: 16px 0;
+20 -20
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 { goto } from '$app/navigation';
import { tick } from 'svelte';
import { plural } from '$ui/plural.js';
@@ -306,8 +309,8 @@
<a href="/scripts/{item.result.scriptId}">View script →</a>
</p>
{:else}
<label
>Destination<select
<Field label="Destination">
<Select
bind:value={item.target}
disabled={busy || item.inspecting}
onchange={() => (item.error = '')}
@@ -320,10 +323,10 @@
? ' · beta active'
: ''}</option
>{/each}
</select></label
</Select></Field
>
{#if item.target === 'new'}<label
>Script name<input bind:value={item.name} disabled={busy} /></label
{#if item.target === 'new'}<Field label="Script name">
<Input bind:value={item.name} disabled={busy} /></Field
>
{:else if item.target !== ''}{@const s = scripts.find(
(s) => s.id === item.target
@@ -362,8 +365,8 @@
></textarea></label
>
{#if hasNew}
<label
>Scope for new scripts<select
<Field label="Scope for new scripts">
<Select
bind:value={scopeType}
disabled={busy}
onchange={() => {
@@ -372,28 +375,28 @@
}}
><option value="none">Unscoped · choose beta targets</option><option
value="game">Game</option
><option value="map">Map</option></select
></label
><option value="map">Map</option></Select
></Field
>
{#if scopeType !== 'none'}
<label
>Game<select
<Field label="Game">
<Select
bind:value={scopeGame}
disabled={busy || !scopeLoaded}
onchange={() => (scopeMap = '')}
><option value="">Choose a game…</option
>{#each scopeOptions.games as g}<option value={g}
>{gameTitle(g)}</option
>{/each}</select
></label
>{/each}</Select
></Field
>
{#if scopeType === 'map'}<label
>Map<select bind:value={scopeMap} disabled={busy || !scopeGame}
{#if scopeType === 'map'}<Field label="Map">
<Select bind:value={scopeMap} disabled={busy || !scopeGame}
><option value="">Choose a map…</option
>{#each scopeOptions.mapsByGame[scopeGame] ?? [] as m}<option
value={m}>{mapLabel(m)}</option
>{/each}</select
></label
>{/each}</Select
></Field
>{/if}
{#if scopeValue}<p class="note">
{matches.length} matching servers: {matches
@@ -548,7 +551,6 @@
color: var(--text-secondary);
}
input,
select,
textarea {
box-sizing: border-box;
width: 100%;
@@ -565,7 +567,6 @@
resize: vertical;
}
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 2px solid var(--accent-primary);
outline-offset: 2px;
@@ -622,7 +623,6 @@
}
}
input:not([type='checkbox']):not([type='file']),
select,
textarea {
border-color: var(--border-control);
}
+56 -57
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 { untrack } from 'svelte';
import { beforeNavigate, goto } from '$app/navigation';
import { confirmAction } from '$ui/confirm.js';
@@ -155,18 +158,18 @@
<svelte:window onbeforeunload={beforeUnload} />
<div class="settings-layout">
<aside class="section-rail">
<label class="search-label" for="settings-search">Find a setting</label>
<input
id="settings-search"
type="search"
placeholder="Search settings"
bind:value={query}
disabled={saving}
oninput={() => (reviewInvalid = false)}
/>
<label class="mobile-section"
>Section
<select
<Field label="Find a setting" forId="settings-search">
<Input
id="settings-search"
type="search"
placeholder="Search settings"
bind:value={query}
disabled={saving}
oninput={() => (reviewInvalid = false)}
/></Field
>
<Field class="mobile-section" label="Section">
<Select
aria-label="Settings section"
value={selectedGroup}
disabled={saving}
@@ -177,8 +180,8 @@
? ` · ${changed.filter((key) => group.keys.includes(key)).length} changed`
: ''}</option
>{/each}
</select>
</label>
</Select>
</Field>
<nav class="group-links" aria-label="Settings sections">
{#each settingsGroups as group}
{@const count = changed.filter((key) => group.keys.includes(key)).length}
@@ -263,30 +266,37 @@
>
<td class="value-cell">
<div class="numeric-value">
{#if unlimited}<input
id={`setting-${key}`}
type="number"
disabled
placeholder="—"
aria-describedby={`setting-${key}-help setting-${key}-default`}
/>
{:else}<input
id={`setting-${key}`}
type="number"
min={Math.max(1, settingMinimum(key))}
max={key.endsWith('Days')
? 36500
: undefined}
step="1"
required
bind:value={draft[key]}
aria-invalid={!!validation}
aria-describedby={`setting-${key}-help setting-${key}-default${validation ? ` setting-${key}-error` : ''}`}
oninput={() => {
error = '';
notice = '';
}}
/>{/if}
{#if unlimited}<Field
><Input
id={`setting-${key}`}
type="number"
disabled
placeholder="—"
aria-describedby={`setting-${key}-help setting-${key}-default`}
/></Field
>
{:else}<Field
><Input
id={`setting-${key}`}
type="number"
min={Math.max(
1,
settingMinimum(key)
)}
max={key.endsWith('Days')
? 36500
: undefined}
step="1"
required
bind:value={draft[key]}
aria-invalid={!!validation}
aria-describedby={`setting-${key}-help setting-${key}-default${validation ? ` setting-${key}-error` : ''}`}
oninput={() => {
error = '';
notice = '';
}}
/></Field
>{/if}
<span class="unit">{settingUnit(key)}</span>
</div>
{#if zeroMode}<label
@@ -393,13 +403,7 @@
gap: var(--space-2);
min-width: 0;
}
.search-label {
font-size: var(--text-sm);
color: var(--text-secondary);
}
#settings-search {
width: 100%;
}
.group-links {
display: grid;
gap: var(--space-1);
@@ -521,17 +525,15 @@
flex-wrap: wrap;
gap: var(--space-1);
}
.numeric-value input {
width: 88px;
min-height: 32px;
padding: 5px var(--space-2);
font-family: var(--font-numeric);
font-variant-numeric: tabular-nums;
}
input:disabled {
color: var(--text-tertiary);
opacity: 0.75;
}
.numeric-value > :global(.field) {
width: 100px;
flex: 0 0 auto;
}
.unit {
color: var(--text-secondary);
font-size: var(--text-xs);
@@ -646,7 +648,7 @@
.notice {
color: var(--state-online);
}
.mobile-section,
.section-rail :global(.mobile-section),
.mobile-label {
display: none;
}
@@ -680,7 +682,7 @@
.group-links {
display: none;
}
.mobile-section {
.section-rail :global(.mobile-section) {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
@@ -688,9 +690,6 @@
font-size: var(--text-sm);
color: var(--text-secondary);
}
.mobile-section select {
width: 100%;
}
.table-wrap {
overflow: visible;
}
@@ -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, untrack } from 'svelte';
import type {
RebalancePreview,
@@ -254,15 +257,15 @@
{/each}
</div>
<div class="settings">
<label
>Game<select bind:value={game} onchange={editSettings}
<Field label="Game">
<Select bind:value={game} onchange={editSettings}
><option value="all">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
>Maximum moves<input
<Field class="max-moves" label="Maximum moves">
<Input
type="number"
min="1"
max="20"
@@ -270,7 +273,7 @@
required
bind:value={maxMoves}
oninput={editSettings}
/></label
/></Field
>
<Button
type="submit"
@@ -301,16 +304,20 @@
<section class="runs" aria-labelledby="rebalance-runs-title">
<div class="heading">
<h3 id="rebalance-runs-title">Rebalance runs</h3>
{#if report.runs.length}<label class="run-picker"
><span class="sr-only">Select rebalance run</span><select
{#if report.runs.length}<Field
class="run-picker"
hiddenLabel
label="Select rebalance run"
>
<Select
value={run?.id ?? ''}
onchange={(event) => (selectedRun = event.currentTarget.value)}
>{#each report.runs as item (item.id)}<option value={item.id}
>{$timeFormat.dateTime(item.createdAt)} · {rebalanceRunLabels[
item.state
]}</option
>{/each}</select
></label
>{/each}</Select
></Field
>{/if}
</div>
{#if preferredRun && !report.runs.some((item) => item.id === preferredRun)}<p
@@ -422,37 +429,18 @@
opacity: 0.65;
cursor: default;
}
.settings > :global(.max-moves) {
width: 110px;
}
.settings {
justify-content: flex-start;
align-items: end;
}
.settings label {
display: grid;
gap: 8px;
font-size: var(--text-xs);
}
.settings input {
width: 110px;
}
select,
input[type='number'] {
box-sizing: border-box;
padding: 9px 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);
min-width: 0;
}
input:disabled,
select:disabled {
input:disabled {
opacity: 0.6;
}
.run-picker {
max-width: 100%;
}
.run-picker select {
.heading :global(.run-picker) {
max-width: 100%;
}
.runs {
@@ -490,19 +478,11 @@
text-decoration: underline;
}
button:focus-visible,
input:focus-visible,
select:focus-visible {
input:focus-visible {
outline: 2px solid var(--accent-primary);
outline-offset: 3px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@media (max-width: 600px) {
.panel {
padding: 16px;
+35 -7
View File
@@ -1,9 +1,11 @@
<script lang="ts">
import { setContext, type Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
import { fieldContext, type FieldContext } from '../../field-context.js';
interface Props {
label?: string;
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
label?: string | Snippet;
hiddenLabel?: boolean;
hint?: string | null;
error?: string | null;
required?: boolean;
@@ -11,11 +13,21 @@
children?: Snippet;
}
let { label, hint = null, error = null, required = false, forId, children }: Props = $props();
let {
label,
hiddenLabel = false,
hint = null,
error = null,
required = false,
forId,
children,
class: className,
...attributes
}: Props = $props();
const id = $props.id();
setContext<FieldContext>(fieldContext, {
get labelId() {
return `${id}-label`;
return label ? `${id}-label` : undefined;
},
get messageId() {
return error || hint ? `${id}-message` : undefined;
@@ -42,10 +54,15 @@
}
</script>
<div class="field" use:connectLabel>
<div {...attributes} class={['field', className]} use:connectLabel>
{#if label}
<label class="label" id={`${id}-label`} for={forId ?? `${id}-control`}>
{label}
<label
class="label"
class:sr-only={hiddenLabel}
id={`${id}-label`}
for={forId ?? `${id}-control`}
>
{#if typeof label === 'string'}{label}{:else}{@render label()}{/if}
{#if required}<span class="req" aria-hidden="true">*</span>{/if}
</label>
{/if}
@@ -60,6 +77,17 @@
</div>
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.field {
display: flex;
flex-direction: column;
+35 -42
View File
@@ -1,61 +1,54 @@
<script lang="ts">
<script lang="ts" generics="Value extends string | number | undefined = string">
import { getContext } from 'svelte';
import type { HTMLInputAttributes } from 'svelte/elements';
import { fieldContext, type FieldContext } from '../../field-context.js';
const field = getContext<FieldContext | undefined>(fieldContext);
interface Props {
value?: string;
type?: 'text' | 'search' | 'email' | 'number' | 'url' | 'password' | 'file';
placeholder?: string;
id?: string;
name?: string;
required?: boolean;
disabled?: boolean;
readonly?: boolean;
autocomplete?: import('svelte/elements').FullAutoFill;
interface Props extends Omit<HTMLInputAttributes, 'value' | 'type'> {
value?: Value;
type?:
| 'text'
| 'search'
| 'email'
| 'number'
| 'url'
| 'password'
| 'file'
| 'tel'
| 'time'
| 'date'
| 'datetime-local'
| 'month'
| 'week';
mono?: boolean;
onchange?: (e: Event) => void;
oninput?: (e: Event) => void;
accept?: string;
ariaLabel?: string;
element?: HTMLInputElement;
}
let {
value = $bindable(''),
value = $bindable(),
element = $bindable(),
type = 'text',
placeholder,
id,
name,
required = false,
disabled = false,
readonly = false,
autocomplete,
mono = false,
onchange,
oninput,
accept,
ariaLabel
ariaLabel,
class: className,
...attributes
}: Props = $props();
</script>
<input
class="input"
class:mono
{...attributes}
class={['input', className, { mono }]}
{type}
{id}
{name}
{placeholder}
{required}
{disabled}
{readonly}
{autocomplete}
{accept}
aria-label={ariaLabel}
aria-labelledby={ariaLabel ? undefined : field?.labelId}
aria-describedby={field?.messageId}
aria-invalid={field?.invalid || undefined}
aria-label={ariaLabel ?? attributes['aria-label']}
aria-labelledby={attributes['aria-labelledby'] ??
(ariaLabel || attributes['aria-label'] ? undefined : field?.labelId)}
aria-describedby={[attributes['aria-describedby'], field?.messageId]
.filter(Boolean)
.join(' ') || undefined}
aria-invalid={attributes['aria-invalid'] ?? (field?.invalid || undefined)}
bind:this={element}
bind:value
{onchange}
{oninput}
/>
<style>
+27 -29
View File
@@ -1,51 +1,48 @@
<script lang="ts">
import { getContext } from 'svelte';
<script lang="ts" generics="Value = string">
import { getContext, type Snippet } from 'svelte';
import type { HTMLSelectAttributes } from 'svelte/elements';
import { fieldContext, type FieldContext } from '../../field-context.js';
const field = getContext<FieldContext | undefined>(fieldContext);
interface Option {
value: string;
value: Value;
label: string;
disabled?: boolean;
}
interface Props {
value?: string;
options: Option[];
id?: string;
name?: string;
required?: boolean;
disabled?: boolean;
interface Props extends Omit<HTMLSelectAttributes, 'value' | 'children' | 'multiple'> {
value?: Value;
options?: Option[];
children?: Snippet;
placeholder?: string;
onchange?: (e: Event) => void;
ariaLabel?: string;
element?: HTMLSelectElement;
}
let {
value = $bindable(''),
options,
id,
name,
required = false,
disabled = false,
value = $bindable(),
element = $bindable(),
options = [],
children,
placeholder,
onchange,
ariaLabel
ariaLabel,
class: className,
...attributes
}: Props = $props();
</script>
<div class="wrap">
<select
class="select"
{id}
{name}
{required}
{disabled}
aria-label={ariaLabel}
aria-labelledby={ariaLabel ? undefined : field?.labelId}
aria-describedby={field?.messageId}
aria-invalid={field?.invalid || undefined}
{...attributes}
class={['select', className]}
aria-label={ariaLabel ?? attributes['aria-label']}
aria-labelledby={attributes['aria-labelledby'] ??
(ariaLabel || attributes['aria-label'] ? undefined : field?.labelId)}
aria-describedby={[attributes['aria-describedby'], field?.messageId]
.filter(Boolean)
.join(' ') || undefined}
aria-invalid={attributes['aria-invalid'] ?? (field?.invalid || undefined)}
bind:this={element}
bind:value
{onchange}
>
{#if placeholder}
<option value="" disabled>{placeholder}</option>
@@ -53,6 +50,7 @@
{#each options as o (o.value)}
<option value={o.value} disabled={o.disabled}>{o.label}</option>
{/each}
{@render children?.()}
</select>
<span class="chev" aria-hidden="true">▾</span>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
export const fieldContext = Symbol('field');
export interface FieldContext {
readonly labelId: string;
readonly labelId: string | undefined;
readonly messageId: string | undefined;
readonly invalid: boolean;
}
+3 -16
View File
@@ -8,20 +8,8 @@
flex: 1 1 260px;
min-width: 0;
}
.library-toolbar input,
.library-toolbar select {
box-sizing: border-box;
width: 100%;
min-height: 40px;
padding: 9px 12px;
border: 1px solid var(--border-control);
border-radius: var(--radius-sm);
background: var(--surface-sunken);
color: var(--text-primary);
font: var(--text-sm) var(--font-body);
}
.library-toolbar > select {
width: auto;
.library-toolbar > .field:not(.search) {
flex: 0 1 auto;
}
.library-table {
overflow-x: auto;
@@ -93,8 +81,7 @@
outline-offset: 3px;
}
@media (max-width: 640px) {
.library-toolbar > select,
.library-toolbar > label {
.library-toolbar > .field:not(.search) {
flex: 1 1 140px;
}
.library-toolbar .search {
+4 -6
View File
@@ -47,11 +47,10 @@ dialog::backdrop {
align-items: center;
gap: var(--space-3);
}
.ui-toolbar .search,
.ui-toolbar > input[type='search'] {
.ui-toolbar > .search {
flex: 1 1 240px;
}
.ui-toolbar select {
.ui-toolbar > .field:not(.search) {
flex: 0 1 auto;
}
.ui-summary {
@@ -114,12 +113,11 @@ dialog::backdrop {
white-space: nowrap;
}
@media (max-width: 600px) {
.ui-toolbar .search,
.ui-toolbar > input[type='search'] {
.ui-toolbar > .search {
flex-basis: 100%;
width: 100%;
}
.ui-toolbar select {
.ui-toolbar > .field:not(.search) {
flex: 1 1 130px;
}
.ui-sticky-actions {