@salesforcedevs/dx-components 1.32.0-alpha.9 → 1.32.0-docs-content-type-1

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.
Files changed (32) hide show
  1. package/lwc.config.json +2 -1
  2. package/package.json +47 -47
  3. package/src/modules/dx/agentMiawUi/agentMiawUi.css +4 -0
  4. package/src/modules/dx/agentMiawUi/agentMiawUi.html +3 -0
  5. package/src/modules/dx/agentMiawUi/agentMiawUi.ts +253 -0
  6. package/src/modules/dx/button/button.css +5 -0
  7. package/src/modules/dx/codeBlock/codeBlock.css +7 -0
  8. package/src/modules/dx/dropdownOption/dropdownOption.css +8 -13
  9. package/src/modules/dx/footer/footer.ts +55 -32
  10. package/src/modules/dx/globalHeader/globalHeader.html +2 -2
  11. package/src/modules/dx/globalHeader/globalHeader.ts +40 -30
  12. package/src/modules/dx/searchHybrid/searchHybrid.css +165 -0
  13. package/src/modules/dx/searchHybrid/searchHybrid.html +136 -0
  14. package/src/modules/dx/searchHybrid/searchHybrid.ts +306 -0
  15. package/src/modules/dx/searchResults/searchResults.css +6 -107
  16. package/src/modules/dx/searchResults/searchResults.html +91 -87
  17. package/src/modules/dx/searchResults/searchResults.ts +477 -174
  18. package/src/modules/dx/sidebar/sidebar.css +0 -1
  19. package/src/modules/dx/sidebar/sidebar.html +5 -1
  20. package/src/modules/dx/sidebar/sidebar.ts +29 -44
  21. package/src/modules/dx/sidebarOld/sidebarOld.html +0 -1
  22. package/src/modules/dx/sidebarSearch/sidebarSearch.ts +343 -159
  23. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.css +24 -51
  24. package/src/modules/dx/sidebarSearchResult/sidebarSearchResult.ts +4 -12
  25. package/src/modules/dx/tree/treeHandler.ts +2 -1
  26. package/src/modules/dx/treeTile/treeTile.css +4 -0
  27. package/src/modules/dx/treeTile/treeTile.html +10 -0
  28. package/src/modules/dx/treeTile/treeTile.ts +4 -0
  29. package/src/modules/dxUtils/async/async.ts +32 -0
  30. package/src/modules/dxUtils/searchLocalePrefs/searchLocalePrefs.ts +193 -0
  31. package/LICENSE +0 -12
  32. package/src/modules/dxUtils/data360Search/data360Search.ts +0 -168
@@ -1,31 +1,59 @@
1
1
  import kebabCase from "lodash.kebabcase";
2
2
  import { LightningElement, api } from "lwc";
3
3
 
4
- const defaultDomain = "https://developer.salesforce.com";
4
+ const defaultDomain = ""; // empty domain means the header settings will be loaded from the same domain as the current page
5
5
  const defaultLocale = "en-us";
6
6
  const defaultHeaderSettingsBasePath = "/c/public/header-settings";
7
- const headerSettingsJsonKeys = ['regionSelectorOverride', 'contactLinksOverride'];
7
+ const defaultPropertyTitle = "Developers";
8
+ const headerSettingsJsonKeys = [
9
+ "regionSelectorOverride",
10
+ "contactLinksOverride"
11
+ ];
8
12
 
