@dirwell/theme-crosswave 0.0.1 → 0.0.2

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-Dr7YgDTv.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
@@ -68,6 +68,10 @@ function createThemePage(context, theme, title) {
68
68
  manifestHref: context.listing?.manifestHref ?? null,
69
69
  ...context.listing ? { listingRevision: context.listing.revision } : {},
70
70
  updatesHref: context.updatesHref ?? null,
71
+ ...context.searchQueryHref ? {
72
+ searchQueryHref: context.searchQueryHref,
73
+ searchRefreshIntervalMs: context.searchRefreshIntervalMs
74
+ } : {},
71
75
  total: context.listing?.page.total ?? entries.length,
72
76
  entries,
73
77
  sort: context.sort,
@@ -255,6 +259,34 @@ function entryCategories(entry, type, categories) {
255
259
  return categories.filter((category) => category.match === "all" || folder && category.match === "folders" || !folder && category.match === "other" && !specific.length || specific.includes(category));
256
260
  }
257
261
  //#endregion
262
+ //#region ../shared-theme/src/live-search.ts
263
+ async function queryLiveArchive(href, query, signal) {
264
+ const url = new URL(href, document.baseURI);
265
+ if (url.origin !== location.origin) throw new Error("Unsafe search endpoint");
266
+ url.searchParams.set("q", query);
267
+ const response = await fetch(url, {
268
+ signal,
269
+ cache: "no-store"
270
+ });
271
+ if (!response.ok) throw new Error("Archive search unavailable");
272
+ const data = await response.json();
273
+ 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");
274
+ return {
275
+ records: data.results.map((entry) => {
276
+ 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");
277
+ const target = entry.href === null ? null : new URL(entry.href, url);
278
+ if (target && !["http:", "https:"].includes(target.protocol)) throw new Error("Unsafe archive link");
279
+ return {
280
+ name: entry.name,
281
+ path: entry.path,
282
+ href: target?.href ?? null,
283
+ exitsExplorer: entry.kind !== "directory"
284
+ };
285
+ }),
286
+ truncated: data.truncated
287
+ };
288
+ }
289
+ //#endregion
258
290
  //#region ../shared-theme/src/customization.ts
259
291
  /** Small serializable inputs shared by official themes; layout stays theme-owned. */
260
292
  const defaultLabels = {
@@ -559,6 +591,7 @@ async function fetchThemePage(dataHref, pageHref, theme, signal, fresh = false,
559
591
  if (data.ui !== void 0) resolveCustomization(data.ui);
560
592
  const result = {
561
593
  ...data,
594
+ searchQueryHref: link(data.searchQueryHref ?? null),
562
595
  updatesHref: link(data.updatesHref ?? null),
563
596
  entries,
564
597
  dataHref: source.href,
@@ -955,6 +988,71 @@ var App_server_default = {
955
988
  setup(__props) {
956
989
  const props = __props, model = shallowRef(restoreThemePage(props.model, Boolean(import.meta.hot))), entries = shallowRef(model.value.entries), ui = computed(() => resolveCustomization(model.value.ui));
957
990
  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());
991
+ const archiveOpen = ref(false), archiveQuery = ref(""), archiveRows = shallowRef([]), archiveStatus = ref(""), archiveFailed = ref(false), archiveTrigger = ref(), archiveDialog = ref(), archiveInput = ref();
992
+ let archiveAbort, archiveRequest = 0, archivePending = false, archiveDebouncing = false;
993
+ async function searchArchive() {
994
+ const token = ++archiveRequest;
995
+ archiveAbort?.abort();
996
+ const local = archiveAbort = new AbortController();
997
+ archiveFailed.value = false;
998
+ if (!archiveQuery.value.trim()) {
999
+ archiveRows.value = [];
1000
+ archiveStatus.value = ui.value.labels.beginArchive;
1001
+ return;
1002
+ }
1003
+ archiveStatus.value = ui.value.labels.searchingArchive;
1004
+ archivePending = true;
1005
+ try {
1006
+ const result = await queryLiveArchive(model.value.searchQueryHref, archiveQuery.value.trim(), local.signal);
1007
+ if (local.signal.aborted || token !== archiveRequest) return;
1008
+ archiveRows.value = result.records;
1009
+ archiveStatus.value = result.truncated ? label(ui.value, "truncatedResults", {
1010
+ shown: result.records.length,
1011
+ count: result.records.length + "+"
1012
+ }) : label(ui.value, "archiveResults", { count: result.records.length });
1013
+ } catch {
1014
+ if (local.signal.aborted || token !== archiveRequest) return;
1015
+ archiveRows.value = [];
1016
+ archiveStatus.value = ui.value.labels.archiveError;
1017
+ archiveFailed.value = true;
1018
+ } finally {
1019
+ if (token === archiveRequest) archivePending = false;
1020
+ }
1021
+ }
1022
+ watch([archiveOpen, archiveQuery], (_, __, cleanup) => {
1023
+ archiveAbort?.abort();
1024
+ archiveRequest++;
1025
+ archivePending = false;
1026
+ if (!archiveOpen.value) return;
1027
+ archiveDebouncing = true;
1028
+ const debounce = setTimeout(() => {
1029
+ archiveDebouncing = false;
1030
+ searchArchive();
1031
+ }, 300);
1032
+ cleanup(() => {
1033
+ clearTimeout(debounce);
1034
+ archiveDebouncing = false;
1035
+ });
1036
+ });
1037
+ watch(archiveOpen, async (open, _, cleanup) => {
1038
+ if (!open) {
1039
+ await nextTick();
1040
+ archiveTrigger.value?.focus();
1041
+ return;
1042
+ }
1043
+ await nextTick();
1044
+ if (!archiveOpen.value) return;
1045
+ archiveDialog.value?.showModal();
1046
+ archiveInput.value?.focus();
1047
+ const timer = setInterval(() => {
1048
+ if (document.visibilityState === "visible" && archiveQuery.value.trim() && !archivePending && !archiveDebouncing) searchArchive();
1049
+ }, model.value.searchRefreshIntervalMs ?? 2e3);
1050
+ cleanup(() => {
1051
+ clearInterval(timer);
1052
+ archiveAbort?.abort();
1053
+ archiveRequest++;
1054
+ });
1055
+ });
958
1056
  const complete = computed(() => entries.value.length === model.value.total), membership = (entry) => Array.isArray(entry.presentation?.categories) ? entry.presentation.categories : [];
959
1057
  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
1058
  const nodes = /* @__PURE__ */ new Map();
@@ -1156,6 +1254,7 @@ var App_server_default = {
1156
1254
  }
1157
1255
  }
1158
1256
  function keyboard(event) {
1257
+ if (archiveOpen.value) return;
1159
1258
  if (event.isComposing || event.altKey || event.ctrlKey || event.metaKey) return;
1160
1259
  if (event.target?.closest?.("[data-dirwell-actions]")) return;
1161
1260
  if (/INPUT|TEXTAREA|SELECT/.test(event.target?.tagName ?? "")) {
@@ -1186,6 +1285,7 @@ var App_server_default = {
1186
1285
  }
1187
1286
  }
1188
1287
  function controller(value) {
1288
+ if (archiveOpen.value) return;
1189
1289
  pad.value = !!value;
1190
1290
  if (!value) {
1191
1291
  pressed.clear();
@@ -1384,6 +1484,7 @@ var App_server_default = {
1384
1484
  }
1385
1485
  });
1386
1486
  onBeforeUnmount(() => {
1487
+ archiveAbort?.abort();
1387
1488
  disposed = true;
1388
1489
  document.body.removeAttribute("data-cw-enhanced");
1389
1490
  abort?.abort();
@@ -1418,7 +1519,10 @@ var App_server_default = {
1418
1519
  ssrRenderList(model.value.categories, (item) => {
1419
1520
  _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
1521
  });
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">`);
1522
+ _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>`);
1523
+ if (model.value.searchQueryHref) _push(`<button class="cw-archive-trigger"${ssrIncludeBooleanAttr(!ready.value) ? " disabled" : ""}>${ssrInterpolate(ui.value.labels.searchArchive)}</button>`);
1524
+ else _push(`<!---->`);
1525
+ _push(`</div><div class="cw-list-scroll"${ssrRenderAttr("data-virtual", virtual.value)}><ul class="cw-files">`);
1422
1526
  if (model.value.parentHref) _push(`<li class="cw-row"><a${ssrRenderAttr("href", model.value.parentHref)} class="cw-entry">../</a></li>`);
1423
1527
  else _push(`<!---->`);
1424
1528
  if (start.value) _push(`<li aria-hidden="true" style="${ssrRenderStyle({ height: offsets.value[start.value] + "px" })}"></li>`);
@@ -1531,7 +1635,21 @@ var App_server_default = {
1531
1635
  ssrRenderList(Object.keys(colors), (name) => {
1532
1636
  _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
1637
  });
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><!--]-->`);
1638
+ _push(`<!--]--></select></label><button class="cw-motion"${ssrIncludeBooleanAttr(!ready.value) ? " disabled" : ""}>${ssrInterpolate(paused.value ? ui.value.labels.resumeMotion : ui.value.labels.pauseMotion)}</button></footer>`);
1639
+ if (archiveOpen.value) {
1640
+ _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>`);
1641
+ if (archiveFailed.value) _push(`<button>${ssrInterpolate(ui.value.labels.retry)}</button>`);
1642
+ else _push(`<!---->`);
1643
+ _push(`<ul><!--[-->`);
1644
+ ssrRenderList(archiveRows.value, (record) => {
1645
+ _push(`<li>`);
1646
+ 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>`);
1647
+ else _push(`<span>${ssrInterpolate(record.path)}</span>`);
1648
+ _push(`</li>`);
1649
+ });
1650
+ _push(`<!--]--></ul><button>${ssrInterpolate(ui.value.labels.close)}</button></dialog>`);
1651
+ } else _push(`<!---->`);
1652
+ _push(`<!--]-->`);
1535
1653
  };
1536
1654
  }
1537
1655
  };
@@ -1546,7 +1664,8 @@ function createCrosswaveTheme(options = {}, renderer) {
1546
1664
  "categories",
1547
1665
  "color",
1548
1666
  "gamepad",
1549
- "backgroundMotion"
1667
+ "backgroundMotion",
1668
+ "archiveSearch"
1550
1669
  ]);
1551
1670
  if (options.title !== void 0 && (typeof options.title !== "string" || !options.title)) throw new TypeError("title must be a non-empty string");
1552
1671
  if (options.color !== void 0 && ![
@@ -1557,7 +1676,11 @@ function createCrosswaveTheme(options = {}, renderer) {
1557
1676
  "jade",
1558
1677
  "graphite"
1559
1678
  ].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`);
1679
+ for (const key of [
1680
+ "gamepad",
1681
+ "backgroundMotion",
1682
+ "archiveSearch"
1683
+ ]) if (options[key] !== void 0 && typeof options[key] !== "boolean") throw new TypeError(`${key} must be a boolean`);
1561
1684
  const ui = resolveCustomization(options);
1562
1685
  const publishedUi = pickCustomization(options);
1563
1686
  const categoryLabels = {
@@ -1592,6 +1715,7 @@ function createCrosswaveTheme(options = {}, renderer) {
1592
1715
  const model = {
1593
1716
  ...createThemePage(context, theme, options.title ?? ui.brand.name ?? "Dirwell"),
1594
1717
  ...publishedUi === void 0 ? {} : { ui: publishedUi },
1718
+ searchQueryHref: options.archiveSearch === false ? null : context.searchQueryHref ?? null,
1595
1719
  categories: categories.map(({ id, label, svg }) => ({
1596
1720
  id,
1597
1721
  label,