@dirwell/theme-crosswave 0.0.1 → 0.0.3

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/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ThemeAction, c as ThemeLabels, i as ThemePage, o as ThemeColumn, s as ThemeCustomization, t as FrameworkRenderer } from "./model-VZ01cfyg.mjs";
1
+ import { a as ThemeAction, c as ThemeLabels, i as ThemePage, o as ThemeColumn, s as ThemeCustomization, t as FrameworkRenderer } from "./model-Bvw3-a6R.mjs";
2
2
  import { ExplorerTheme, ListingEntry } from "@dirwell/core";
3
3
  //#region src/icons.d.ts
4
4
  type CrosswaveIcon = "all" | "folder" | "image" | "audio" | "video" | "document" | "other" | "link" | "back" | "search" | "pause" | "play" | "controller";
@@ -26,6 +26,7 @@ export interface CrosswaveThemeOptions extends ThemeCustomization {
26
26
  readonly categories?: readonly CrosswaveCategory[];
27
27
  readonly color?: "azure" | "violet" | "amber" | "rose" | "jade" | "graphite";
28
28
  readonly gamepad?: boolean;
29
+ readonly archiveSearch?: boolean;
29
30
  readonly backgroundMotion?: boolean;
30
31
  }
31
32
  export declare function createCrosswaveTheme(options?: CrosswaveThemeOptions, renderer?: FrameworkRenderer): ExplorerTheme;
package/dist/index.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { createHash } from "node:crypto";
3
3
  import { copyJsonObject, escapeHtml, projectListingEntry } from "@dirwell/core";
4
- import path from "node:path";
5
4
  import mime from "mime";
6
5
  import { computed, createSSRApp, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch, watchEffect } from "vue";
7
6
  import { renderToString, ssrIncludeBooleanAttr, ssrInterpolate, ssrLooseContain, ssrLooseEqual, ssrRenderAttr, ssrRenderClass, ssrRenderDynamicAttr, ssrRenderList, ssrRenderSlot, ssrRenderStyle } from "vue/server-renderer";
@@ -11,20 +10,9 @@ import { ListingDataError, loadListing, parseListingEntries } from "@dirwell/cor
11
10
  const pageAsset = (theme, directory, page = 1) => `${theme}-page-${createHash("sha256").update(directory).digest("hex").slice(0, 24)}-${page}.json`;
12
11
  function folderDataHref(entry, context, theme) {
13
12
  if (entry.exitsExplorer || entry.href === null || !(entry.kind === "directory" || entry.link?.targetKind === "directory")) return null;
14
- const fake = "https://dirwell.invalid/";
15
- const base = new URL(context.documentBaseHref ?? `${context.directoryPath ? context.directoryPath + "/" : ""}`, fake);
16
- const root = new URL(context.hrefForDirectory(""), base);
17
- const target = new URL(entry.href, base);
18
- const rootPath = root.pathname.endsWith("/") ? root.pathname : root.pathname.slice(0, root.pathname.lastIndexOf("/") + 1);
19
- const directoryPath = target.pathname.endsWith("/") ? target.pathname : target.pathname.slice(0, target.pathname.lastIndexOf("/") + 1);
20
- const logical = directoryPath.slice(rootPath.length).split("/").filter(Boolean).map(decodeURIComponent).join("/");
21
- const filename = pageAsset(theme, logical);
22
- if (context.pageDataHref !== void 0) return context.pageDataHref(theme, logical, 1);
23
- if (context.mode === "mpa") return context.assetHref(filename);
24
- const href = new URL(filename, new URL(directoryPath, target));
25
- if (href.origin !== new URL(fake).origin) return href.href;
26
- if (context.documentBaseHref === null && !entry.href.startsWith("/")) return path.posix.relative(context.directoryPath || ".", path.posix.join(logical, filename)).split("/").map((segment) => segment === ".." || segment === "." ? segment : encodeURIComponent(segment)).join("/");
27
- return href.pathname;
13
+ const logical = context.directoryPathForEntry(entry.id);
14
+ if (logical === null) return null;
15
+ return context.pageDataHref?.(theme, logical, 1) ?? context.assetHrefForDirectory(logical, pageAsset(theme, logical));
28
16
  }
