Add live registry search and themed input states

This commit is contained in:
JB
2026-07-25 21:36:05 -04:00
parent c0a8e05e78
commit f0fbdc2853
3 changed files with 54 additions and 4 deletions
+15
View File
@@ -400,6 +400,13 @@ squeezed beside long module headings on mobile.
- Labels are uppercase and letter-spaced. - Labels are uppercase and letter-spaced.
- Inputs use dark surfaces, one-pixel borders, and clear disabled styling. - 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. - Related min/max fields remain visually paired.
- Small mutually exclusive option sets use a compact segmented radio control, - Small mutually exclusive option sets use a compact segmented radio control,
even when visually presented like selectable checkbox cells. Include an 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 History and System Metrics. Badge text must expose an `awaiting review` label to
assistive technology; color supplements that meaning rather than replacing it. 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 Pending records may be used as dependencies in later proposals by any
contributor. Render these choices in the ordinary assignment control with a contributor. Render these choices in the ordinary assignment control with a
restrained orange border/background and the explicit text `PENDING` plus the restrained orange border/background and the explicit text `PENDING` plus the
+4
View File
@@ -10,6 +10,9 @@
--shd-text-dim:#5d6567; --shd-text-dim:#5d6567;
--shd-orange:#f6a821; --shd-orange:#f6a821;
--shd-orange-hover:#ffc45d; --shd-orange-hover:#ffc45d;
--shd-focus-ring:#f6a821;
--shd-selection-bg:#f6a821;
--shd-selection-text:#111415;
--shd-success:#75c08c; --shd-success:#75c08c;
--shd-danger:#df8069; --shd-danger:#df8069;
--shd-info:#70b9d6; --shd-info:#70b9d6;
@@ -29,6 +32,7 @@
--shd-transition:transform .12s ease,filter .18s ease,opacity .18s ease,box-shadow .18s ease; --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} *{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} .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} .site-footer{flex:0 0 auto;margin-top:auto}
.contact-page.contact-page{margin:0 auto} .contact-page.contact-page{margin:0 auto}
+35 -4
View File
@@ -1,5 +1,7 @@
<script lang="ts"> <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 { let {
pagination, pagination,
namespace = '', namespace = '',
@@ -14,6 +16,35 @@
const pageKey = $derived(namespace ? `${namespace}Page` : 'page'); const pageKey = $derived(namespace ? `${namespace}Page` : 'page');
const perPageKey = $derived(namespace ? `${namespace}PerPage` : 'perPage'); const perPageKey = $derived(namespace ? `${namespace}PerPage` : 'perPage');
const searchKey = $derived(namespace ? `${namespace}Q` : 'q'); 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 href = (targetPage: number, perPage = pagination.perPage) => {
const params = new URLSearchParams(currentPage.url.searchParams); const params = new URLSearchParams(currentPage.url.searchParams);
params.set(pageKey, String(targetPage)); params.set(pageKey, String(targetPage));
@@ -38,10 +69,10 @@
<nav class="registry-pagination" aria-label="Registry pagination"> <nav class="registry-pagination" aria-label="Registry pagination">
{#if search !== undefined} {#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} {#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}/> <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">SEARCH</button> <button class="secondary" aria-busy={searchPending} disabled={searchPending}>{searchPending ? 'SEARCHING…' : 'SEARCH'}</button>
{#if search}<a href={clearSearchHref}>CLEAR</a>{/if} {#if search}<a href={clearSearchHref}>CLEAR</a>{/if}
</form> </form>
{/if} {/if}