@dirwell/theme-ledger 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.
@@ -1,5 +1,6 @@
1
1
  <script>
2
2
  import { onMount, tick, untrack } from "svelte";
3
+ import { queryLiveArchive } from "./shared/live-search.mjs";
3
4
  import {
4
5
  readView,
5
6
  createViewHistory,
@@ -168,17 +169,6 @@
168
169
  return () => cancelAnimationFrame(frame);
169
170
  });
170
171
  const isDirectory = directoryLike;
171
- function safeHref(record) {
172
- if (
173
- typeof location !== "undefined" &&
174
- location.protocol === "file:" &&
175
- isDirectory(record) &&
176
- !record.exitsExplorer &&
177
- record.href?.endsWith("/")
178
- )
179
- return record.href + "index.html";
180
- return record.href;
181
- }
182
172
  async function hydrateData() {
183
173
  if (complete || !model.manifestHref || location.protocol === "file:")
184
174
  return;
@@ -205,6 +195,7 @@
205
195
  }
206
196
  }
207
197
  async function navigate(event, record) {
198
+ selected = record.id;
208
199
  saveView();
209
200
  const dataHref = record.presentation?.folderDataHref;
210
201
  if (
@@ -346,7 +337,7 @@
346
337
  if (globalOpen) {
347
338
  if (event.key === "Escape") {
348
339
  event.preventDefault();
349
- globalOpen = false;
340
+ closeArchive();
350
341
  }
351
342
  return;
352
343
  }
@@ -401,6 +392,44 @@
401
392
  localStorage.setItem("dirwell-color-scheme", value);
402
393
  } catch {}
403
394
  }
395
+ let archiveDebounce,
396
+ archiveTrigger,
397
+ archivePending = false;
398
+ async function closeArchive() {
399
+ globalOpen = false;
400
+ await tick();
401
+ archiveTrigger?.focus();
402
+ }
403
+ function scheduleArchiveSearch() {
404
+ archiveAbort?.abort();
405
+ archiveRequest++;
406
+ archivePending = false;
407
+ clearTimeout(archiveDebounce);
408
+ archiveDebounce = setTimeout(() => {
409
+ archiveDebounce = undefined;
410
+ searchArchive();
411
+ }, 300);
412
+ }
413
+ $effect(() => {
414
+ if (!globalOpen || !model.searchQueryHref) return;
415
+ const timer = setInterval(() => {
416
+ if (
417
+ document.visibilityState === "visible" &&
418
+ globalQuery.trim() &&
419
+ !archivePending &&
420
+ archiveDebounce === undefined
421
+ )
422
+ searchArchive();
423
+ }, model.searchRefreshIntervalMs ?? 2000);
424
+ return () => {
425
+ clearInterval(timer);
426
+ clearTimeout(archiveDebounce);
427
+ archiveDebounce = undefined;
428
+ archivePending = false;
429
+ archiveAbort?.abort();
430
+ archiveRequest++;
431
+ };
432
+ });
404
433
  async function searchArchive() {
405
434
  const token = ++archiveRequest;
406
435
  archiveAbort?.abort();
@@ -413,7 +442,24 @@
413
442
  return;
414
443
  }
415
444
  globalStatus = ui.labels.searchingArchive;
445
+ archivePending = true;
416
446
  try {
447
+ if (model.searchQueryHref) {
448
+ const result = await queryLiveArchive(
449
+ model.searchQueryHref,
450
+ query,
451
+ local.signal,
452
+ );
453
+ if (token !== archiveRequest || local.signal.aborted) return;
454
+ globalRows = result.records;
455
+ globalStatus = result.truncated
456
+ ? label(ui, "truncatedResults", {
457
+ shown: result.records.length,
458
+ count: result.records.length + "+",
459
+ })
460
+ : label(ui, "archiveResults", { count: result.records.length });
461
+ return;
462
+ }
417
463
  const url = new URL(model.searchIndexHref, document.baseURI);
418
464
  let all =
419
465
  archiveCacheHref === url.href && Date.now() - archiveCacheAt < 60000
@@ -488,6 +534,8 @@
488
534
  globalStatus = ui.labels.archiveError;
489
535
  archiveFailed = true;
490
536
  }
537
+ } finally {
538
+ if (token === archiveRequest) archivePending = false;
491
539
  }
492
540
  }
493
541
  onMount(() => {
@@ -681,9 +729,10 @@
681
729
  ><option value="dark">{ui.labels.dark}</option></select
682
730
  ></label
683
731
  >
684
- {#if model.searchIndexHref}
732
+ {#if model.searchIndexHref || model.searchQueryHref}
685
733
  <button
686
- disabled={!ready || !model.searchIndexHref}
734
+ bind:this={archiveTrigger}
735
+ disabled={!ready}
687
736
  onclick={() => (globalOpen = true)}>{ui.labels.searchArchive}</button
688
737
  >
689
738
  {/if}
@@ -756,7 +805,7 @@
756
805
  {#if column === "name"}<span class="identity"
757
806
  >{#if entry.href}<a
758
807
  class="name"
759
- href={safeHref(entry)}
808
+ href={entry.href}
760
809
  target={entry.exitsExplorer ? "_blank" : undefined}
761
810
  rel={entry.exitsExplorer ? "noopener" : undefined}
762
811
  onclick={(event) => navigate(event, entry)}
@@ -808,7 +857,7 @@
808
857
  })}{/if}
809
858
  {#each ui.actions as action}
810
859
  {#if action === "download" && entry.href && !isDirectory(entry) && !entry.link?.unavailable}<a
811
- href={safeHref(entry)}
860
+ href={entry.href}
812
861
  download>{ui.labels.download}</a
813
862
  >
814
863
  {:else if action !== "download"}<button
@@ -882,7 +931,15 @@
882
931
  </dialog>{/if}
883
932
  {#if globalOpen}<dialog
884
933
  bind:this={globalDialog}
885
- onclose={() => (globalOpen = false)}
934
+ aria-label={ui.labels.searchArchive}
935
+ onclose={closeArchive}
936
+ onkeydown={(event) => {
937
+ if (event.key === "Escape") {
938
+ event.preventDefault();
939
+ event.stopPropagation();
940
+ closeArchive();
941
+ }
942
+ }}
886
943
  >
887
944
  <h2>{ui.labels.searchArchive}</h2>
888
945
  <input
@@ -890,7 +947,7 @@
890
947
  bind:this={globalInput}
891
948
  type="search"
892
949
  bind:value={globalQuery}
893
- oninput={searchArchive}
950
+ oninput={scheduleArchiveSearch}
894
951
  />
895
952
  <p role="status">{globalStatus}</p>
896
953
  {#if archiveFailed}<button onclick={searchArchive}>{ui.labels.retry}</button
@@ -905,5 +962,5 @@
905
962
  >{:else}{record.path}{/if}
906
963
  </li>{/each}
907
964
  </ul>
908
- <button onclick={() => (globalOpen = false)}>{ui.labels.close}</button>
965
+ <button onclick={closeArchive}>{ui.labels.close}</button>
909
966
  </dialog>{/if}
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dirwell/theme-ledger",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Svelte-owned searchable directory explorer.",
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.3",
42
42
  "svelte": "5.57.2"
43
43
  },
44
44
  "devDependencies": {