From 961b40ed0355bbe17b7ef04e0de88a98cf7d1e09 Mon Sep 17 00:00:00 2001
From: David Pine <7679720+IEvangelist@users.noreply.github.com>
Date: Fri, 18 Sep 2026 16:08:35 -0500
Subject: [PATCH 1/6] Fix release hub navigation and unify search recovery UX
Preserve Astro ClientRouter lifecycle and history ownership, restore deferred artwork after navigation, and align search controls and contextual recovery across the hub, API reference and galleries.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.../CatalogSearchActiveFilters.astro | 35 ++
.../src/components/Integrations.astro | 242 +++++----
src/frontend/src/components/SampleGrid.astro | 258 +++++-----
src/frontend/src/components/SessionGrid.astro | 209 +++-----
.../api-reference/ApiSearchBar.astro | 174 +------
.../api-reference/InpageSearch.astro | 196 +-------
.../api-reference/VersionFilter.astro | 31 +-
.../api-reference/inpage-search-sync.ts | 2 +
.../dev-center/BrowseDropdown.astro | 3 +-
.../dev-center/GlossaryBrowser.astro | 37 +-
.../components/dev-center/GlossaryBrowser.ts | 50 +-
.../{DevLayout.astro => HubLayout.astro} | 2 +-
.../dev-center/ResourceBrowser.astro | 49 +-
.../components/dev-center/ResourceBrowser.ts | 87 ++--
.../components/dev-center/empty-results.ts | 20 +
.../components/dev-center/filter-history.ts | 93 ++++
.../components/search/SearchEmptyState.astro | 21 +
.../src/components/search/SearchField.astro | 47 ++
.../components/search/search-empty-state.ts | 59 +++
.../src/components/starlight/Search.astro | 97 +++-
.../src/components/starlight/Sidebar.astro | 274 +++-------
.../docs/community/contributor-guide.mdx | 8 +
src/frontend/src/content/i18n/en.json | 35 ++
src/frontend/src/pages/hub/browse/index.astro | 6 +-
.../src/pages/hub/glossary/[term].astro | 6 +-
.../src/pages/hub/glossary/index.astro | 6 +-
src/frontend/src/pages/hub/index.astro | 8 +-
.../reference/api/_search-presentation.ts | 97 ++++
.../api/csharp/[package]/[type]/index.astro | 47 +-
.../api/csharp/[package]/index.astro | 45 +-
.../pages/reference/api/csharp/index.astro | 72 ++-
.../typescript/[module]/[item]/index.astro | 47 +-
.../api/typescript/[module]/index.astro | 45 +-
.../reference/api/typescript/index.astro | 73 ++-
src/frontend/src/styles/dev-hub.css | 19 +-
src/frontend/src/styles/search-controls.css | 149 ++++++
.../utils/dev-center/resource-card-html.ts | 9 +-
.../tests/e2e/api-search-contract.spec.ts | 302 +++++++++++
src/frontend/tests/e2e/dev-browse.spec.ts | 40 +-
.../tests/e2e/dev-empty-recovery.spec.ts | 148 ++++++
src/frontend/tests/e2e/dev-glossary.spec.ts | 39 +-
src/frontend/tests/e2e/dev-grain.spec.ts | 59 +++
src/frontend/tests/e2e/dev-navigation.spec.ts | 138 +++++
.../tests/e2e/filter-navigation.spec.ts | 2 +-
.../tests/e2e/gallery-search-contract.spec.ts | 472 ++++++++++++++++++
.../tests/e2e/search-visual-parity.spec.ts | 80 +++
src/frontend/tests/e2e/site-search.spec.ts | 149 ++++++
.../typecheck/component-props.contracts.ts | 15 +
.../unit/api-search-lifecycle.vitest.test.ts | 24 +-
.../unit/api-search-recovery.vitest.test.ts | 95 ++++
.../unit/custom-components.vitest.test.ts | 48 +-
.../tests/unit/dev-glossary.vitest.test.ts | 9 +-
.../tests/unit/filter-history.vitest.test.ts | 164 ++++++
.../unit/resource-browser.vitest.test.ts | 10 +-
.../unit/search-presentation.vitest.test.ts | 191 +++++++
.../tests/unit/search-recovery.vitest.test.ts | 32 ++
56 files changed, 3439 insertions(+), 1236 deletions(-)
create mode 100644 src/frontend/src/components/CatalogSearchActiveFilters.astro
rename src/frontend/src/components/dev-center/{DevLayout.astro => HubLayout.astro} (94%)
create mode 100644 src/frontend/src/components/dev-center/empty-results.ts
create mode 100644 src/frontend/src/components/dev-center/filter-history.ts
create mode 100644 src/frontend/src/components/search/SearchEmptyState.astro
create mode 100644 src/frontend/src/components/search/SearchField.astro
create mode 100644 src/frontend/src/components/search/search-empty-state.ts
create mode 100644 src/frontend/src/pages/reference/api/_search-presentation.ts
create mode 100644 src/frontend/src/styles/search-controls.css
create mode 100644 src/frontend/tests/e2e/api-search-contract.spec.ts
create mode 100644 src/frontend/tests/e2e/dev-empty-recovery.spec.ts
create mode 100644 src/frontend/tests/e2e/dev-navigation.spec.ts
create mode 100644 src/frontend/tests/e2e/gallery-search-contract.spec.ts
create mode 100644 src/frontend/tests/e2e/search-visual-parity.spec.ts
create mode 100644 src/frontend/tests/unit/api-search-recovery.vitest.test.ts
create mode 100644 src/frontend/tests/unit/filter-history.vitest.test.ts
create mode 100644 src/frontend/tests/unit/search-presentation.vitest.test.ts
create mode 100644 src/frontend/tests/unit/search-recovery.vitest.test.ts
diff --git a/src/frontend/src/components/CatalogSearchActiveFilters.astro b/src/frontend/src/components/CatalogSearchActiveFilters.astro
new file mode 100644
index 000000000..16d6dd96e
--- /dev/null
+++ b/src/frontend/src/components/CatalogSearchActiveFilters.astro
@@ -0,0 +1,35 @@
+---
+interface Props {
+ label: string;
+}
+const { label } = Astro.props;
+---
+
+
+
+
diff --git a/src/frontend/src/components/Integrations.astro b/src/frontend/src/components/Integrations.astro
index 5fb1b023d..17cba62de 100644
--- a/src/frontend/src/components/Integrations.astro
+++ b/src/frontend/src/components/Integrations.astro
@@ -1,7 +1,10 @@
---
-import { CardGrid, Card } from '@astrojs/starlight/components';
+import { CardGrid } from '@astrojs/starlight/components';
import IntegrationCard from '@components/IntegrationCard.astro';
import IntegrationTotals from '@components/IntegrationTotals.astro';
+import '../styles/search-controls.css';
+import SearchField from '@components/search/SearchField.astro';
+import CatalogSearchActiveFilters from '@components/CatalogSearchActiveFilters.astro';
interface Props {
integrations: Integrations;
@@ -9,6 +12,21 @@ interface Props {
}
const { integrations, availableDocs } = Astro.props;
+const messages = {
+ clearSearch: Astro.locals.t('catalogSearch.clearSearch'),
+ clearFilters: Astro.locals.t('catalogSearch.clearFilters'),
+ resetAll: Astro.locals.t('catalogSearch.resetAll'),
+ count: Astro.locals.t('catalogSearch.integrationsCount'),
+ empty: Astro.locals.t('catalogSearch.integrationsEmpty'),
+ queryEmpty: Astro.locals.t('catalogSearch.integrationsQueryEmpty'),
+ filteredQueryEmpty: Astro.locals.t('catalogSearch.integrationsFilteredQueryEmpty'),
+ queryGuidance: Astro.locals.t('catalogSearch.queryGuidance'),
+ preserveFiltersGuidance: Astro.locals.t('catalogSearch.preserveFiltersGuidance'),
+ preserveQueryGuidance: Astro.locals.t('catalogSearch.preserveQueryGuidance'),
+ filtersGuidance: Astro.locals.t('catalogSearch.filtersGuidance'),
+ resetGuidance: Astro.locals.t('catalogSearch.resetGuidance'),
+ excludedFilter: Astro.locals.t('catalogSearch.excludedFilter'),
+};
interface Integration {
title: string;
@@ -49,35 +67,19 @@ const sortedIntegrations = visibleIntegrations.sort((a, b) => {
-
-
-
-
-
-
- {Astro.locals.t('integrations.clear')}
-
{
{
{
{
}
+
+
+ {messages.clearFilters}
+ {messages.resetAll}
+
+
{
sortedIntegrations.length > 0 && (
@@ -179,21 +190,38 @@ const sortedIntegrations = visibleIntegrations.sort((a, b) => {
-
-
- {Astro.locals.t('integrations.noResults')}
-
+
+
{Astro.locals.t('catalogSearch.integrationsEmpty')}
+
{Astro.locals.t('integrations.noResults')}
+
+
{messages.clearSearch}
)
}
+{sortedIntegrations.length === 0 &&
{Astro.locals.t('catalogSearch.integrationsUnavailable')}
}
-
diff --git a/src/frontend/src/components/SampleGrid.astro b/src/frontend/src/components/SampleGrid.astro
index 8c03c98f2..30c6d9c69 100644
--- a/src/frontend/src/components/SampleGrid.astro
+++ b/src/frontend/src/components/SampleGrid.astro
@@ -1,5 +1,7 @@
---
import SampleCard from '@components/SampleCard.astro';
+import CatalogSearchActiveFilters from '@components/CatalogSearchActiveFilters.astro';
+import '../styles/search-controls.css';
import { tagLabel } from '@utils/sample-tags';
import {
isThemeAwareSampleImage,
@@ -15,6 +17,20 @@ interface Props {
}
const { samples } = Astro.props;
+const messages = {
+ clearSearch: Astro.locals.t('catalogSearch.clearSearch'),
+ clearFilters: Astro.locals.t('catalogSearch.clearFilters'),
+ resetAll: Astro.locals.t('catalogSearch.resetAll'),
+ count: Astro.locals.t('catalogSearch.samplesCount'),
+ empty: Astro.locals.t('catalogSearch.samplesEmpty'),
+ queryEmpty: Astro.locals.t('catalogSearch.samplesQueryEmpty'),
+ filteredQueryEmpty: Astro.locals.t('catalogSearch.samplesFilteredQueryEmpty'),
+ queryGuidance: Astro.locals.t('catalogSearch.queryGuidance'),
+ preserveFiltersGuidance: Astro.locals.t('catalogSearch.preserveFiltersGuidance'),
+ preserveQueryGuidance: Astro.locals.t('catalogSearch.preserveQueryGuidance'),
+ filtersGuidance: Astro.locals.t('catalogSearch.filtersGuidance'),
+ resetGuidance: Astro.locals.t('catalogSearch.resetGuidance'),
+};
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
// Eagerly import all sample images at build time
@@ -62,12 +78,10 @@ const tagCounts = new Map(
-
+
-
+
-
Search samples
+
{Astro.locals.t('catalogSearch.samplesLabel')}
-
+
-
-
+
@@ -110,7 +122,7 @@ const tagCounts = new Map(
chips fill with the accent color so they read as the only "loud" item
in the row. On mobile the row collapses to a two-row max-height with a
"Show all" expander to avoid the overwhelming 28-tag wall. -->
-
+
-
-
-
-
-
-
-
No samples match your filters.
-
Try removing a filter or adjusting your search.
-
Reset all filters
+
+
{Astro.locals.t(samples.length ? 'catalogSearch.samplesEmpty' : 'catalogSearch.samplesUnavailable')}
+ {samples.length > 0 &&
{Astro.locals.t('catalogSearch.guidance')}
}
+
+
{messages.resetAll}
-
diff --git a/src/frontend/src/components/api-reference/ApiSearchBar.astro b/src/frontend/src/components/api-reference/ApiSearchBar.astro
index bbb6c0426..673ae99b5 100644
--- a/src/frontend/src/components/api-reference/ApiSearchBar.astro
+++ b/src/frontend/src/components/api-reference/ApiSearchBar.astro
@@ -3,9 +3,10 @@
* ApiSearchBar — unified search bar with kind filters, version dropdown,
* stats line, no-results state, and search results container.
*
- * Composes InpageSearch and VersionFilter into a single coordinated UI.
+ * Composes SearchField and VersionFilter into a single coordinated UI.
* On tablet/desktop the version dropdown sits to the right of the search input.
- * On mobile they stack. The "Clear filters" button resets both kind and version filters.
+ * On mobile they stack. Clear search affects only the query; Clear filters
+ * resets kinds and versions. Reset all clears both without changing sort preferences.
*
* Usage:
* ```astro
@@ -19,13 +20,16 @@
* />
* ```
*/
-import VersionFilter from './VersionFilter.astro';
+import VersionFilter from '@components/api-reference/VersionFilter.astro';
+import SearchField from '@components/search/SearchField.astro';
interface Props {
/** Unique prefix for element IDs (e.g. "api" or "ts-api") */
id: string;
/** Placeholder text for the search input */
placeholder: string;
+ /** Stable accessible name; defaults to the contextual placeholder. */
+ label?: string;
/** Unique kind strings to render as filter chips */
kinds: string[];
/** Version strings for the version dropdown (empty array to hide) */
@@ -36,7 +40,7 @@ interface Props {
size?: 'lg' | 'sm';
}
-const { id, placeholder, kinds, versions = [], defaultStatsText, size = 'lg' } = Astro.props;
+const { id, placeholder, label = placeholder, kinds, versions = [], defaultStatsText, size = 'lg' } = Astro.props;
const hasVersions = versions.length > 0;
const searchInputId = id + '-search-input';
const searchStatusId = id + '-search-status';
@@ -51,33 +55,10 @@ const getKindClassName = (kind: string) => 'api-filter-dot kind-' + kind.replace
-
-
-
-
-
- {placeholder}
-
-
-
-
-
-
-
-
+
{hasVersions && (
@@ -90,7 +71,7 @@ const getKindClassName = (kind: string) => 'api-filter-dot kind-' + kind.replace
{kinds.map(kind => (
'api-filter-dot kind-' + kind.replace
-
{defaultStatsText}
-
Clear filters
+
{defaultStatsText}
+
+ Clear filters
+ Reset all
+
@@ -135,89 +119,7 @@ const getKindClassName = (kind: string) => 'api-filter-dot kind-' + kind.replace
margin-bottom: 0.25rem;
}
- .asb-input-wrap {
- position: relative;
- display: flex;
- align-items: center;
- flex: 1;
- min-width: 0;
- }
-
- .asb-search-icon {
- position: absolute;
- left: 0.75rem;
- width: 1.25rem;
- height: 1.25rem;
- color: var(--sl-color-gray-1);
- pointer-events: none;
- z-index: 1;
- }
-
- .asb-search-input {
- width: 100%;
- padding: 0.5rem 2.5rem 0.5rem 2.5rem;
- border: 1px solid var(--sl-color-gray-5);
- border-radius: 0.625rem;
- background: var(--sl-color-bg-nav);
- color: var(--sl-color-text);
- font-size: var(--sl-text-sm);
- font-family: inherit;
- outline: none;
- transition: border-color 0.15s;
- box-sizing: border-box;
- }
-
- .asb-search-input::placeholder {
- color: var(--sl-color-gray-1);
- }
-
- .asb-search-input::-webkit-search-cancel-button {
- -webkit-appearance: none;
- appearance: none;
- }
-
- .asb-search-input:focus {
- border-color: var(--sl-color-accent);
- box-shadow: 0 0 0 1px var(--sl-color-accent);
- }
-
- [data-size="sm"] .asb-search-input {
- padding: 0.4375rem 2.25rem 0.4375rem 2.25rem;
- font-size: var(--sl-text-sm);
- border-radius: 0.5rem;
- }
-
- [data-size="sm"] .asb-search-icon {
- width: 1rem;
- height: 1rem;
- left: 0.625rem;
- }
-
- .asb-search-clear {
- position: absolute;
- right: 0.5rem;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 1.5rem;
- height: 1.5rem;
- border: none;
- background: none;
- color: var(--sl-color-gray-3);
- cursor: pointer;
- border-radius: 50%;
- padding: 0;
- }
-
- .asb-search-clear svg {
- width: 1rem;
- height: 1rem;
- }
-
- .asb-search-clear:hover {
- color: var(--sl-color-text);
- background: var(--sl-color-gray-6);
- }
+ .inpage-search-actions { display: flex; flex-wrap: wrap; gap: 0.25rem; }
/* Version slot aligns with the search bar */
.asb-version-slot {
@@ -249,6 +151,7 @@ const getKindClassName = (kind: string) => 'api-filter-dot kind-' + kind.replace
}
.api-search-bar-unified .api-filter-chip {
+ min-height: 2.25rem;
display: inline-flex;
align-items: center;
gap: 0.35rem;
@@ -271,6 +174,11 @@ const getKindClassName = (kind: string) => 'api-filter-dot kind-' + kind.replace
background: var(--sl-color-gray-6);
}
+ .api-search-bar-unified .api-filter-chip:focus-visible {
+ outline: 2px solid var(--search-control-focus);
+ outline-offset: 2px;
+ }
+
.api-search-bar-unified .api-filter-chip.active {
border-color: currentColor;
background: color-mix(in srgb, currentColor 12%, transparent);
@@ -323,27 +231,11 @@ const getKindClassName = (kind: string) => 'api-filter-dot kind-' + kind.replace
/* ── Stats line ─────────────────────────────────────────────── */
.api-search-bar-unified .inpage-search-stats {
display: flex;
+ flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.4rem 0 0.15rem;
- font-size: var(--api-text-sm, 0.875rem);
- color: var(--sl-color-gray-3);
- }
-
- .api-search-bar-unified .inpage-clear-filters {
- background: none;
- border: none;
- color: var(--sl-color-text-accent);
- font-size: 0.82rem;
- cursor: pointer;
- padding: 0;
- text-decoration: underline;
- white-space: nowrap;
- }
-
- .api-search-bar-unified .inpage-clear-filters:hover {
- color: var(--sl-color-white);
}
/* ── Search result items (dynamically injected by controllers) ── */
@@ -452,20 +344,6 @@ const getKindClassName = (kind: string) => 'api-filter-dot kind-' + kind.replace
text-decoration-thickness: 2px;
}
- .api-search-bar-unified .api-search-empty {
- padding: 2.5rem 1.5rem;
- text-align: center;
- background: var(--api-surface, var(--sl-color-gray-6));
- border-radius: var(--api-radius-lg, 0.75rem);
- color: var(--sl-color-gray-2);
- }
-
- .api-search-bar-unified .api-search-empty-hint {
- margin-top: 0.5rem;
- font-size: var(--api-text-sm, 0.875rem);
- color: var(--sl-color-gray-3);
- }
-
.api-search-bar-unified .api-load-more {
display: block;
width: 100%;
diff --git a/src/frontend/src/components/api-reference/InpageSearch.astro b/src/frontend/src/components/api-reference/InpageSearch.astro
index 1ca6d31da..413cb606f 100644
--- a/src/frontend/src/components/api-reference/InpageSearch.astro
+++ b/src/frontend/src/components/api-reference/InpageSearch.astro
@@ -10,6 +10,7 @@
* Each page still provides its own `
-
+
@@ -87,13 +92,14 @@ const groups = [
-
{resources.length} resources
- Clear all
+ {resources.length} resources
+ Clear filters
+ Reset all
-
-
No matching resources
-
Try a broader search or remove a filter.
-
Show all resources
+
+
{resources.length ? 'No matching resources' : 'No resources available'}
+
{resources.length ? 'Try a broader search or remove a filter.' : 'Resources will appear here when they are available.'}
+
Reset all