@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/README.md +3 -1
- package/dist/client.js +16 -16
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +128 -4
- package/dist/index.mjs.map +1 -1
- package/dist/{model-VZ01cfyg.d.mts → model-Dr7YgDTv.d.mts} +3 -1
- package/dist/model.d.mts +1 -1
- package/dist/source/Crosswave.vue +131 -0
- package/dist/source/shared/live-search.mjs +28 -0
- package/dist/source/shared/navigation.mjs +1 -0
- package/dist/styles.css +18 -0
- package/package.json +2 -2
|
@@ -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-
|
|
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-
|
|
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.
|
|
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.
|
|
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",
|