9
13
  export default class DevExNavigation extends LightningElement {
14
+ static renderMode = "light"; // Light DOM currently required due to other elements reading from this element's DOM; we should fix that and use existing nav CSS variables in those other elements (TODO)
15
+
10
16
  @api locale: string = defaultLocale;
11
17
  @api path: string = defaultHeaderSettingsBasePath;
12
18
  @api domain: string = defaultDomain;
19
+ @api propertyTitle: string = defaultPropertyTitle; // used only as a fallback in the "barebones" nav
20
+
21
+ // Fallback basic nav config used when the header settings are not available
22
+ private get barebonesNavConfig() {
23
+ return {
24
+ headerSettings: {
25
+ origin: "",
26
+ contextNavEnabled: "true"
27
+ },
28
+ navItems: {
29
+ variation: "static",
30
+ propertyTitle: {
31
+ label: this.propertyTitle,
32
+ url: "/"
33
+ },
34
+ menuGroup: {}
35
+ }
36
+ };
37
+ }
13
38
 
14
39
  async connectedCallback(): Promise<void> {
40
+ let navConfig = null;
15
41
  try {
16
- const headerSettingsResponse = await fetch(`${this.domain}${this.path}/${this.locale}.json`, {
17
- headers: {
18
- "Content-Type": "application/json",
19
- },
20
- });
42
+ const headerSettingsResponse = await fetch(
43
+ `${this.domain}${this.path}/${this.locale}.json`,
44
+ {
45
+ headers: {
46
+ "Content-Type": "application/json"
47
+ }
48
+ }
49
+ );
21
50
  if (headerSettingsResponse.ok) {
22
- this.createFullNav(await headerSettingsResponse.json());
23
- } else {
24
- this.createBarebonesNav();
51
+ navConfig = await headerSettingsResponse.json();
25
52
  }
26
53
  } catch (ex) {
27
- console.error(`Navigation error: ${ex}`);
28
- this.createBarebonesNav();
54
+ console.error(`Header settings error: ${ex}`);
55
+ } finally {
56
+ this.createFullNav(navConfig || this.barebonesNavConfig);
29
57
  }
30
58
  }
31
59
 
@@ -55,22 +83,4 @@ export default class DevExNavigation extends LightningElement {
55
83
  containerEl.appendChild(hgfNav);
56
84
  containerEl.appendChild(hgfNavContext);
57
85
  }
58
-
59
- private createBarebonesNav(): void {
60
- const hgfNav = this.createGlobalNav({
61
- origin: "",
62
- contextNavEnabled: "true",
63
- });
64
- const hgfNavContext = this.createContextNav({
65
- variation: "static",
66
- propertyTitle: {
67
- label: "Developers",
68
- url: "/",
69
- },
70
- menuGroup: {},
71
- });
72
- const containerEl = this.refs.globalNavContainer as Element;
73
- containerEl.appendChild(hgfNav);
74
- containerEl.appendChild(hgfNavContext);
75
- }
76
86
  }
