@dogsbay/docs-layout 0.2.0-beta.71 → 0.2.0-beta.72

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/docs-layout",
3
- "version": "0.2.0-beta.71",
3
+ "version": "0.2.0-beta.72",
4
4
  "description": "Standard documentation layout components for Dogsbay",
5
5
  "type": "module",
6
6
  "exports": {
@@ -29,8 +29,8 @@
29
29
  "./json-ld": "./src/json-ld.ts"
30
30
  },
31
31
  "dependencies": {
32
- "@dogsbay/ui": "0.2.0-beta.71",
33
- "@dogsbay/primitives": "0.2.0-beta.71"
32
+ "@dogsbay/ui": "0.2.0-beta.72",
33
+ "@dogsbay/primitives": "0.2.0-beta.72"
34
34
  },
35
35
  "devDependencies": {
36
36
  "vitest": "^3.0.0"
@@ -161,6 +161,7 @@ const {
161
161
  shapeFacets,
162
162
  resolveFacetLabel,
163
163
  resolveFacetTitle,
164
+ sortFacetNames,
164
165
  filterStateToUrlParams,
165
166
  parseFiltersFromUrl,
166
167
  filterStateToPagefindFilters,
@@ -182,10 +183,15 @@ const {
182
183
  meta: { title?: string };
183
184
  sub_results?: Array<{ title: string; url: string; excerpt: string }>;
184
185
  };
186
+ // Filter values match what filterStateToPagefindFilters emits:
187
+ // each facet wrapped in `{any: [...]}` for OR-within-facet semantics.
188
+ // Pagefind also accepts other operator shapes (`all`/`none`/`not`,
189
+ // bare strings, bare arrays) but we only emit the `any` form.
190
+ type PagefindFilterValue = { any: string[] };
185
191
  type PagefindModule = {
186
192
  search(
187
193
  query: string,
188
- options?: { filters?: Record<string, string[]> },
194
+ options?: { filters?: Record<string, PagefindFilterValue> },
189
195
  ): Promise<{ results: PagefindResult[] }>;
190
196
  filters(): Promise<Record<string, Record<string, number>>>;
191
197
  };
@@ -340,7 +346,10 @@ const {
340
346
  * filters, the sidebar stays hidden — single-column layout.
341
347
  */
342
348
  function renderFacets() {
343
- const facetNames = Object.keys(availableFacets);
349
+ const facetNames = sortFacetNames(
350
+ Object.keys(availableFacets),
351
+ taxonomyDisplay,
352
+ );
344
353
  if (facetNames.length === 0) {
345
354
  facetsBox!.classList.add("hidden");
346
355
  return;
@@ -16,6 +16,13 @@ import type { PrefixDisplay } from "./tag-list-data.js";
16
16
  export interface TaxonomyDisplay {
17
17
  prefixes?: Record<string, PrefixDisplay>;
18
18
  labels?: Record<string, string>;
19
+ /**
20
+ * Sort weight for the facet column in the search dialog. Lower
21
+ * numbers appear first. Facets without an `order` sort
22
+ * alphabetically *after* any pinned ones — so `{ type: { order: 1 } }`
23
+ * promotes "Type" to the top while everything else stays alpha.
24
+ */
25
+ order?: number;
19
26
  }
20
27
 
21
28
  /** Map of taxonomy name → display config. */
@@ -120,6 +127,40 @@ export function resolveFacetTitle(facetName: string): string {
120
127
  .replace(/\b\w/g, (c) => c.toUpperCase());
121
128
  }
122
129
 
130
+ /**
131
+ * Return facet names in sidebar render order.
132
+ *
133
+ * Pagefind's `filters()` map iterates in index-discovery order
134
+ * (effectively the first page Pagefind happened to read), so without
135
+ * sorting the column order is arbitrary and shifts between builds.
136
+ * Order rule:
137
+ * 1. Facets with a numeric `order` in their `TaxonomyDisplay`
138
+ * come first, ascending — use this to pin "Type" or "Audience"
139
+ * to the top regardless of name.
140
+ * 2. Everything else sorts alphabetically by facet name.
141
+ * Stable within each tier so ties don't shuffle.
142
+ */
143
+ export function sortFacetNames(
144
+ names: string[],
145
+ display?: TaxonomyDisplayMap,
146
+ ): string[] {
147
+ const withOrder = (name: string): number | undefined => {
148
+ const o = display?.[name]?.order;
149
+ return typeof o === "number" ? o : undefined;
150
+ };
151
+ return [...names].sort((a, b) => {
152
+ const oa = withOrder(a);
153
+ const ob = withOrder(b);
154
+ if (oa !== undefined && ob !== undefined) {
155
+ if (oa !== ob) return oa - ob;
156
+ return a.localeCompare(b);
157
+ }
158
+ if (oa !== undefined) return -1;
159
+ if (ob !== undefined) return 1;
160
+ return a.localeCompare(b);
161
+ });
162
+ }
163
+
123
164
  // ── URL persistence ──────────────────────────────────────────────
124
165
 
125
166
  /**
@@ -175,21 +216,32 @@ export function parseFiltersFromUrl(
175
216
  }
176
217
 
177
218
  /**
178
- * Pagefind's `search()` accepts filters keyed by facet name with
179
- * either a single value, an array (OR), or a nested operator
180
- * object. We always pass the array form so multi-select works
181
- * within a facet without special-casing single-select.
219
+ * Build Pagefind's `filters` argument from internal facet state.
220
+ *
221
+ * Pagefind's array shape (`{ tag: ["a", "b"] }`) is AND by default —
222
+ * "page must have BOTH tags" — per
223
+ * https://pagefind.app/docs/js-api-filtering/ ("All filtering
224
+ * defaults to AND filtering"). That's the wrong UX for faceted
225
+ * search: clicking two checkboxes in the same group should widen
226
+ * the result set, not collapse it to zero. We wrap each facet in
227
+ * `{ any: [...] }` so multi-select within a facet is OR. Across
228
+ * different facets Pagefind already ANDs the keys, which matches
229
+ * the standard "narrow with each additional dimension" UX.
230
+ *
231
+ * Single-value selections also go through `{ any: ["x"] }` —
232
+ * Pagefind treats a one-element `any` identically to a bare string,
233
+ * so there's no behavioural delta and the code stays branch-free.
182
234
  *
183
235
  * Empty filter state → `{}` (Pagefind returns the unfiltered
184
- * result set). A facet with an empty array also drops out so we
185
- * don't accidentally narrow to "must equal nothing".
236
+ * result set). A facet with an empty array drops out so we don't
237
+ * accidentally narrow to "must equal nothing".
186
238
  */
187
239
  export function filterStateToPagefindFilters(
188
240
  filters: FilterState,
189
- ): Record<string, string[]> {
190
- const out: Record<string, string[]> = {};
241
+ ): Record<string, { any: string[] }> {
242
+ const out: Record<string, { any: string[] }> = {};
191
243
  for (const [name, values] of Object.entries(filters)) {
192
- if (values.length > 0) out[name] = [...values];
244
+ if (values.length > 0) out[name] = { any: [...values] };
193
245
  }
194
246
  return out;
195
247
  }