@sarimarcus/content-sites-ui 0.16.0 → 0.18.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.
@@ -1,4 +1,11 @@
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';
6
+ export { default as DocumentShell } from './DocumentShell.astro';
7
+ export { default as ConsentAnalytics } from './ConsentAnalytics.astro';
8
+ export type { ConsentType } from './ConsentAnalytics.astro';
9
+ export { shellResources, GTM_ORIGIN, SILKTIDE_BASE, SILKTIDE_STYLESHEET } from './resources.ts';
10
+ export { track, pageTypeOf, setupDelegatedTracking, setupFaqTracking, setupScrollDepthTracking, setupAnalytics } from './analytics.ts';
11
+ export type { AnalyticsOptions } from './analytics.ts';
@@ -0,0 +1,19 @@
1
+ // The third-party hosts the document shell loads, per CSP directive. Every site renders the shell, so every site's
2
+ // Content-Security-Policy must allow these (scripts/validate-csp-coverage.mjs checks each site's _headers against this
3
+ // list). Plain data and erasable types only: the gate imports this file under Node.
4
+
5
+ import type { CspDirective } from '../affiliate/resources.ts';
6
+
7
+ /** Google Tag Manager: gtm.js and, for visitors without JavaScript, the ns.html iframe. */
8
+ export const GTM_ORIGIN = 'https://www.googletagmanager.com';
9
+ /** Silktide Consent Manager, pinned. */
10
+ export const SILKTIDE_BASE = 'https://cdn.jsdelivr.net/gh/silktide/consent-manager@v2.0.0';
11
+ /** The consent manager's stylesheet; the CSP gate recognises a shell page by it. */
12
+ export const SILKTIDE_STYLESHEET = `${SILKTIDE_BASE}/silktide-consent-manager.css`;
13
+ const SILKTIDE_ORIGIN = new URL(SILKTIDE_BASE).origin;
14
+
15
+ export const shellResources: Partial<Record<CspDirective, readonly string[]>> = {
16
+ 'script-src': [GTM_ORIGIN, SILKTIDE_ORIGIN],
17
+ 'style-src': [SILKTIDE_ORIGIN],
18
+ 'frame-src': [GTM_ORIGIN],
19
+ };
@@ -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';