@@ -0,0 +1,165 @@
1
+ :host {
2
+ display: block;
3
+ font-family: var(--dx-g-font-sans);
4
+ }
5
+
6
+ .dx-search-hybrid {
7
+ background-color: #fafaf9;
8
+ }
9
+
10
+ /* Header --------------------------------------------------------------- */
11
+ .dx-search-header {
12
+ padding: var(--dx-g-spacing-xl);
13
+ background: linear-gradient(
14
+ 77deg,
15
+ var(--dx-g-indigo-vibrant-30) 40.1%,
16
+ var(--dx-g-indigo-vibrant-40) 99.07%
17
+ );
18
+ color: white;
19
+ }
20
+
21
+ .dx-search-header-container {
22
+ margin: 0 auto;
23
+ max-width: 1200px;
24
+ }
25
+
26
+ .dx-search-header-title {
27
+ font-size: var(--dx-g-text-lg);
28
+ line-height: var(--dx-g-text-lg);
29
+ font-weight: var(--dx-g-font-normal);
30
+ margin-bottom: var(--dx-g-spacing-md);
31
+ }
32
+
33
+ .dx-search-header-title-results {
34
+ font-weight: var(--dx-g-font-bold);
35
+ }
36
+
37
+ .dx-search-header-query {
38
+ font-family: var(--dx-g-font-display);
39
+ font-size: var(--dx-g-text-2xl);
40
+ line-height: var(--dx-g-text-2xl);
41
+ font-weight: var(--dx-g-font-demi);
42
+ margin-top: var(--dx-g-spacing-md);
43
+ }
44
+
45
+ /* Body + toolbar ------------------------------------------------------- */
46
+ .dx-search-body {
47
+ margin: 0 auto;
48
+ max-width: 1200px;
49
+ padding: var(--dx-g-spacing-xl);
50
+ }
51
+
52
+ .dx-search-toolbar {
53
+ display: flex;
54
+ flex-wrap: wrap;
55
+ align-items: flex-end;
56
+ justify-content: space-between;
57
+ gap: var(--dx-g-spacing-md);
58
+ margin-bottom: var(--dx-g-spacing-lg);
59
+ }
60
+
61
+ .dx-facet-row {
62
+ display: flex;
63
+ flex-wrap: wrap;
64
+ gap: var(--dx-g-spacing-sm);
65
+ }
66
+
67
+ .dx-facet-chip {
68
+ background-color: white;
69
+ color: var(--dx-g-blue-vibrant-20);
70
+ border: 1px solid var(--dx-g-blue-vibrant-50);
71
+ border-radius: 500px;
72
+ padding: 6px 14px;
73
+ font-size: var(--dx-g-text-sm);
74
+ cursor: pointer;
75
+ }
76
+
77
+ .dx-facet-chip:hover {
78
+ background-color: var(--dx-g-blue-vibrant-95);
79
+ }
80
+
81
+ .dx-facet-chip_active {
82
+ background-color: var(--dx-g-blue-vibrant-50);
83
+ color: white;
84
+ font-weight: var(--dx-g-font-demi);
85
+ }
86
+
87
+ .dx-facet-chip-count {
88
+ margin-left: 6px;
89
+ opacity: 0.75;
90
+ }
91
+
92
+ .dx-search-controls {
93
+ display: flex;
94
+ gap: var(--dx-g-spacing-md);
95
+ }
96
+
97
+ /* Status states -------------------------------------------------------- */
98
+ .dx-search-status {
99
+ display: flex;
100
+ justify-content: center;
101
+ padding: var(--dx-g-spacing-2xl) 0;
102
+ }
103
+
104
+ .dx-search-error {
105
+ color: var(--dx-g-red-vibrant-40, #ba0517);
106
+ }
107
+
108
+ /* Result cards (ported from dx/searchResults visual style) ------------- */
109
+ .dx-result-list {
110
+ list-style: none;
111
+ margin: 0;
112
+ padding: 0;
113
+ }
114
+
115
+ .dx-result {
116
+ background: white;
117
+ border-radius: 16px;
118
+ border: 1px solid rgb(24 24 24 / 4%);
119
+ box-shadow: 0 0 2px 0 rgb(24 24 24 / 8%), 0 2px 4px 1px rgb(24 24 24 / 16%);
120
+ padding: 20px;
121
+ margin-bottom: var(--dx-g-spacing-lg);
122
+ }
123
+
124
+ .dx-result-info {
125
+ display: flex;
126
+ gap: 12px;
127
+ margin-bottom: var(--dx-g-spacing-smd);
128
+ }
129
+
130
+ .dx-badge {
131
+ display: inline-flex;
132
+ align-items: center;
133
+ border-radius: 4px;
134
+ padding: 2px 8px;
135
+ font-size: var(--dx-g-text-xs);
136
+ background-color: var(--dx-g-blue-vibrant-95);
137
+ color: var(--dx-g-blue-vibrant-20);
138
+ }
139
+
140
+ .dx-result-title {
141
+ color: var(--dx-g-blue-vibrant-20);
142
+ display: block;
143
+ font-family: var(--dx-g-font-display);
144
+ font-size: var(--dx-g-text-lg);
145
+ margin: 0 0 var(--dx-g-spacing-sm);
146
+ }
147
+
148
+ .dx-result-date {
149
+ color: #555;
150
+ font-size: var(--dx-g-text-xs);
151
+ margin: 0 0 var(--dx-g-spacing-xs);
152
+ }
153
+
154
+ .dx-result-excerpt {
155
+ color: var(--dx-g-gray-10);
156
+ font-size: 14px;
157
+ line-height: 20px;
158
+ margin: 0;
159
+ }
160
+
161
+ /* Pagination ----------------------------------------------------------- */
162
+ .dx-pagination-container {
163
+ margin: 0 auto;
164
+ width: fit-content;
165
+ }
@@ -0,0 +1,136 @@
1
+ <template>
2
+ <div class="dx-search-hybrid">
3
+ <!-- Header: result count + query (?q-driven; no input box per D15.3) -->
4
+ <div if:true={hasQuery} class="dx-search-header">
5
+ <div class="dx-search-header-container">
6
+ <p class="dx-search-header-title">
7
+ <span class="dx-search-header-title-results">{total}</span>
8
+ &nbsp;results for
9
+ </p>
10
+ <p class="dx-search-header-query">&ldquo;{query}&rdquo;</p>
11
+ </div>
12
+ </div>
13
+
14
+ <div class="dx-search-body">
15
+ <!-- Toolbar: content-type facets + sort + locale selector -->
16
+ <div class="dx-search-toolbar">
17
+ <div
18
+ if:true={hasFacets}
19
+ class="dx-facet-row"
20
+ role="group"
21
+ aria-label="Filter by content type"
22
+ >
23
+ <template for:each={facetChips} for:item="chip">
24
+ <button
25
+ key={chip.value}
26
+ type="button"
27
+ class={chip.cssClass}
28
+ data-value={chip.value}
29
+ aria-pressed={chip.active}
30
+ onclick={handleFacetClick}
31
+ >
32
+ {chip.label}
33
+ <span class="dx-facet-chip-count">
34
+ {chip.count}
35
+ </span>
36
+ </button>
37
+ </template>
38
+ </div>
39
+
40
+ <div class="dx-search-controls">
41
+ <div if:true={showLocaleSelector} class="dx-search-locale">
42
+ <dx-select
43
+ label="Language"
44
+ options={localeSelectOptions}
45
+ value={localeValue}
46
+ onchange={handleLocaleChange}
47
+ ></dx-select>
48
+ </div>
49
+ <div class="dx-search-sort">
50
+ <dx-select
51
+ label="Sort by"
52
+ options={sortOptions}
53
+ value={sortValue}
54
+ onchange={handleSortChange}
55
+ ></dx-select>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <!-- Loading -->
61
+ <template if:true={isLoading}>
62
+ <div class="dx-search-status" aria-live="polite">
63
+ <dx-spinner size="medium" variant="base"></dx-spinner>
64
+ </div>
65
+ </template>
66
+
67
+ <!-- Idle prompt (no query yet) -->
68
+ <template if:true={isIdle}>
69
+ <div class="dx-search-status" aria-live="polite">
70
+ <dx-empty-state
71
+ header="Search the developer site"
72
+ subtitle="Enter a search term to get started."
73
+ variant="alt"
74
+ ></dx-empty-state>
75
+ </div>
76
+ </template>
77
+
78
+ <!-- Empty (query ran, no results) -->
79
+ <template if:true={isEmpty}>
80
+ <div class="dx-search-status" aria-live="polite">
81
+ <dx-empty-state
82
+ header="No results found"
83
+ subtitle="Try different keywords or check your spelling."
84
+ variant="alt"
85
+ ></dx-empty-state>
86
+ </div>
87
+ </template>
88
+
89
+ <!-- Error -->
90
+ <template if:true={isError}>
91
+ <div class="dx-search-status dx-search-error" role="alert">
92
+ <p>{errorMessage}</p>
93
+ </div>
94
+ </template>
95
+
96
+ <!-- Results -->
97
+ <template if:true={hasResults}>
98
+ <ul class="dx-result-list">
99
+ <template for:each={displayResults} for:item="result">
100
+ <li key={result.key} class="dx-result">
101
+ <div
102
+ if:true={result.hasBadge}
103
+ class="dx-result-info"
104
+ >
105
+ <span class="dx-badge">
106
+ {result.contentTypeLabel}
107
+ </span>
108
+ </div>
109
+ <a href={result.url} class="dx-result-title">
110
+ {result.title}
111
+ </a>
112
+ <p if:true={result.hasDate} class="dx-result-date">
113
+ {result.displayDate}
114
+ </p>
115
+ <p
116
+ if:true={result.hasDescription}
117
+ class="dx-result-excerpt"
118
+ >
119
+ {result.description}
120
+ </p>
121
+ </li>
122
+ </template>
123
+ </ul>
124
+
125
+ <div if:true={showPagination} class="dx-pagination-container">
126
+ <dx-pagination
127
+ current-page={currentPage}
128
+ total-pages={totalPages}
129
+ pages-to-show="5"
130
+ onpagechange={handlePageChange}
131
+ ></dx-pagination>
132
+ </div>
133
+ </template>
134
+ </div>
135
+ </div>
136
+ </template>
@@ -0,0 +1,306 @@
1
+ import { LightningElement, api, track } from "lwc";
2
+ import {
3
+ SearchController,
4
+ type SearchViewModel
5
+ } from "docUtils/searchController";
6
+ import type { LocaleOption } from "docUtils/searchLocale";
7
+ import {
8
+ decodeHtmlEntities,
9
+ isSortOption,
10
+ SORT_OPTIONS,
11
+ type SearchResult,
12
+ type SortOption
13
+ } from "docUtils/searchClient";
14
+ import { searchLocalePrefs } from "dxUtils/searchLocalePrefs";
15
+ import { track as trackGTM } from "dxUtils/analytics";
16
+ import { CONTENT_TYPE_LABELS } from "dxConstants/contentTypes";
17
+ import { parseSupportedLocales } from "docUtils/searchLocale";
18
+
19
+ // The developer site pins itself as the "developer" site in the unified
20
+ // search corpus (matches the dx search call; the SSG/WordPress developer docs
21
+ // are ingested under this site value).
22
+ const DX_SITE = "developer";
23
+
24
+ const SORT_LABELS: Record<SortOption, string> = {
25
+ relevance: "Relevance",
26
+ date: "Most recent"
27
+ };
28
+
29
+ interface DisplayResult {
30
+ key: string;
31
+ url: string;
32
+ title: string;
33
+ description: string | null;
34
+ hasDescription: boolean;
35
+ contentTypeLabel: string | null;
36
+ hasBadge: boolean;
37
+ displayDate: string;
38
+ hasDate: boolean;
39
+ }
40
+
41
+ interface FacetChip {
42
+ value: string;
43
+ label: string;
44
+ count: number;
45
+ active: boolean;
46
+ cssClass: string;
47
+ }
48
+
49
+ interface SortSelectOption {
50
+ value: string;
51
+ label: string;
52
+ }
53
+
54
+ export default class SearchHybrid extends LightningElement {
55
+ /**
56
+ * JSON string of the supported locale catalog (LocaleOption[]). Injected by
57
+ * the host page. Drives both the controller catalog and the locale selector.
58
+ */
59
+ @api locales?: string;
60
+
61
+ /** The page's server locale, used as the default search language. */
62
+ @api defaultLanguage: string = "en-us";
63
+
64
+ /** Results per page. */
65
+ @api limit?: number;
66
+
67
+ /** Override the /api/search base path (mostly for tests / storybook). */
68
+ @api basePath?: string;
69
+
70
+ @track private vm: SearchViewModel | null = null;
71
+
72
+ private controller: SearchController | null = null;
73
+ private unsubscribe: (() => void) | null = null;
74
+
75
+ connectedCallback(): void {
76
+ const catalog = parseSupportedLocales(this.locales ?? "");
77
+ const supportedIds = catalog.map((l: LocaleOption) => l.id);
78
+
79
+ this.controller = new SearchController({
80
+ catalog,
81
+ defaultLanguage: this.defaultLanguage,
82
+ supportedIds,
83
+ site: DX_SITE,
84
+ limit: this.limit,
85
+ basePath: this.basePath,
86
+ deps: searchLocalePrefs,
87
+ instrument: (event, payload) => {
88
+ // dxUtils/analytics.track requires an EventTarget; route through
89
+ // the host element so the instrumentation listener can capture it.
90
+ trackGTM(this as unknown as EventTarget, event, payload);
91
+ }
92
+ });
93
+
94
+ this.unsubscribe = this.controller.subscribe((vm) => {
95
+ this.vm = vm;
96
+ });
97
+ this.vm = this.controller.viewModel;
98
+ this.controller.init();
99
+ }
100
+
101
+ disconnectedCallback(): void {
102
+ this.unsubscribe?.();
103
+ this.unsubscribe = null;
104
+ this.controller?.dispose();
105
+ this.controller = null;
106
+ }
107
+
108
+ // ---- status flags -----------------------------------------------------
109
+
110
+ get isIdle(): boolean {
111
+ return this.vm?.status === "idle";
112
+ }
113
+
114
+ get isLoading(): boolean {
115
+ return this.vm?.status === "loading";
116
+ }
117
+
118
+ get isError(): boolean {
119
+ return this.vm?.status === "error";
120
+ }
121
+
122
+ get isEmpty(): boolean {
123
+ return this.vm?.status === "empty";
124
+ }
125
+
126
+ get hasResults(): boolean {
127
+ return this.vm?.status === "results";
128
+ }
129
+
130
+ get errorMessage(): string {
131
+ return this.vm?.errorMessage || "Something went wrong. Try again.";
132
+ }
133
+
134
+ get query(): string {
135
+ return this.vm?.query ?? "";
136
+ }
137
+
138
+ get hasQuery(): boolean {
139
+ return this.query !== "";
140
+ }
141
+
142
+ get total(): number {
143
+ return this.vm?.total ?? 0;
144
+ }
145
+
146
+ // ---- results ----------------------------------------------------------
147
+
148
+ get displayResults(): DisplayResult[] {
149
+ const results = this.vm?.results ?? [];
150
+ return results.map((r: SearchResult, i: number) => {
151
+ const description =
152
+ r.description != null && r.description !== ""
153
+ ? decodeHtmlEntities(r.description)
154
+ : null;
155
+ const label = this.contentTypeLabel(r.contentType);
156
+ const displayDate = this.formatDate(r.lastmod);
157
+ return {
158
+ key: `${r.url}-${i}`,
159
+ url: r.url,
160
+ title: decodeHtmlEntities(r.title),
161
+ description,
162
+ hasDescription: description !== null,
163
+ contentTypeLabel: label,
164
+ hasBadge: label !== null,
165
+ displayDate,
166
+ // Tie hasDate to the FORMATTED output so a non-null but
167
+ // unparseable lastmod renders no date element (no raw ISO).
168
+ hasDate: displayDate.length > 0
169
+ };
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Null/NaN-safe date formatter (mirrors arch/searchHybrid). lastmod is null
175
+ * for the entire SSG corpus (the common case → ""); the WordPress portion
176
+ * of the developer corpus emits an ISO timestamp, which we render as a
177
+ * human date rather than the raw machine string.
178
+ */
179
+ private formatDate(lastmod: string | null): string {
180
+ if (typeof lastmod !== "string" || lastmod.length === 0) {
181
+ return "";
182
+ }
183
+ const date = new Date(lastmod);
184
+ if (Number.isNaN(date.getTime())) {
185
+ return "";
186
+ }
187
+ return date.toLocaleDateString(undefined, {
188
+ year: "numeric",
189
+ month: "short",
190
+ day: "numeric"
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Null-safe content-type label lookup. contentType is null for the entire
196
+ * SSG corpus and is the common case; unknown values fall back to the raw
197
+ * value so we never render an empty badge or crash.
198
+ */
199
+ private contentTypeLabel(contentType: string | null): string | null {
200
+ if (contentType == null || contentType === "") {
201
+ return null;
202
+ }
203
+ const labels = CONTENT_TYPE_LABELS as Record<string, string>;
204
+ return labels[contentType] ?? contentType;
205
+ }
206
+
207
+ // ---- content-type facet chips (disjunctive) ---------------------------
208
+
209
+ get facetChips(): FacetChip[] {
210
+ const facets = this.vm?.contentTypeFacets ?? [];
211
+ const selected = this.vm?.selectedType ?? null;
212
+ return facets
213
+ .filter((b) => b.value != null && b.value !== "")
214
+ .map((b) => {
215
+ const active = selected === b.value;
216
+ return {
217
+ value: b.value,
218
+ label: this.contentTypeLabel(b.value) ?? b.value,
219
+ count: b.count,
220
+ active,
221
+ cssClass: active
222
+ ? "dx-facet-chip dx-facet-chip_active"
223
+ : "dx-facet-chip"
224
+ };
225
+ });
226
+ }
227
+
228
+ get hasFacets(): boolean {
229
+ return this.facetChips.length > 0;
230
+ }
231
+
232
+ // ---- sort + locale select options -------------------------------------
233
+
234
+ get sortOptions(): SortSelectOption[] {
235
+ return SORT_OPTIONS.map((value) => ({
236
+ value,
237
+ label: SORT_LABELS[value]
238
+ }));
239
+ }
240
+
241
+ get sortValue(): string {
242
+ return this.vm?.sort ?? "relevance";
243
+ }
244
+
245
+ get showLocaleSelector(): boolean {
246
+ return this.controller?.showLocaleSelector ?? false;
247
+ }
248
+
249
+ get localeSelectOptions(): SortSelectOption[] {
250
+ const options = this.controller?.localeOptions ?? [];
251
+ return options.map((l: LocaleOption) => ({
252
+ value: l.id,
253
+ label: l.displayText ?? l.label ?? l.id
254
+ }));
255
+ }
256
+
257
+ get localeValue(): string {
258
+ return this.vm?.locale ?? this.defaultLanguage;
259
+ }
260
+
261
+ // ---- pagination -------------------------------------------------------
262
+
263
+ get currentPage(): string {
264
+ return String(this.vm?.page ?? 1);
265
+ }
266
+
267
+ get totalPages(): string {
268
+ return String(this.vm?.totalPages ?? 1);
269
+ }
270
+
271
+ get showPagination(): boolean {
272
+ return (this.vm?.totalPages ?? 1) > 1;
273
+ }
274
+
275
+ // ---- event handlers ---------------------------------------------------
276
+
277
+ handleSortChange(event: CustomEvent<{ value: string }>): void {
278
+ const value = event.detail?.value;
279
+ if (isSortOption(value)) {
280
+ this.controller?.setSort(value);
281
+ }
282
+ }
283
+
284
+ handleLocaleChange(event: CustomEvent<{ value: string }>): void {
285
+ const value = event.detail?.value;
286
+ if (value) {
287
+ this.controller?.setLanguage(value);
288
+ }
289
+ }
290
+
291
+ handleFacetClick(event: Event): void {
292
+ const value = (event.currentTarget as HTMLElement)?.dataset?.value;
293
+ if (value) {
294
+ // Disjunctive toggle: the controller clears it if it is already
295
+ // active.
296
+ this.controller?.setType(value);
297
+ }
298
+ }
299
+
300
+ handlePageChange(event: CustomEvent<number>): void {
301
+ const page = event.detail;
302
+ if (typeof page === "number" && page >= 1) {
303
+ this.controller?.setPage(page);
304
+ }
305
+ }
306
+ }