@sarimarcus/content-sites-ui 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ ---
2
+ /**
3
+ * ListingShell — the frame of a listing hub: breadcrumb band, hero, optional full-width bands, <main> with a
4
+ * Container, and trailing bands, all inside one Pagefind-ignore wrapper (listings are not indexed). The route keeps
5
+ * Layout and passes StructuredData to Layout directly, so its JSON-LD lands in <head>; everything else comes in by slot.
6
+ */
7
+ import BreadcrumbBand from './BreadcrumbBand.astro';
8
+ import Container from './Container.astro';
9
+
10
+ interface Props {
11
+ mainClass?: string;
12
+ }
13
+
14
+ const { mainClass = 'py-12 lg:py-20' } = Astro.props;
15
+ ---
16
+
17
+ <div data-pagefind-ignore>
18
+ <BreadcrumbBand ignore={false}>
19
+ <slot name="breadcrumb" />
20
+ </BreadcrumbBand>
21
+
22
+ <slot name="hero" />
23
+ <slot name="before-main" />
24
+
25
+ <main class={mainClass}>
26
+ <Container>
27
+ <slot />
28
+ </Container>
29
+ </main>
30
+
31
+ <slot name="after-main" />
32
+ </div>
@@ -1,6 +1,7 @@
1
1
  // Public surface of @sarimarcus/content-sites-ui/layout.
2
2
  export { default as Container } from './Container.astro';
3
3
  export { default as BreadcrumbBand } from './BreadcrumbBand.astro';
4
+ export { default as ListingShell } from './ListingShell.astro';
4
5
  export { default as AsyncStylesheet } from './AsyncStylesheet.astro';
5
6
  export { default as DocumentShell } from './DocumentShell.astro';
6
7
  export { default as ConsentAnalytics } from './ConsentAnalytics.astro';
