@stapel/search-react 0.14.2 → 0.16.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.
- package/CHANGELOG.md +65 -0
- package/dist/api/generated/schema.d.ts +2 -0
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/default/EmptyExits.d.ts +75 -0
- package/dist/default/EmptyExits.d.ts.map +1 -0
- package/dist/default/EmptyExits.js +100 -0
- package/dist/default/EmptyExits.js.map +1 -0
- package/dist/default/FacetPanelPane.d.ts +28 -1
- package/dist/default/FacetPanelPane.d.ts.map +1 -1
- package/dist/default/FacetPanelPane.js +1 -1
- package/dist/default/FacetPanelPane.js.map +1 -1
- package/dist/default/FilterChips.d.ts +4 -2
- package/dist/default/FilterChips.d.ts.map +1 -1
- package/dist/default/FilterChips.js +45 -5
- package/dist/default/FilterChips.js.map +1 -1
- package/dist/default/SearchPage.d.ts +11 -0
- package/dist/default/SearchPage.d.ts.map +1 -1
- package/dist/default/SearchPage.js +10 -4
- package/dist/default/SearchPage.js.map +1 -1
- package/dist/default/SearchResultsPane.d.ts +15 -0
- package/dist/default/SearchResultsPane.d.ts.map +1 -1
- package/dist/default/SearchResultsPane.js +7 -2
- package/dist/default/SearchResultsPane.js.map +1 -1
- package/dist/default/index.d.ts +2 -0
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +1 -0
- package/dist/default/index.js.map +1 -1
- package/dist/headless/FacetPanel.d.ts.map +1 -1
- package/dist/headless/FacetPanel.js +1 -0
- package/dist/headless/FacetPanel.js.map +1 -1
- package/dist/headless/useSearchBox.d.ts +16 -9
- package/dist/headless/useSearchBox.d.ts.map +1 -1
- package/dist/headless/useSearchBox.js +17 -12
- package/dist/headless/useSearchBox.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +5 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +15 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +21 -0
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +5 -0
- package/dist/i18n/ru.js.map +1 -1
- package/dist/state/facets.d.ts.map +1 -1
- package/dist/state/facets.js +40 -10
- package/dist/state/facets.js.map +1 -1
- package/llms.txt +3 -2
- package/manifest.json +25 -2
- package/nav-manifest.json +1 -1
- package/package.json +7 -7
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +2 -0
- package/src/default/EmptyExits.tsx +203 -0
- package/src/default/FacetPanelPane.tsx +34 -7
- package/src/default/FilterChips.tsx +50 -7
- package/src/default/SearchPage.tsx +25 -0
- package/src/default/SearchResultsPane.tsx +33 -4
- package/src/default/index.ts +2 -0
- package/src/headless/FacetPanel.tsx +1 -0
- package/src/headless/useSearchBox.ts +17 -11
- package/src/i18n/es.ts +5 -0
- package/src/i18n/keys.ts +22 -0
- package/src/i18n/ru.ts +5 -0
- package/src/state/facets.ts +36 -8
|
@@ -197,6 +197,9 @@ export interface SearchPageProps extends ThemeModeProp, ParseSearchStateOptions
|
|
|
197
197
|
* coordinate, with or without it.
|
|
198
198
|
*/
|
|
199
199
|
readonly geoLabel?: ReactNode;
|
|
200
|
+
/** Print the engine's list of uncounted facet slugs in the filter panel.
|
|
201
|
+
* Default `false` — see {@link FacetPanelPaneProps.skippedNotice}. */
|
|
202
|
+
readonly skippedNotice?: boolean;
|
|
200
203
|
/**
|
|
201
204
|
* Open the search centred where the visitor is, when the URL says nothing
|
|
202
205
|
* about location — see {@link SearchStateProviderProps.defaultGeo} for the
|
|
@@ -250,6 +253,14 @@ export interface SearchPageProps extends ThemeModeProp, ParseSearchStateOptions
|
|
|
250
253
|
* not the viewport — not an escape hatch for "I prefer a column on phones".
|
|
251
254
|
*/
|
|
252
255
|
readonly filtersLayout?: SearchFiltersLayout;
|
|
256
|
+
/**
|
|
257
|
+
* The host's own exits from an empty result — sibling sections with their
|
|
258
|
+
* counts. A SLOT for the same reason `breadcrumb` is one: walking the tree
|
|
259
|
+
* belongs to `categories-react`. Everything the pair can derive on its own
|
|
260
|
+
* (up a level, widen the radius, search everywhere, drop one filter) is
|
|
261
|
+
* offered whether this is filled or not — see {@link EmptyExits}.
|
|
262
|
+
*/
|
|
263
|
+
readonly renderEmptyExits?: () => ReactNode;
|
|
253
264
|
/**
|
|
254
265
|
* Open the phone filter sheet on mount.
|
|
255
266
|
*
|
|
@@ -306,6 +317,7 @@ export interface SearchPageProps extends ThemeModeProp, ParseSearchStateOptions
|
|
|
306
317
|
interface SearchPageBodyProps {
|
|
307
318
|
readonly renderCard?: SearchCardRenderer;
|
|
308
319
|
readonly categoryFeatures?: readonly FeatureDef[];
|
|
320
|
+
readonly renderEmptyExits?: () => ReactNode;
|
|
309
321
|
readonly locale?: string;
|
|
310
322
|
readonly resolveFacetLabels?: FacetLabelResolver;
|
|
311
323
|
readonly searchBox?: boolean;
|
|
@@ -314,6 +326,7 @@ interface SearchPageBodyProps {
|
|
|
314
326
|
readonly categoryLabel?: ReactNode;
|
|
315
327
|
readonly renderGeoFilter?: (slot: GeoFilterSlotProps) => ReactNode;
|
|
316
328
|
readonly geoLabel?: ReactNode;
|
|
329
|
+
readonly skippedNotice?: boolean;
|
|
317
330
|
readonly footer?: ReactNode;
|
|
318
331
|
readonly filtersHeader?: ReactNode;
|
|
319
332
|
readonly resultsHeader?: ReactNode;
|
|
@@ -415,6 +428,9 @@ function SearchPageBody(props: SearchPageBodyProps): ReactElement {
|
|
|
415
428
|
<FacetPanelPane
|
|
416
429
|
{...(layout === "sheet" ? { heading: null } : {})}
|
|
417
430
|
{...(categoryFeatures !== undefined ? { categoryFeatures } : {})}
|
|
431
|
+
{...(props.renderEmptyExits !== undefined
|
|
432
|
+
? { renderEmptyExits: props.renderEmptyExits }
|
|
433
|
+
: {})}
|
|
418
434
|
{...(locale !== undefined ? { locale } : {})}
|
|
419
435
|
{...(resolveFacetLabels !== undefined ? { resolveFacetLabels } : {})}
|
|
420
436
|
{...(props.languages !== undefined ? { languages: props.languages } : {})}
|
|
@@ -425,6 +441,9 @@ function SearchPageBody(props: SearchPageBodyProps): ReactElement {
|
|
|
425
441
|
? { renderGeoFilter: props.renderGeoFilter }
|
|
426
442
|
: {})}
|
|
427
443
|
{...(props.geoLabel !== undefined ? { geoLabel: props.geoLabel } : {})}
|
|
444
|
+
{...(props.skippedNotice !== undefined
|
|
445
|
+
? { skippedNotice: props.skippedNotice }
|
|
446
|
+
: {})}
|
|
428
447
|
/>
|
|
429
448
|
)}
|
|
430
449
|
</Flex>
|
|
@@ -479,6 +498,10 @@ function SearchPageBody(props: SearchPageBodyProps): ReactElement {
|
|
|
479
498
|
{...(props.degradationNotice !== undefined
|
|
480
499
|
? { degradationNotice: props.degradationNotice }
|
|
481
500
|
: {})}
|
|
501
|
+
{...(categoryFeatures !== undefined ? { categoryFeatures } : {})}
|
|
502
|
+
{...(props.renderEmptyExits !== undefined
|
|
503
|
+
? { renderEmptyExits: props.renderEmptyExits }
|
|
504
|
+
: {})}
|
|
482
505
|
/>
|
|
483
506
|
);
|
|
484
507
|
|
|
@@ -595,6 +618,7 @@ export function SearchPage(props: SearchPageProps): ReactElement {
|
|
|
595
618
|
categoryLabel,
|
|
596
619
|
renderGeoFilter,
|
|
597
620
|
geoLabel,
|
|
621
|
+
skippedNotice,
|
|
598
622
|
defaultGeo,
|
|
599
623
|
footer,
|
|
600
624
|
filtersHeader,
|
|
@@ -628,6 +652,7 @@ export function SearchPage(props: SearchPageProps): ReactElement {
|
|
|
628
652
|
{...(categoryLabel !== undefined ? { categoryLabel } : {})}
|
|
629
653
|
{...(renderGeoFilter !== undefined ? { renderGeoFilter } : {})}
|
|
630
654
|
{...(geoLabel !== undefined ? { geoLabel } : {})}
|
|
655
|
+
{...(skippedNotice !== undefined ? { skippedNotice } : {})}
|
|
631
656
|
{...(footer !== undefined ? { footer } : {})}
|
|
632
657
|
{...(filtersHeader !== undefined ? { filtersHeader } : {})}
|
|
633
658
|
{...(resultsHeader !== undefined ? { resultsHeader } : {})}
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
visuallyHidden,
|
|
37
37
|
} from "@stapel/tokens-antd/skin";
|
|
38
38
|
import { spacing } from "@stapel/tokens";
|
|
39
|
+
import type { FeatureDef } from "@stapel/attributes-react";
|
|
39
40
|
import type { SearchItem } from "../api/types.js";
|
|
40
41
|
import { SearchResults } from "../headless/SearchResults.js";
|
|
41
42
|
import type { SearchResultsBag } from "../headless/SearchResults.js";
|
|
@@ -43,6 +44,7 @@ import { useScorerNames } from "../headless/useScorerNames.js";
|
|
|
43
44
|
import { SEARCH_WINDOW_EXCEEDED } from "../i18n/errorsMap.js";
|
|
44
45
|
import { SEARCH_I18N_KEYS } from "../i18n/keys.js";
|
|
45
46
|
import { DegradationNotice } from "./DegradationNotice.js";
|
|
47
|
+
import { EmptyExits } from "./EmptyExits.js";
|
|
46
48
|
import type { DegradationNoticeVariant } from "./DegradationNotice.js";
|
|
47
49
|
import { SearchResultCard } from "./SearchResultCard.js";
|
|
48
50
|
import type { SearchCardRenderer } from "./SearchResultCard.js";
|
|
@@ -191,6 +193,20 @@ export interface SearchResultsPaneProps extends ThemeModeProp {
|
|
|
191
193
|
* viewport is not spent saying "Results" over a list of results.
|
|
192
194
|
*/
|
|
193
195
|
readonly header?: "banner" | "compact";
|
|
196
|
+
/**
|
|
197
|
+
* The category's feature schema, used ONLY to name an applied filter in the
|
|
198
|
+
* empty state's exits ("Without Brand" rather than "Without vendor").
|
|
199
|
+
* Absent, an exit falls back to the slug, which is still a removable
|
|
200
|
+
* constraint the person can read.
|
|
201
|
+
*/
|
|
202
|
+
readonly categoryFeatures?: readonly FeatureDef[];
|
|
203
|
+
/**
|
|
204
|
+
* The host's own exits from an empty result — sibling sections with their
|
|
205
|
+
* counts, most usefully. Rendered above the derived exits (see
|
|
206
|
+
* {@link EmptyExits}); walking the tree is `categories-react`'s job, so
|
|
207
|
+
* this pair offers the slot and never the tree.
|
|
208
|
+
*/
|
|
209
|
+
readonly renderEmptyExits?: () => ReactNode;
|
|
194
210
|
}
|
|
195
211
|
|
|
196
212
|
function Count(props: { bag: SearchResultsBag }): ReactElement | null {
|
|
@@ -311,10 +327,23 @@ export function SearchResultsPane(props: SearchResultsPaneProps): ReactElement {
|
|
|
311
327
|
skeletonRows={6}
|
|
312
328
|
onRetry={bag.refetch}
|
|
313
329
|
empty={
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
330
|
+
<>
|
|
331
|
+
<EmptyState
|
|
332
|
+
title={t(SEARCH_I18N_KEYS.resultsEmpty)}
|
|
333
|
+
testId="search-empty"
|
|
334
|
+
/>
|
|
335
|
+
{/* The sentence alone was the terminal state of a whole
|
|
336
|
+
catalogue. Every exit here removes exactly one
|
|
337
|
+
constraint the person did not necessarily choose. */}
|
|
338
|
+
<EmptyExits
|
|
339
|
+
{...(props.categoryFeatures !== undefined
|
|
340
|
+
? { categoryFeatures: props.categoryFeatures }
|
|
341
|
+
: {})}
|
|
342
|
+
{...(props.renderEmptyExits !== undefined
|
|
343
|
+
? { renderExtra: props.renderEmptyExits }
|
|
344
|
+
: {})}
|
|
345
|
+
/>
|
|
346
|
+
</>
|
|
318
347
|
}
|
|
319
348
|
failed={(error) => {
|
|
320
349
|
// The window refusal is a DIFFERENT sentence from a failed
|
package/src/default/index.ts
CHANGED
|
@@ -58,6 +58,8 @@ export {
|
|
|
58
58
|
orderChipFilters,
|
|
59
59
|
} from "./FilterChips.js";
|
|
60
60
|
export type { ChipBand, ChipSpec, FilterChipsProps } from "./FilterChips.js";
|
|
61
|
+
export { EmptyExits, RADIUS_WIDEN_FACTOR, parentCategory } from "./EmptyExits.js";
|
|
62
|
+
export type { EmptyExitsProps } from "./EmptyExits.js";
|
|
61
63
|
|
|
62
64
|
export { LocationSummaryLine } from "./LocationSummaryLine.js";
|
|
63
65
|
export type { LocationSummaryLineProps } from "./LocationSummaryLine.js";
|
|
@@ -173,24 +173,30 @@ function degradedWith(answer: SuggestAnswer | undefined, literal: string): boole
|
|
|
173
173
|
/**
|
|
174
174
|
* The destinations worth offering out of one answer.
|
|
175
175
|
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
176
|
+
* **Every category the server ranked, in the order it ranked them.** Until
|
|
177
|
+
* 0.15 this dropped every row whose count was `0`, on the reasoning that an
|
|
178
|
+
* empty section is a dead end dressed as a destination. That reasoning holds
|
|
179
|
+
* for a stocked board and inverts on a young one, which is what the live
|
|
180
|
+
* measurement showed: 3036 leaves, ~100 listings, so 2924 leaves read zero
|
|
181
|
+
* and the filter deleted the answer. Typing a word with six real sections behind it in
|
|
182
|
+
* the catalogue produced NO PANEL AT ALL, and so did two other everyday
|
|
183
|
+
* words; the type-ahead told a buyer those sections do not exist, about
|
|
184
|
+
* sections that do. A catalogue you cannot navigate until somebody stocks it
|
|
185
|
+
* is worse than one that admits a section is empty.
|
|
180
186
|
*
|
|
181
|
-
* The
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
187
|
+
* The server already ranks stocked sections above empty ones
|
|
188
|
+
* (`stapel-search` 0.8: stock, then match quality, then count) so the useful
|
|
189
|
+
* rows still come first, and every row carries its own `count` for the
|
|
190
|
+
* surface to print honestly — "0 listings" is a fact a person can act on,
|
|
191
|
+
* an absent panel is not. Nothing is dropped here; a surface that wants a
|
|
192
|
+
* shorter list slices it.
|
|
186
193
|
*/
|
|
187
194
|
export function offerableCategories(
|
|
188
195
|
answer: SuggestAnswer | undefined
|
|
189
196
|
): readonly SuggestCategory[] {
|
|
190
197
|
const categories = answer?.categories;
|
|
191
198
|
if (categories === undefined || categories.length === 0) return NO_CATEGORIES;
|
|
192
|
-
|
|
193
|
-
return categories.filter((category) => category.count > 0);
|
|
199
|
+
return categories;
|
|
194
200
|
}
|
|
195
201
|
|
|
196
202
|
export function useSearchBox(options: UseSearchBoxOptions = {}): SearchBoxBag {
|
package/src/i18n/es.ts
CHANGED
|
@@ -102,6 +102,11 @@ export const searchI18nBundleEs: I18nDictionary = {
|
|
|
102
102
|
"search.filters.short": "Filtros",
|
|
103
103
|
"search.filters.chip_more": ", +{count}",
|
|
104
104
|
|
|
105
|
+
"search.empty.exits_title": "Prueba a ampliar la búsqueda",
|
|
106
|
+
"search.empty.up_a_level": "Subir un nivel",
|
|
107
|
+
"search.empty.widen_radius": "Buscar en un radio de {km} km",
|
|
108
|
+
"search.empty.anywhere": "Buscar en todas partes",
|
|
109
|
+
"search.empty.drop_filter": "Sin «{name}»",
|
|
105
110
|
"search.category.title": "Categoría",
|
|
106
111
|
"search.category.clear": "Buscar en todo el catálogo",
|
|
107
112
|
"search.category.current": "Buscando dentro de {path}",
|
package/src/i18n/keys.ts
CHANGED
|
@@ -162,6 +162,23 @@ export const SEARCH_I18N_KEYS = {
|
|
|
162
162
|
* family — nothing is being counted in words. */
|
|
163
163
|
filtersChipMore: "search.filters.chip_more",
|
|
164
164
|
|
|
165
|
+
// ── the way OUT of a search that found nothing ───────────────────────────
|
|
166
|
+
/** Caption above the exit buttons. Not a second "nothing found" — that
|
|
167
|
+
* sentence has already been said; this one says what can be done next. */
|
|
168
|
+
emptyExitsTitle: "search.empty.exits_title",
|
|
169
|
+
/** Drop the last segment of the category path. Deliberately unnamed: the
|
|
170
|
+
* pair holds a path of slugs or ids and cannot name the parent, and a
|
|
171
|
+
* guessed name on a navigation control is worse than an honest direction. */
|
|
172
|
+
emptyUpALevel: "search.empty.up_a_level",
|
|
173
|
+
/** "Search within {km} km" — the widened radius, already multiplied. */
|
|
174
|
+
emptyWidenRadius: "search.empty.widen_radius",
|
|
175
|
+
/** Drop the location constraint. Distinct from `search.geo.clear`, which
|
|
176
|
+
* labels the same action inside the location picker: here it is one exit
|
|
177
|
+
* among several and has to name the constraint it removes. */
|
|
178
|
+
emptyAnywhere: "search.empty.anywhere",
|
|
179
|
+
/** "Without {name}" — one applied filter, named as its own chip names it. */
|
|
180
|
+
emptyDropFilter: "search.empty.drop_filter",
|
|
181
|
+
|
|
165
182
|
// ── category (a host slot, plus the control that removes it) ─────────────
|
|
166
183
|
categoryTitle: "search.category.title",
|
|
167
184
|
categoryClear: "search.category.clear",
|
|
@@ -390,6 +407,11 @@ export const searchI18nBundleEn: Record<string, string> = {
|
|
|
390
407
|
"search.filters.short": "Filters",
|
|
391
408
|
"search.filters.chip_more": ", +{count}",
|
|
392
409
|
|
|
410
|
+
"search.empty.exits_title": "Try widening the search",
|
|
411
|
+
"search.empty.up_a_level": "Go up a level",
|
|
412
|
+
"search.empty.widen_radius": "Search within {km} km",
|
|
413
|
+
"search.empty.anywhere": "Search anywhere",
|
|
414
|
+
"search.empty.drop_filter": "Without {name}",
|
|
393
415
|
"search.category.title": "Category",
|
|
394
416
|
"search.category.clear": "Search the whole catalogue",
|
|
395
417
|
"search.category.current": "Searching inside {path}",
|
package/src/i18n/ru.ts
CHANGED
|
@@ -131,6 +131,11 @@ export const searchI18nBundleRu: I18nDictionary = {
|
|
|
131
131
|
"search.filters.short": "Фильтры",
|
|
132
132
|
"search.filters.chip_more": ", +{count}",
|
|
133
133
|
|
|
134
|
+
"search.empty.exits_title": "Попробуйте расширить поиск",
|
|
135
|
+
"search.empty.up_a_level": "Подняться на уровень выше",
|
|
136
|
+
"search.empty.widen_radius": "Искать в радиусе {km} км",
|
|
137
|
+
"search.empty.anywhere": "Искать везде",
|
|
138
|
+
"search.empty.drop_filter": "Без «{name}»",
|
|
134
139
|
"search.category.title": "Категория",
|
|
135
140
|
"search.category.clear": "Искать по всему каталогу",
|
|
136
141
|
"search.category.current": "Ищем внутри {path}",
|
package/src/state/facets.ts
CHANGED
|
@@ -31,7 +31,11 @@
|
|
|
31
31
|
* A slug the server SKIPPED (`facet_meta.skipped`, dropped at
|
|
32
32
|
* `MAX_FACET_FIELDS`) is not counted at all. Its options carry `count: null`,
|
|
33
33
|
* never `0` — "we did not count this" and "there are none" are different
|
|
34
|
-
* sentences and the honest one has to survive to the screen.
|
|
34
|
+
* sentences and the honest one has to survive to the screen. Its options come
|
|
35
|
+
* from the CATEGORY SCHEMA, because a count is not a licence to filter: the
|
|
36
|
+
* server accepts `f.<slug>` whether or not it counted it, so an uncounted
|
|
37
|
+
* facet is a filter that exists and a number that is missing, not a filter
|
|
38
|
+
* that does not exist. See {@link buildFacetGroups}.
|
|
35
39
|
*
|
|
36
40
|
* 3. **Not every counted slug is a FILTER.** The plan is built from the leaf
|
|
37
41
|
* category's feature defs and the counter counts whatever is indexed, so a
|
|
@@ -352,13 +356,37 @@ export function buildFacetGroups(input: BuildFacetGroupsInput): readonly FacetGr
|
|
|
352
356
|
const push = (value: string): void => {
|
|
353
357
|
if (!values.includes(value)) values.push(value);
|
|
354
358
|
};
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
359
|
+
if (counted) {
|
|
360
|
+
// Declared order first (closed sets), then whatever else the counter
|
|
361
|
+
// returned, then anything selected that neither of them mentioned.
|
|
362
|
+
for (const value of declared) if (value in counts) push(value);
|
|
363
|
+
const remaining = Object.keys(counts)
|
|
364
|
+
.filter((value) => !declared.includes(value))
|
|
365
|
+
.sort((a, b) => (counts[b] ?? 0) - (counts[a] ?? 0) || a.localeCompare(b));
|
|
366
|
+
for (const value of remaining) push(value);
|
|
367
|
+
} else {
|
|
368
|
+
// A COUNT IS NOT A LICENCE TO FILTER.
|
|
369
|
+
//
|
|
370
|
+
// `counts` is empty here by definition — the counter never looked at
|
|
371
|
+
// this slug (`facet_meta.skipped`, the plan overrunning
|
|
372
|
+
// `MAX_FACET_FIELDS`) or the deployment counts nothing at all. Gating
|
|
373
|
+
// the options on `value in counts` therefore left the facet with NONE,
|
|
374
|
+
// and a group with no options is dropped by every surface that draws
|
|
375
|
+
// one. Measured on a live cars leaf: 26 facetable features declared, 12
|
|
376
|
+
// counted, 14 rendered as a warning naming filters the person could not
|
|
377
|
+
// then use — while `/query` accepts `f.<slug>` for every one of them.
|
|
378
|
+
//
|
|
379
|
+
// So an uncounted facet is built from the SCHEMA: the option table the
|
|
380
|
+
// category config already carries (`config.options`), or the captions
|
|
381
|
+
// the answer sent for it. The counts stay `null` below — "nobody
|
|
382
|
+
// counted this" is still said, in the place it belongs (beside the
|
|
383
|
+
// option), and it no longer decides whether the filter exists.
|
|
384
|
+
//
|
|
385
|
+
// A `ref_select` whose config is a bare `optionsRef` pointer has no
|
|
386
|
+
// table here and no options to draw; naming that gap is
|
|
387
|
+
// `MODULE.md`'s job, inventing values would not be.
|
|
388
|
+
for (const value of declared) push(value);
|
|
389
|
+
}
|
|
362
390
|
for (const value of selected) push(value);
|
|
363
391
|
|
|
364
392
|
const labelOptions = {
|