29
17
  function createThemePage(context, theme, title) {
30
18
  const directoryPath = context.directory.current.relativePath;
@@ -33,20 +21,13 @@ function createThemePage(context, theme, title) {
33
21
  mode: context.mode,
34
22
  outputName: context.outputName,
35
23
  assetHref: context.assetHref,
24
+ assetHrefForDirectory: context.assetHrefForDirectory,
25
+ directoryPathForEntry: context.directoryPathForEntry,
36
26
  hrefForDirectory: context.hrefForDirectory,
37
27
  documentBaseHref: context.documentBaseHref,
38
28
  directoryPath
39
29
  };
40
- const dataForDirectory = (logicalPath) => context.hasPageForDirectory?.(logicalPath) === false ? null : folderDataHref({
41
- id: logicalPath,
42
- name: logicalPath,
43
- kind: "directory",
44
- href: context.hrefForDirectory(logicalPath),
45
- exitsExplorer: false,
46
- size: 0,
47
- modifiedAt: "",
48
- link: null
49
- }, safeContext, theme.name);
30
+ const dataForDirectory = (logicalPath) => context.hasPageForDirectory?.(logicalPath) === false ? null : context.pageDataHref?.(theme.name, logicalPath, 1) ?? context.assetHrefForDirectory(logicalPath, pageAsset(theme.name, logicalPath));
50
31
  const entries = context.directory.entries.map((entry) => {
51
32
  const record = projectListingEntry(entry, context);
52
33
  return theme.entryData === void 0 ? record : {
@@ -68,6 +49,10 @@ function createThemePage(context, theme, title) {
68
49
  manifestHref: context.listing?.manifestHref ?? null,
69
50
  ...context.listing ? { listingRevision: context.listing.revision } : {},
70
51
  updatesHref: context.updatesHref ?? null,
52
+ ...context.searchQueryHref ? {
53
+ searchQueryHref: context.searchQueryHref,
54
+ searchRefreshIntervalMs: context.searchRefreshIntervalMs
55
+ } : {},
71
56
  total: context.listing?.page.total ?? entries.length,
72
57
  entries,
73
58
  sort: context.sort,
@@ -255,6 +240,34 @@ function entryCategories(entry, type, categories) {
255
240
  return categories.filter((category) => category.match === "all" || folder && category.match === "folders" || !folder && category.match === "other" && !specific.length || specific.includes(category));
256
241
  }
257
242
  //#endregion
243
+ //#region ../shared-theme/src/live-search.ts
244
+ async function queryLiveArchive(href, query, signal) {
245
+ const url = new URL(href, document.baseURI);
246
+ if (url.origin !== location.origin) throw new Error("Unsafe search endpoint");
247
+ url.searchParams.set("q", query);
248
+ const response = await fetch(url, {
249
+ signal,
250
+ cache: "no-store"
251
+ });
252
+ if (!response.ok) throw new Error("Archive search unavailable");
253
+ const data = await response.json();
254
+ if (typeof data !== "object" || data === null || !("version" in data) || data.version !== 1 || !("results" in data) || !Array.isArray(data.results) || data.results.length > 1e4 || !("truncated" in data) || typeof data.truncated !== "boolean") throw new Error("Invalid archive results");
255
+ return {
256
+ records: data.results.map((entry) => {
257
+ if (typeof entry !== "object" || entry === null || !("name" in entry) || typeof entry.name !== "string" || !("path" in entry) || typeof entry.path !== "string" || !("kind" in entry) || !["file", "directory"].includes(String(entry.kind)) || !("href" in entry) || entry.href !== null && typeof entry.href !== "string") throw new Error("Invalid archive record");
258
+ const target = entry.href === null ? null : new URL(entry.href, url);
259
+ if (target && !["http:", "https:"].includes(target.protocol)) throw new Error("Unsafe archive link");
260
+ return {
261
+ name: entry.name,
262
+ path: entry.path,
263
+ href: target?.href ?? null,
264
+ exitsExplorer: entry.kind !== "directory"
265
+ };
266
+ }),
267
+ truncated: data.truncated
268
+ };
269
+ }
270
+ //#endregion
258
271
  //#region ../shared-theme/src/customization.ts
259
272
  /** Small serializable inputs shared by official themes; layout stays theme-owned. */
260
273
  const defaultLabels = {
@@ -559,6 +572,7 @@ async function fetchThemePage(dataHref, pageHref, theme, signal, fresh = false,
559
572
  if (data.ui !== void 0) resolveCustomization(data.ui);
560
573
  const result = {
561
574
  ...data,
575
+ searchQueryHref: link(data.searchQueryHref ?? null),
562
576
  updatesHref: link(data.updatesHref ?? null),
563
577
  entries,
564
578
  dataHref: source.href,
@@ -955,6 +969,71 @@ var App_server_default = {
955
969
  setup(__props) {
956
970
  const props = __props, model = shallowRef(restoreThemePage(props.model, Boolean(import.meta.hot))), entries = shallowRef(model.value.entries), ui = computed(() => resolveCustomization(model.value.ui));
957
971
  const query = ref(""), category = ref(model.value.categories[0]?.id ?? null), selected = ref(null), ready = ref(false), busy = ref(false), error = ref(""), failure = ref(null), color = ref(props.model.color), paused = ref(!props.model.backgroundMotion), clock = ref(""), pad = ref(false), waveReady = ref(false), pendingUpdates = ref(null), canvas = ref(), search = ref(), listScroll = ref(), footerElement = ref(), scrollTop = ref(0), viewportHeight = ref(400), retryRequest = shallowRef(null), heights = shallowRef(/* @__PURE__ */ new Map());
972
+ const archiveOpen = ref(false), archiveQuery = ref(""), archiveRows = shallowRef([]), archiveStatus = ref(""), archiveFailed = ref(false), archiveTrigger = ref(), archiveDialog = ref(), archiveInput = ref();
973
+ let archiveAbort, archiveRequest = 0, archivePending = false, archiveDebouncing = false;
974
+ async function searchArchive() {
975
+ const token = ++archiveRequest;
976
+ archiveAbort?.abort();
977
+ const local = archiveAbort = new AbortController();
978
+ archiveFailed.value = false;
979
+ if (!archiveQuery.value.trim()) {
980
+ archiveRows.value = [];
981
+ archiveStatus.value = ui.value.labels.beginArchive;
982
+ return;
983
+ }
984
+ archiveStatus.value = ui.value.labels.searchingArchive;
985
+ archivePending = true;
986
+ try {
987
+ const result = await queryLiveArchive(model.value.searchQueryHref, archiveQuery.value.trim(), local.signal);
988
+ if (local.signal.aborted || token !== archiveRequest) return;
989
+ archiveRows.value = result.records;
990
+ archiveStatus.value = result.truncated ? label(ui.value, "truncatedResults", {
991
+ shown: result.records.length,
992
+ count: result.records.length + "+"
993
+ }) : label(ui.value, "archiveResults", { count: result.records.length });
994
+ } catch {
995
+ if (local.signal.aborted || token !== archiveRequest) return;
996
+ archiveRows.value = [];
997
+ archiveStatus.value = ui.value.labels.archiveError;
998
+ archiveFailed.value = true;
999
+ } finally {
1000
+ if (token === archiveRequest) archivePending = false;
1001
+ }
1002
+ }
1003
+ watch([archiveOpen, archiveQuery], (_, __, cleanup) => {
1004
+ archiveAbort?.abort();
1005
+ archiveRequest++;
1006
+ archivePending = false;
1007
+ if (!archiveOpen.value) return;
1008
+ archiveDebouncing = true;
1009
+ const debounce = setTimeout(() => {
1010
+ archiveDebouncing = false;
1011
+ searchArchive();
1012
+ }, 300);
1013
+ cleanup(() => {
1014
+ clearTimeout(debounce);
1015
+ archiveDebouncing = false;
1016
+ });
1017
+ });
1018
+ watch(archiveOpen, async (open, _, cleanup) => {
1019
+ if (!open) {
1020
+ await nextTick();
1021
+ archiveTrigger.value?.focus();
1022
+ return;
1023
+ }
1024
+ await nextTick();
1025
+ if (!archiveOpen.value) return;
1026
+ archiveDialog.value?.showModal();
1027
+ archiveInput.value?.focus();
1028
+ const timer = setInterval(() => {
1029
+ if (document.visibilityState === "visible" && archiveQuery.value.trim() && !archivePending && !archiveDebouncing) searchArchive();
1030
+ }, model.value.searchRefreshIntervalMs ?? 2e3);
1031
+ cleanup(() => {
1032
+ clearInterval(timer);
1033
+ archiveAbort?.abort();
1034
+ archiveRequest++;
1035
+ });
1036
+ });
958
1037
  const complete = computed(() => entries.value.length === model.value.total), membership = (entry) => Array.isArray(entry.presentation?.categories) ? entry.presentation.categories : [];
959
1038
  const rows = computed(() => entries.value.filter((entry) => membership(entry).includes(category.value) && `${entry.name} ${entry.link?.target ?? ""}`.toLocaleLowerCase().includes(query.value.trim().toLocaleLowerCase()))), active = computed(() => rows.value.find((entry) => entry.id === selected.value) ?? rows.value[0]), heading = computed(() => model.value.categories.find((item) => item.id === category.value)?.label ?? ui.value.labels.allFiles);
960
1039
  const nodes = /* @__PURE__ */ new Map();
@@ -1037,9 +1116,6 @@ var App_server_default = {
1037
1116
  if (listScroll.value) listScroll.value.scrollTop = 0;
1038
1117
  });
1039
1118
  let wave, gamepad, abort, navigation = 0, timer, lastPad = 0, pressed = /* @__PURE__ */ new Set(), disposed = false, padIdentity = null, motionQuery;
1040
- function safeHref(entry) {
1041
- return typeof location !== "undefined" && location.protocol === "file:" && directoryLike(entry) && !entry.exitsExplorer && entry.href?.endsWith("/") ? entry.href + "index.html" : entry.href;
1042
- }
1043
1119
  async function loadComplete() {
1044
1120
  if (complete.value || !model.value.manifestHref || location.protocol === "file:") return;
1045
1121
  const local = abort, token = navigation;
@@ -1156,6 +1232,7 @@ var App_server_default = {
1156
1232
  }
1157
1233
  }
1158
1234
  function keyboard(event) {
1235
+ if (archiveOpen.value) return;
1159
1236
  if (event.isComposing || event.altKey || event.ctrlKey || event.metaKey) return;
1160
1237
  if (event.target?.closest?.("[data-dirwell-actions]")) return;
1161
1238
  if (/INPUT|TEXTAREA|SELECT/.test(event.target?.tagName ?? "")) {
@@ -1186,6 +1263,7 @@ var App_server_default = {
1186
1263
  }
1187
1264
  }
1188
1265
  function controller(value) {
1266
+ if (archiveOpen.value) return;
1189
1267
  pad.value = !!value;
1190
1268
  if (!value) {
1191
1269
  pressed.clear();
@@ -1384,6 +1462,7 @@ var App_server_default = {
1384
1462
  }
1385
1463
  });
1386
1464
  onBeforeUnmount(() => {
1465
+ archiveAbort?.abort();
1387
1466
  disposed = true;
1388
1467
  document.body.removeAttribute("data-cw-enhanced");
1389
1468
  abort?.abort();
@@ -1418,7 +1497,10 @@ var App_server_default = {
1418
1497
  ssrRenderList(model.value.categories, (item) => {
1419
1498
  _push(`<button role="tab"${ssrRenderAttr("id", "cw-tab-" + item.id)} aria-controls="cw-panel"${ssrRenderAttr("aria-selected", category.value === item.id)}${ssrRenderAttr("tabindex", category.value === item.id ? 0 : -1)} class="${ssrRenderClass(["cw-category", { "cw-active": category.value === item.id }])}"${ssrIncludeBooleanAttr(!complete.value) ? " disabled" : ""}><span>${item.svg ?? ""}</span><span>${ssrInterpolate(item.label)}</span><small>${ssrInterpolate(entries.value.filter((entry) => membership(entry).includes(item.id)).length)}</small></button>`);
1420
1499
  });
1421
- _push(`<!--]--></div></nav><section class="cw-browser" id="cw-panel" role="tabpanel"${ssrRenderAttr("aria-labelledby", "cw-tab-" + category.value)}><div class="cw-list-head"><div class="cw-directory"><h2>${ssrInterpolate(heading.value)}</h2><span data-cw-count>${ssrInterpolate(unref(label)(ui.value, "items", { count: complete.value ? rows.value.length : entries.value.length }))}</span></div><label class="cw-search"><span>${ssrInterpolate(ui.value.labels.search)}</span><input type="search"${ssrRenderAttr("value", query.value)}${ssrIncludeBooleanAttr(!ready.value || !complete.value) ? " disabled" : ""}></label></div><div class="cw-list-scroll"${ssrRenderAttr("data-virtual", virtual.value)}><ul class="cw-files">`);
1500
+ _push(`<!--]--></div></nav><section class="cw-browser" id="cw-panel" role="tabpanel"${ssrRenderAttr("aria-labelledby", "cw-tab-" + category.value)}><div class="cw-list-head"><div class="cw-directory"><h2>${ssrInterpolate(heading.value)}</h2><span data-cw-count>${ssrInterpolate(unref(label)(ui.value, "items", { count: complete.value ? rows.value.length : entries.value.length }))}</span></div><label class="cw-search"><span>${ssrInterpolate(ui.value.labels.search)}</span><input type="search"${ssrRenderAttr("value", query.value)}${ssrIncludeBooleanAttr(!ready.value || !complete.value) ? " disabled" : ""}></label>`);
1501
+ if (model.value.searchQueryHref) _push(`<button class="cw-archive-trigger"${ssrIncludeBooleanAttr(!ready.value) ? " disabled" : ""}>${ssrInterpolate(ui.value.labels.searchArchive)}</button>`);
1502
+ else _push(`<!---->`);
1503
+ _push(`</div><div class="cw-list-scroll"${ssrRenderAttr("data-virtual", virtual.value)}><ul class="cw-files">`);
1422
1504
  if (model.value.parentHref) _push(`<li class="cw-row"><a${ssrRenderAttr("href", model.value.parentHref)} class="cw-entry">../</a></li>`);
1423
1505
  else _push(`<!---->`);
1424
1506
  if (start.value) _push(`<li aria-hidden="true" style="${ssrRenderStyle({ height: offsets.value[start.value] + "px" })}"></li>`);
@@ -1427,7 +1509,7 @@ var App_server_default = {
1427
1509
  ssrRenderList(visibleRows.value, (entry, visibleIndex) => {
1428
1510
  _push(`<li${ssrRenderAttr("aria-posinset", start.value + visibleIndex + 1)}${ssrRenderAttr("aria-setsize", rows.value.length)} class="${ssrRenderClass(["cw-row", { "cw-selected": active.value?.id === entry.id }])}" data-cw-entry${ssrRenderAttr("data-custom-columns", Boolean(model.value.ui?.columns))}${ssrRenderAttr("data-id", entry.id)}>`);
1429
1511
  if (entry.href) {
1430
- _push(`<a class="cw-entry"${ssrRenderAttr("href", safeHref(entry))}${ssrRenderAttr("target", entry.exitsExplorer ? "_blank" : void 0)}${ssrRenderAttr("rel", entry.exitsExplorer ? "noopener" : void 0)}>`);
1512
+ _push(`<a class="cw-entry"${ssrRenderAttr("href", entry.href)}${ssrRenderAttr("target", entry.exitsExplorer ? "_blank" : void 0)}${ssrRenderAttr("rel", entry.exitsExplorer ? "noopener" : void 0)}>`);
1431
1513
  ssrRenderSlot(_ctx.$slots, "row", {
1432
1514
  model: model.value,
1433
1515
  entry,
@@ -1472,7 +1554,7 @@ var App_server_default = {
1472
1554
  _push(`<!--[-->`);
1473
1555
  ssrRenderList(ui.value.actions, (action) => {
1474
1556
  _push(`<!--[-->`);
1475
- if (action === "download" && entry.href && !unref(directoryLike)(entry) && !entry.link?.unavailable) _push(`<a${ssrRenderAttr("href", safeHref(entry))} download>${ssrInterpolate(ui.value.labels.download)}</a>`);
1557
+ if (action === "download" && entry.href && !unref(directoryLike)(entry) && !entry.link?.unavailable) _push(`<a${ssrRenderAttr("href", entry.href)} download>${ssrInterpolate(ui.value.labels.download)}</a>`);
1476
1558
  else if (action !== "download") _push(`<button${ssrIncludeBooleanAttr(!ready.value || action === "copy-link" && !entry.href) ? " disabled" : ""}${ssrRenderAttr("aria-label", (action === "copy-link" ? ui.value.labels.copyLink : ui.value.labels.copyPath) + ": " + entry.name)}>${ssrInterpolate(action === "copy-link" ? ui.value.labels.copyLink : ui.value.labels.copyPath)}</button>`);
1477
1559
  else _push(`<!---->`);
1478
1560
  _push(`<!--]-->`);
@@ -1531,7 +1613,21 @@ var App_server_default = {
1531
1613
  ssrRenderList(Object.keys(colors), (name) => {
1532
1614
  _push(`<option${ssrRenderAttr("value", name)}${ssrIncludeBooleanAttr(Array.isArray(color.value) ? ssrLooseContain(color.value, name) : ssrLooseEqual(color.value, name)) ? " selected" : ""}>${ssrInterpolate(ui.value.labels[name])}</option>`);
1533
1615
  });
1534
- _push(`<!--]--></select></label><button class="cw-motion"${ssrIncludeBooleanAttr(!ready.value) ? " disabled" : ""}>${ssrInterpolate(paused.value ? ui.value.labels.resumeMotion : ui.value.labels.pauseMotion)}</button></footer><!--]-->`);
1616
+ _push(`<!--]--></select></label><button class="cw-motion"${ssrIncludeBooleanAttr(!ready.value) ? " disabled" : ""}>${ssrInterpolate(paused.value ? ui.value.labels.resumeMotion : ui.value.labels.pauseMotion)}</button></footer>`);
1617
+ if (archiveOpen.value) {
1618
+ _push(`<dialog class="cw-archive"${ssrRenderAttr("aria-label", ui.value.labels.searchArchive)}><h2>${ssrInterpolate(ui.value.labels.searchArchive)}</h2><input type="search"${ssrRenderAttr("value", archiveQuery.value)}${ssrRenderAttr("aria-label", ui.value.labels.archiveSearchInput)}><p role="status">${ssrInterpolate(archiveStatus.value)}</p>`);
1619
+ if (archiveFailed.value) _push(`<button>${ssrInterpolate(ui.value.labels.retry)}</button>`);
1620
+ else _push(`<!---->`);
1621
+ _push(`<ul><!--[-->`);
1622
+ ssrRenderList(archiveRows.value, (record) => {
1623
+ _push(`<li>`);
1624
+ if (record.href) _push(`<a${ssrRenderAttr("href", record.href)}${ssrRenderAttr("target", record.exitsExplorer ? "_blank" : void 0)}${ssrRenderAttr("rel", record.exitsExplorer ? "noopener" : void 0)}>${ssrInterpolate(record.path)}</a>`);
1625
+ else _push(`<span>${ssrInterpolate(record.path)}</span>`);
1626
+ _push(`</li>`);
1627
+ });
1628
+ _push(`<!--]--></ul><button>${ssrInterpolate(ui.value.labels.close)}</button></dialog>`);
1629
+ } else _push(`<!---->`);
1630
+ _push(`<!--]-->`);
1535
1631
  };
1536
1632
  }
1537
1633
  };
@@ -1546,7 +1642,8 @@ function createCrosswaveTheme(options = {}, renderer) {
1546
1642
  "categories",
1547
1643
  "color",
1548
1644
  "gamepad",
1549
- "backgroundMotion"
1645
+ "backgroundMotion",
1646
+ "archiveSearch"
1550
1647
  ]);
1551
1648
  if (options.title !== void 0 && (typeof options.title !== "string" || !options.title)) throw new TypeError("title must be a non-empty string");
1552
1649
  if (options.color !== void 0 && ![
@@ -1557,7 +1654,11 @@ function createCrosswaveTheme(options = {}, renderer) {
1557
1654
  "jade",
1558
1655
  "graphite"
1559
1656
  ].includes(options.color)) throw new TypeError("Unsupported Crosswave color");
1560
- for (const key of ["gamepad", "backgroundMotion"]) if (options[key] !== void 0 && typeof options[key] !== "boolean") throw new TypeError(`${key} must be a boolean`);
1657
+ for (const key of [
1658
+ "gamepad",
1659
+ "backgroundMotion",
1660
+ "archiveSearch"
1661
+ ]) if (options[key] !== void 0 && typeof options[key] !== "boolean") throw new TypeError(`${key} must be a boolean`);
1561
1662
  const ui = resolveCustomization(options);
1562
1663
  const publishedUi = pickCustomization(options);
1563
1664
  const categoryLabels = {
@@ -1592,6 +1693,7 @@ function createCrosswaveTheme(options = {}, renderer) {
1592
1693
  const model = {
1593
1694
  ...createThemePage(context, theme, options.title ?? ui.brand.name ?? "Dirwell"),
1594
1695
  ...publishedUi === void 0 ? {} : { ui: publishedUi },
1696
+ searchQueryHref: options.archiveSearch === false ? null : context.searchQueryHref ?? null,
1595
1697
  categories: categories.map(({ id, label, svg }) => ({
1596
1698
  id,
1597
1699
  label,