@dirwell/theme-ledger 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.
@@ -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,
@@ -346,7 +347,7 @@
346
347
  if (globalOpen) {
347
348
  if (event.key === "Escape") {
348
349
  event.preventDefault();
349
- globalOpen = false;
350
+ closeArchive();
350
351
  }
351
352
  return;
352
353
  }
@@ -401,6 +402,44 @@
401
402
  localStorage.setItem("dirwell-color-scheme", value);
402
403
  } catch {}
403
404
  }
405
+ let archiveDebounce,
406
+ archiveTrigger,
407
+ archivePending = false;
408
+ async function closeArchive() {
409
+ globalOpen = false;
410
+ await tick();
411
+ archiveTrigger?.focus();
412
+ }
413
+ function scheduleArchiveSearch() {
414
+ archiveAbort?.abort();
415
+ archiveRequest++;
416
+ archivePending = false;
417
+ clearTimeout(archiveDebounce);
418
+ archiveDebounce = setTimeout(() => {
419
+ archiveDebounce = undefined;
420
+ searchArchive();
421
+ }, 300);
422
+ }
423
+ $effect(() => {
424
+ if (!globalOpen || !model.searchQueryHref) return;
425
+ const timer = setInterval(() => {
426
+ if (
427
+ document.visibilityState === "visible" &&
428
+ globalQuery.trim() &&
429
+ !archivePending &&
430
+ archiveDebounce === undefined
431
+ )
432
+ searchArchive();
433
+ }, model.searchRefreshIntervalMs ?? 2000);
434
+ return () => {
435
+ clearInterval(timer);
436
+ clearTimeout(archiveDebounce);
437
+ archiveDebounce = undefined;
438
+ archivePending = false;
439
+ archiveAbort?.abort();
440
+ archiveRequest++;
441
+ };
442
+ });
404
443
  async function searchArchive() {
405
444
  const token = ++archiveRequest;
406
445
  archiveAbort?.abort();
@@ -413,7 +452,24 @@
413
452
  return;
414
453
  }
415
454
  globalStatus = ui.labels.searchingArchive;
455
+ archivePending = true;
416
456
  try {
457
+ if (model.searchQueryHref) {
458
+ const result = await queryLiveArchive(
459
+ model.searchQueryHref,
460
+ query,
461
+ local.signal,
462
+ );
463
+ if (token !== archiveRequest || local.signal.aborted) return;
464
+ globalRows = result.records;
465
+ globalStatus = result.truncated
466
+ ? label(ui, "truncatedResults", {
467
+ shown: result.records.length,
468
+ count: result.records.length + "+",
469
+ })
470
+ : label(ui, "archiveResults", { count: result.records.length });
471
+ return;
472
+ }
417
473
  const url = new URL(model.searchIndexHref, document.baseURI);
418
474
  let all =
419
475
  archiveCacheHref === url.href && Date.now() - archiveCacheAt < 60000
@@ -488,6 +544,8 @@
488
544
  globalStatus = ui.labels.archiveError;
489
545
  archiveFailed = true;
490
546
  }
547
+ } finally {
548
+ if (token === archiveRequest) archivePending = false;
491
549
  }
492
550
  }
493
551
  onMount(() => {
@@ -681,9 +739,10 @@
681
739
  ><option value="dark">{ui.labels.dark}</option></select
682
740
  ></label
683
741
  >
684
- {#if model.searchIndexHref}
742
+ {#if model.searchIndexHref || model.searchQueryHref}
685
743
  <button
686
- disabled={!ready || !model.searchIndexHref}
744
+ bind:this={archiveTrigger}
745
+ disabled={!ready}
687
746
  onclick={() => (globalOpen = true)}>{ui.labels.searchArchive}</button
688
747
  >
689
748
  {/if}
@@ -882,7 +941,15 @@
882
941
  </dialog>{/if}
883
942
  {#if globalOpen}<dialog
884
943
  bind:this={globalDialog}
885
- onclose={() => (globalOpen = false)}
944
+ aria-label={ui.labels.searchArchive}
945
+ onclose={closeArchive}
946
+ onkeydown={(event) => {
947
+ if (event.key === "Escape") {
948
+ event.preventDefault();
949
+ event.stopPropagation();
950
+ closeArchive();
951
+ }
952
+ }}
886
953
  >
887
954
  <h2>{ui.labels.searchArchive}</h2>
888
955
  <input
@@ -890,7 +957,7 @@
890
957
  bind:this={globalInput}
891
958
  type="search"
892
959
  bind:value={globalQuery}
893
- oninput={searchArchive}
960
+ oninput={scheduleArchiveSearch}
894
961
  />
895
962
  <p role="status">{globalStatus}</p>
896
963
  {#if archiveFailed}<button onclick={searchArchive}>{ui.labels.retry}</button
@@ -905,5 +972,5 @@
905
972
  >{:else}{record.path}{/if}
906
973
  </li>{/each}
907
974
  </ul>
908
- <button onclick={() => (globalOpen = false)}>{ui.labels.close}</button>
975
+ <button onclick={closeArchive}>{ui.labels.close}</button>
909
976
  </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.2",
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.2",
42
42
  "svelte": "5.57.2"
43
43
  },
44
44
  "devDependencies": {