@@ -0,0 +1,222 @@
1
+ ---
2
+ /**
3
+ * SearchPage — the markup of a site search page over its Pagefind index: hero and search box, type chips, an optional
4
+ * audience row (Pagefind `audience` filter values), the result states and the type-icon template the result cards
5
+ * clone. The route keeps Layout (`noindex`), imports `@sarimarcus/content-sites-ui/search.css` and calls
6
+ * `initSearchPage` from `@sarimarcus/content-sites-ui/search-page` in its own <script> with the same taxonomy and copy.
7
+ */
8
+ import Container from '../layout/Container.astro';
9
+ import Icon from '../primitives/Icon.astro';
10
+ import SearchSuggestions from './SearchSuggestions.astro';
11
+ import type { SearchPageStrings, SearchPageType } from './searchPage.ts';
12
+
13
+ interface Props {
14
+ strings: SearchPageStrings;
15
+ /** Insertion order is the chip order. */
16
+ types: Record<string, SearchPageType>;
17
+ suggestions: { label: string; query: string }[];
18
+ fallbackLinks: { label: string; href: string }[];
19
+ /** Renders the audience row when set (needs the audience strings). */
20
+ audienceFacets?: { value: string; label: string; count: number }[];
21
+ }
22
+
23
+ const { strings, types, suggestions, fallbackLinks, audienceFacets } = Astro.props;
24
+ const typeOrder = Object.keys(types);
25
+ ---
26
+
27
+ <main class="search-page" data-pagefind-ignore>
28
+ <section class="search-hero" aria-labelledby="search-hero-heading">
29
+ <Container>
30
+ <p class="search-hero__eyebrow">{strings.heroEyebrow}</p>
31
+ <h1 id="search-hero-heading" class="search-hero__heading">
32
+ {strings.heroHeading}
33
+ </h1>
34
+ <p class="search-hero__subhead">{strings.heroSubhead}</p>
35
+
36
+ <form class="search-form" role="search" autocomplete="off" novalidate>
37
+ <span class="search-form__icon" aria-hidden="true">
38
+ <Icon name="search" class="w-5 h-5" />
39
+ </span>
40
+ <label class="sr-only" for="search-input">{strings.inputLabel}</label>
41
+ <input
42
+ id="search-input"
43
+ type="search"
44
+ class="search-form__input"
45
+ placeholder={strings.inputPlaceholder}
46
+ spellcheck="false"
47
+ autocapitalize="off"
48
+ autocorrect="off"
49
+ enterkeyhint="search"
50
+ aria-controls="search-results-region"
51
+ aria-describedby="search-announce"
52
+ />
53
+ <div id="search-announce" class="sr-only" role="status" aria-live="polite" aria-atomic="true"></div>
54
+ <button
55
+ type="button"
56
+ class="search-form__clear"
57
+ aria-label={strings.clearLabel}
58
+ data-search-clear
59
+ hidden
60
+ >
61
+ <Icon name="x" class="w-4 h-4" />
62
+ </button>
63
+ </form>
64
+
65
+ <div
66
+ class="search-filters"
67
+ role="group"
68
+ aria-label={strings.filterGroupLabel}
69
+ hidden
70
+ >
71
+ <button
72
+ type="button"
73
+ class="search-chip search-chip--active"
74
+ data-search-filter=""
75
+ aria-pressed="true"
76
+ >
77
+ {strings.filterAllLabel}
78
+ </button>
79
+ {
80
+ typeOrder.map((t) => (
81
+ <button
82
+ type="button"
83
+ class="search-chip"
84
+ data-search-filter={t}
85
+ aria-pressed="false"
86
+ >
87
+ <Icon name={types[t].iconName} class="search-chip__icon" />
88
+ <span>{types[t].pluralLabel}</span>
89
+ </button>
90
+ ))
91
+ }
92
+ </div>
93
+
94
+ {audienceFacets && (
95
+ <div
96
+ class="search-filters search-filters--audience"
97
+ role="group"
98
+ aria-label={strings.audienceGroupLabel}
99
+ hidden
100
+ >
101
+ <span class="search-filters__label" aria-hidden="true">{strings.audienceRowLabel}</span>
102
+ <button
103
+ type="button"
104
+ class="search-chip search-chip--active"
105
+ data-search-audience=""
106
+ aria-pressed="true"
107
+ >
108
+ {strings.audienceAllLabel}
109
+ </button>
110
+ {
111
+ audienceFacets.map((f) => (
112
+ <button
113
+ type="button"
114
+ class="search-chip"
115
+ data-search-audience={f.value}
116
+ data-search-audience-total={f.count}
117
+ aria-pressed="false"
118
+ >
119
+ <span>{f.label}</span>
120
+ <span class="search-chip__count">{f.count}</span>
121
+ </button>
122
+ ))
123
+ }
124
+ </div>
125
+ )}
126
+ </Container>
127
+ </section>
128
+
129
+ {/* Hidden icon template: result cards clone an icon node by type. */}
130
+ <template id="search-type-icons">
131
+ {
132
+ typeOrder.map((t) => (
133
+ <span data-icon-for={t}>
134
+ <Icon name={types[t].iconName} class="search-card__badge-icon" />
135
+ </span>
136
+ ))
137
+ }
138
+ </template>
139
+
140
+ <section
141
+ id="search-results-region"
142
+ class="search-results-region"
143
+ aria-busy="false"
144
+ >
145
+ <Container>
146
+ <div class="search-state" data-search-state="empty">
147
+ <SearchSuggestions label={strings.emptySuggestionsLabel} suggestions={suggestions} />
148
+ </div>
149
+
150
+ <div class="search-state" data-search-state="loading" hidden>
151
+ <p class="search-state__loading">
152
+ <span class="search-state__spinner" aria-hidden="true"></span>
153
+ {strings.loadingLabel}
154
+ </p>
155
+ </div>
156
+
157
+ <div class="search-state" data-search-state="results" hidden>
158
+ <h2 class="search-results__heading" data-search-results-heading></h2>
159
+ {/* Only the entity templates and articles carry an `audience` filter value,
160
+ so an audience-filtered search cannot return guides, /discover/ pages,
161
+ neighborhoods or walks — they have no audience tags to match. Rather
162
+ than drop them silently, say how many the filter removed. */}
163
+ <p class="search-filter-note" data-search-filter-note hidden></p>
164
+ <ul class="search-results__list" data-search-results-list></ul>
165
+ <div class="search-results__actions">
166
+ <button
167
+ type="button"
168
+ class="search-load-more"
169
+ data-search-load-more
170
+ hidden
171
+ >
172
+ {strings.loadMoreLabel}
173
+ </button>
174
+ </div>
175
+ </div>
176
+
177
+ <div class="search-state" data-search-state="zero" hidden>
178
+ <h2 class="search-state__heading" data-search-zero-heading></h2>
179
+ <p class="search-state__subhead">{strings.zeroSubhead}</p>
180
+ <SearchSuggestions label={strings.emptySuggestionsLabel} suggestions={suggestions} />
181
+ <p class="search-fallback__label">{strings.zeroFallbackLabel}</p>
182
+ <ul class="search-fallback__links">
183
+ {
184
+ fallbackLinks.map((l) => (
185
+ <li>
186
+ <a href={l.href} class="search-fallback__link">{l.label}</a>
187
+ </li>
188
+ ))
189
+ }
190
+ </ul>
191
+ </div>
192
+
193
+ <div class="search-state" data-search-state="filter-empty" hidden>
194
+ <h2 class="search-state__heading" data-search-filter-empty-heading>{strings.filterEmptyHeading}</h2>
195
+ <button
196
+ type="button"
197
+ class="search-chip search-chip--suggest"
198
+ data-search-clear-filter
199
+ >
200
+ {strings.filterEmptyClearLabel}
201
+ </button>
202
+ </div>
203
+
204
+ <div class="search-state" data-search-state="error" hidden>
205
+ <h2 class="search-state__heading">{strings.errorHeading}</h2>
206
+ <p class="search-state__subhead">{strings.errorSubhead}</p>
207
+ <button type="button" class="search-load-more" data-search-retry>
208
+ {strings.errorRetryLabel}
209
+ </button>
210
+ <ul class="search-fallback__links" style="margin-top: 1.5rem">
211
+ {
212
+ fallbackLinks.map((l) => (
213
+ <li>
214
+ <a href={l.href} class="search-fallback__link">{l.label}</a>
215
+ </li>
216
+ ))
217
+ }
218
+ </ul>
219
+ </div>
220
+ </Container>
221
+ </section>
222
+ </main>
@@ -1,5 +1,7 @@
1
1
  // Public surface of @sarimarcus/content-sites-ui/search: search-page building blocks. The site keeps its page, its
2
2
  // type taxonomy, its copy and its Pagefind index.
3
3
  export { default as SearchSuggestions } from './SearchSuggestions.astro';
4
+ export { default as SearchPage } from './SearchPage.astro';
5
+ export type { SearchPageStrings, SearchPageType } from './searchPage.ts';
4
6
  export { appendResults } from './renderResult.ts';
5
7
  export type { PagefindResultFragment, SearchTaxonomy, SearchTypeMeta } from './renderResult.ts';