Add live registry search and themed input states
This commit is contained in:
@@ -400,6 +400,13 @@ squeezed beside long module headings on mobile.
|
||||
|
||||
- Labels are uppercase and letter-spaced.
|
||||
- Inputs use dark surfaces, one-pixel borders, and clear disabled styling.
|
||||
- Text selection uses the SHD orange selection token with dark contrasting text;
|
||||
never rely on the browser's white or platform-blue highlight. Focused text,
|
||||
number, search, select, and textarea controls use a one-pixel orange border,
|
||||
while keyboard `focus-visible` adds a restrained one-pixel external orange
|
||||
ring with a one-pixel offset. Do not stack inset accent edges with this focus
|
||||
treatment. Carets and native checkbox, radio, and range accents use orange.
|
||||
Browser-autofilled fields retain the dark SHD surface and light text.
|
||||
- Related min/max fields remain visually paired.
|
||||
- Small mutually exclusive option sets use a compact segmented radio control,
|
||||
even when visually presented like selectable checkbox cells. Include an
|
||||
@@ -742,6 +749,14 @@ Do not add zero badges or counts to informational registries such as Audit
|
||||
History and System Metrics. Badge text must expose an `awaiting review` label to
|
||||
assistive technology; color supplements that meaning rather than replacing it.
|
||||
|
||||
Registry name search is server-backed and updates as the user types after a
|
||||
short debounce; it must search the complete filtered registry rather than only
|
||||
the visible page. Preserve active filters and row-count settings, reset results
|
||||
to page one, keep input focus, avoid scrolling, and replace browser history so
|
||||
each keystroke does not create a navigation entry. Retain an explicit Search
|
||||
button for immediate submission and keyboard/accessibility fallback. During
|
||||
navigation, expose a textual pending state and suppress duplicate submissions.
|
||||
|
||||
Pending records may be used as dependencies in later proposals by any
|
||||
contributor. Render these choices in the ordinary assignment control with a
|
||||
restrained orange border/background and the explicit text `PENDING` plus the
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
--shd-text-dim:#5d6567;
|
||||
--shd-orange:#f6a821;
|
||||
--shd-orange-hover:#ffc45d;
|
||||
--shd-focus-ring:#f6a821;
|
||||
--shd-selection-bg:#f6a821;
|
||||
--shd-selection-text:#111415;
|
||||
--shd-success:#75c08c;
|
||||
--shd-danger:#df8069;
|
||||
--shd-info:#70b9d6;
|
||||
@@ -29,6 +32,7 @@
|
||||
--shd-transition:transform .12s ease,filter .18s ease,opacity .18s ease,box-shadow .18s ease;
|
||||
}
|
||||
*{box-sizing:border-box} html{scroll-behavior:smooth} body{min-height:calc(100vh / var(--shd-display-scale));display:flex;flex-direction:column;margin:0;background:var(--shd-bg);color:var(--shd-text);font-family:Arial,Helvetica,sans-serif} button,input{font:inherit} a,a:hover,a:focus,a:focus-visible,a:active,a:visited{text-decoration:none}
|
||||
::selection{background:var(--shd-selection-bg);color:var(--shd-selection-text)}input,textarea{caret-color:var(--shd-orange)}input[type=checkbox],input[type=radio],input[type=range]{accent-color:var(--shd-orange)}:where(input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]):not([type=hidden]),select,textarea):focus{border-color:var(--shd-focus-ring);outline:none}:where(input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]):not([type=hidden]),select,textarea):focus-visible{outline:1px solid var(--shd-focus-ring);outline-offset:1px}:where(input,textarea):-webkit-autofill,:where(input,textarea):-webkit-autofill:hover,:where(input,textarea):-webkit-autofill:focus{-webkit-text-fill-color:var(--shd-text);caret-color:var(--shd-orange);box-shadow:inset 0 0 0 1000px #15130e;transition:background-color 9999s ease-out}
|
||||
.sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}
|
||||
.site-footer{flex:0 0 auto;margin-top:auto}
|
||||
.contact-page.contact-page{margin:0 auto}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { page as currentPage } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { navigating, page as currentPage } from '$app/state';
|
||||
import { onDestroy } from 'svelte';
|
||||
let {
|
||||
pagination,
|
||||
namespace = '',
|
||||
@@ -14,6 +16,35 @@
|
||||
const pageKey = $derived(namespace ? `${namespace}Page` : 'page');
|
||||
const perPageKey = $derived(namespace ? `${namespace}PerPage` : 'perPage');
|
||||
const searchKey = $derived(namespace ? `${namespace}Q` : 'q');
|
||||
let draftSearch = $state<string | null>(null);
|
||||
const searchValue = $derived(draftSearch ?? search ?? '');
|
||||
let searchTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
const searchPending = $derived(Boolean(navigating.to) && navigating.to?.url.searchParams.get(searchKey) !== currentPage.url.searchParams.get(searchKey));
|
||||
$effect(() => {
|
||||
if (draftSearch !== null && draftSearch.trim() === (search ?? '').trim()) draftSearch = null;
|
||||
});
|
||||
const searchHref = (query: string) => {
|
||||
const params = new URLSearchParams(currentPage.url.searchParams);
|
||||
const normalized = query.trim();
|
||||
if (normalized) params.set(searchKey, normalized);
|
||||
else params.delete(searchKey);
|
||||
params.delete(pageKey);
|
||||
const serialized = params.toString();
|
||||
return `${currentPage.url.pathname}${serialized ? `?${serialized}` : ''}`;
|
||||
};
|
||||
const submitSearch = (query = searchValue) => {
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
if (query.trim() === (search ?? '').trim()) return;
|
||||
void goto(searchHref(query), { replaceState: true, keepFocus: true, noScroll: true });
|
||||
};
|
||||
const scheduleSearch = (query: string) => {
|
||||
draftSearch = query;
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => submitSearch(query), 350);
|
||||
};
|
||||
onDestroy(() => {
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
});
|
||||
const href = (targetPage: number, perPage = pagination.perPage) => {
|
||||
const params = new URLSearchParams(currentPage.url.searchParams);
|
||||
params.set(pageKey, String(targetPage));
|
||||
@@ -38,10 +69,10 @@
|
||||
|
||||
<nav class="registry-pagination" aria-label="Registry pagination">
|
||||
{#if search !== undefined}
|
||||
<form class="pagination-search">
|
||||
<form class="pagination-search" role="search" onsubmit={(event) => { event.preventDefault(); submitSearch(); }}>
|
||||
{#each retainedParams as [key, value]}<input type="hidden" name={key} {value}/>{/each}
|
||||
<input type="search" name={searchKey} value={search} maxlength="80" placeholder={searchPlaceholder} aria-label={searchPlaceholder}/>
|
||||
<button class="secondary">SEARCH</button>
|
||||
<input type="search" name={searchKey} value={searchValue} maxlength="80" placeholder={searchPlaceholder} aria-label={`${searchPlaceholder} — results update as you type`} oninput={(event) => scheduleSearch(event.currentTarget.value)}/>
|
||||
<button class="secondary" aria-busy={searchPending} disabled={searchPending}>{searchPending ? 'SEARCHING…' : 'SEARCH'}</button>
|
||||
{#if search}<a href={clearSearchHref}>CLEAR</a>{/if}
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user