@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.
@@ -133,6 +133,8 @@ interface ThemePage$1 {
133
133
  readonly manifestHref: string | null;
134
134
  readonly listingRevision?: string;
135
135
  readonly updatesHref: string | null;
136
+ readonly searchQueryHref?: string | null;
137
+ readonly searchRefreshIntervalMs?: number;
136
138
  readonly total: number;
137
139
  readonly entries: readonly ListingEntry[];
138
140
  readonly sort: Required<SortOptions>;
@@ -169,4 +171,4 @@ interface RowContext {
169
171
  }
170
172
  //#endregion
171
173
  export { ThemeAction as a, ThemeLabels as c, ThemePage as i, ListingEntry$1 as n, ThemeColumn as o, RowContext as r, ThemeCustomization as s, FrameworkRenderer as t };
172
- //# sourceMappingURL=model-VZ01cfyg.d.mts.map
174
+ //# sourceMappingURL=model-Dr7YgDTv.d.mts.map
package/dist/model.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { i as ThemePage, n as ListingEntry, r as RowContext, t as FrameworkRenderer } from "./model-VZ01cfyg.mjs";
1
+ import { i as ThemePage, n as ListingEntry, r as RowContext, t as FrameworkRenderer } from "./model-Dr7YgDTv.mjs";
2
2
  export { FrameworkRenderer, type ListingEntry, RowContext, ThemePage };
@@ -9,6 +9,7 @@ import {
9
9
  watch,
10
10
  nextTick,
11
11
  } from "vue";
12
+ import { queryLiveArchive } from "./shared/live-search.mjs";
12
13
  import { useGamepad } from "@vueuse/core";
13
14
  import { loadListing } from "@dirwell/core/client";
14
15
  import {
@@ -52,6 +53,93 @@ const query = ref(""),
52
53
  viewportHeight = ref(400),
53
54
  retryRequest = shallowRef(null),
54
55
  heights = shallowRef(new Map());
56
+ const archiveOpen = ref(false),
57
+ archiveQuery = ref(""),
58
+ archiveRows = shallowRef([]),
59
+ archiveStatus = ref(""),
60
+ archiveFailed = ref(false),
61
+ archiveTrigger = ref(),
62
+ archiveDialog = ref(),
63
+ archiveInput = ref();
64
+ let archiveAbort,
65
+ archiveRequest = 0,
66
+ archivePending = false,
67
+ archiveDebouncing = false;
68
+ async function searchArchive() {
69
+ const token = ++archiveRequest;
70
+ archiveAbort?.abort();
71
+ const local = (archiveAbort = new AbortController());
72
+ archiveFailed.value = false;
73
+ if (!archiveQuery.value.trim()) {
74
+ archiveRows.value = [];
75
+ archiveStatus.value = ui.value.labels.beginArchive;
76
+ return;
77
+ }
78
+ archiveStatus.value = ui.value.labels.searchingArchive;
79
+ archivePending = true;
80
+ try {
81
+ const result = await queryLiveArchive(
82
+ model.value.searchQueryHref,
83
+ archiveQuery.value.trim(),
84
+ local.signal,
85
+ );
86
+ if (local.signal.aborted || token !== archiveRequest) return;
87
+ archiveRows.value = result.records;
88
+ archiveStatus.value = result.truncated
89
+ ? label(ui.value, "truncatedResults", {
90
+ shown: result.records.length,
91
+ count: result.records.length + "+",
92
+ })
93
+ : label(ui.value, "archiveResults", { count: result.records.length });
94
+ } catch {
95
+ if (local.signal.aborted || token !== archiveRequest) return;
96
+ archiveRows.value = [];
97
+ archiveStatus.value = ui.value.labels.archiveError;
98
+ archiveFailed.value = true;
99
+ } finally {
100
+ if (token === archiveRequest) archivePending = false;
101
+ }
102
+ }
103
+ watch([archiveOpen, archiveQuery], (_, __, cleanup) => {
104
+ archiveAbort?.abort();
105
+ archiveRequest++;
106
+ archivePending = false;
107
+ if (!archiveOpen.value) return;
108
+ archiveDebouncing = true;
109
+ const debounce = setTimeout(() => {
110
+ archiveDebouncing = false;
111
+ searchArchive();
112
+ }, 300);
113
+ cleanup(() => {
114
+ clearTimeout(debounce);
115
+ archiveDebouncing = false;
116
+ });
117
+ });
118
+ watch(archiveOpen, async (open, _, cleanup) => {
119
+ if (!open) {
120
+ await nextTick();
121
+ archiveTrigger.value?.focus();
122
+ return;
123
+ }
124
+ await nextTick();
125
+ if (!archiveOpen.value) return;
126
+ archiveDialog.value?.showModal();
127
+ archiveInput.value?.focus();
128
+ const timer = setInterval(() => {
129
+ if (
130
+ document.visibilityState === "visible" &&
131
+ archiveQuery.value.trim() &&
132
+ !archivePending &&
133
+ !archiveDebouncing
134
+ )
135
+ searchArchive();
136
+ }, model.value.searchRefreshIntervalMs ?? 2000);
137
+ cleanup(() => {
138
+ clearInterval(timer);
139
+ archiveAbort?.abort();
140
+ archiveRequest++;
141
+ });
142
+ });
55
143
  const complete = computed(() => entries.value.length === model.value.total),
56
144
  membership = (entry) =>
57
145
  Array.isArray(entry.presentation?.categories) ? entry.presentation.categories : [];
@@ -369,6 +457,7 @@ function parent() {
369
457
  }
370
458
  }
371
459
  function keyboard(event) {
460
+ if (archiveOpen.value) return;
372
461
  if (event.isComposing || event.altKey || event.ctrlKey || event.metaKey) return;
373
462
  if (event.target?.closest?.("[data-dirwell-actions]")) return;
374
463
  const editing = /INPUT|TEXTAREA|SELECT/.test(event.target?.tagName ?? "");
@@ -401,6 +490,7 @@ function keyboard(event) {
401
490
  }
402
491
  }
403
492
  function controller(value) {
493
+ if (archiveOpen.value) return;
404
494
  pad.value = !!value;
405
495
  if (!value) {
406
496
  pressed.clear();
@@ -628,6 +718,7 @@ watchEffect(() => {
628
718
  }
629
719
  });
630
720
  onBeforeUnmount(() => {
721
+ archiveAbort?.abort();
631
722
  disposed = true;
632
723
  document.body.removeAttribute("data-cw-enhanced");
633
724
  abort?.abort();
@@ -725,6 +816,15 @@ onBeforeUnmount(() => {
725
816
  ><span>{{ ui.labels.search }}</span
726
817
  ><input ref="search" type="search" v-model="query" :disabled="!ready || !complete"
727
818
  /></label>
819
+ <button
820
+ class="cw-archive-trigger"
821
+ ref="archiveTrigger"
822
+ v-if="model.searchQueryHref"
823
+ :disabled="!ready"
824
+ @click="archiveOpen = true"
825
+ >
826
+ {{ ui.labels.searchArchive }}
827
+ </button>
728
828
  </div>
729
829
  <div
730
830
  class="cw-list-scroll"
@@ -914,4 +1014,35 @@ onBeforeUnmount(() => {
914
1014
  {{ paused ? ui.labels.resumeMotion : ui.labels.pauseMotion }}
915
1015
  </button>
916
1016
  </footer>
1017
+ <dialog
1018
+ v-if="archiveOpen"
1019
+ ref="archiveDialog"
1020
+ class="cw-archive"
1021
+ :aria-label="ui.labels.searchArchive"
1022
+ @close="archiveOpen = false"
1023
+ @keydown.esc.prevent.stop="archiveOpen = false"
1024
+ >
1025
+ <h2>{{ ui.labels.searchArchive }}</h2>
1026
+ <input
1027
+ ref="archiveInput"
1028
+ type="search"
1029
+ v-model="archiveQuery"
1030
+ :aria-label="ui.labels.archiveSearchInput"
1031
+ />
1032
+ <p role="status">{{ archiveStatus }}</p>
1033
+ <button v-if="archiveFailed" @click="searchArchive">{{ ui.labels.retry }}</button>
1034
+ <ul>
1035
+ <li v-for="record in archiveRows" :key="record.path">
1036
+ <a
1037
+ v-if="record.href"
1038
+ :href="record.href"
1039
+ :target="record.exitsExplorer ? '_blank' : undefined"
1040
+ :rel="record.exitsExplorer ? 'noopener' : undefined"
1041
+ >{{ record.path }}</a
1042
+ >
1043
+ <span v-else>{{ record.path }}</span>
1044
+ </li>
1045
+ </ul>
1046
+ <button @click="archiveOpen = false">{{ ui.labels.close }}</button>
1047
+ </dialog>
917
1048
  </template>
@@ -0,0 +1,28 @@
1
+ // ../shared-theme/src/live-search.ts
2
+ async function queryLiveArchive(href, query, signal) {
3
+ const url = new URL(href, document.baseURI);
4
+ if (url.origin !== location.origin) throw new Error("Unsafe search endpoint");
5
+ url.searchParams.set("q", query);
6
+ const response = await fetch(url, { signal, cache: "no-store" });
7
+ if (!response.ok) throw new Error("Archive search unavailable");
8
+ const data = await response.json();
9
+ 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")
10
+ throw new Error("Invalid archive results");
11
+ const records = data.results.map((entry) => {
12
+ 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")
13
+ throw new Error("Invalid archive record");
14
+ const target = entry.href === null ? null : new URL(entry.href, url);
15
+ if (target && !["http:", "https:"].includes(target.protocol))
16
+ throw new Error("Unsafe archive link");
17
+ return {
18
+ name: entry.name,
19
+ path: entry.path,
20
+ href: target?.href ?? null,
21
+ exitsExplorer: entry.kind !== "directory"
22
+ };
23
+ });
24
+ return { records, truncated: data.truncated };
25
+ }
26
+ export {
27
+ queryLiveArchive
28
+ };
@@ -402,6 +402,7 @@ async function fetchThemePage(dataHref, pageHref, theme, signal, fresh = false,
402
402
  resolveCustomization(data.ui);
403
403
  const result = {
404
404
  ...data,
405
+ searchQueryHref: link(data.searchQueryHref ?? null),
405
406
  updatesHref: link(data.updatesHref ?? null),
406
407
  entries,
407
408
  dataHref: source.href,
package/dist/styles.css CHANGED
@@ -22,6 +22,24 @@ body:not([data-cw-enhanced]) .cw-browser{padding-left:0}body:not([data-cw-enhanc
22
22
  .cw-list-scroll[data-virtual=true]{max-height:var(--cw-list-height,calc(100svh - 365px));min-height:160px}.cw-stage{margin-bottom:calc(var(--cw-footer-height,82px) + 24px)}
23
23
  @media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation:none!important;transition:none!important}::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation:none!important}.cw-row.cw-selected .cw-entry,.cw-category.cw-active,.cw-category:hover{transform:none}}
24
24
 
25
+ .cw-archive-trigger { min-height: 44px; padding: 8px 12px; color: var(--cw-text); background: transparent; border: 1px solid var(--cw-muted); border-radius: 4px; }
26
+ .cw-archive {
27
+ width: min(42rem, calc(100vw - 2rem));
28
+ max-height: calc(100dvh - 2rem);
29
+ overflow: auto;
30
+ padding: 1.5rem;
31
+ color: var(--cw-text);
32
+ background: var(--cw-bottom);
33
+ border: 1px solid var(--cw-muted);
34
+ border-radius: 1rem;
35
+ }
36
+ .cw-archive::backdrop { background: #000a; }
37
+ .cw-archive input { width: 100%; min-height: 2.75rem; }
38
+ .cw-archive a { color: var(--cw-muted); overflow-wrap: anywhere; }
39
+ .cw-archive button { min-height: 2.75rem; margin-block: .5rem; }
40
+ .cw-archive li { margin-block: .75rem; }
41
+
42
+
25
43
  .cw-directory{flex-wrap:wrap;gap:.35rem}.cw-list-head h2{font-size:17px;font-weight:400;margin:0}.cw-list-head [data-cw-count]{font-size:12px;color:var(--cw-muted)}
26
44
  .cw-motion{display:flex;align-items:center;background:transparent;border:1px solid #d7e9ff33;border-radius:4px;font-size:12px;min-height:44px;padding:6px 12px;white-space:nowrap}
27
45
  .cw-appearance{min-width:0}.cw-appearance select{max-width:116px}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dirwell/theme-crosswave",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Vue-owned media navigation with VueUse gamepad support.",
5
5
  "homepage": "https://vp-tw.github.io/dirwell/themes/",
6
6
  "bugs": {
@@ -38,7 +38,7 @@
38
38
  "tag": "latest"
39
39
  },
40
40
  "dependencies": {
41
- "@dirwell/core": "0.0.1",
41
+ "@dirwell/core": "0.0.2",
42
42
  "@vue/server-renderer": "3.5.43",
43
43
  "@vueuse/core": "15.0.0",
44
44
  "mime": "4.1.0",