@dirwell/theme-ledger 0.0.1-alpha.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.mjs CHANGED
@@ -4,7 +4,7 @@ import { copyJsonObject, escapeHtml, projectListingEntry } from "@dirwell/core";
4
4
  import path from "node:path";
5
5
  import { render } from "svelte/server";
6
6
  import * as $ from "svelte/internal/server";
7
- import { onMount, untrack } from "svelte";
7
+ import { onMount, tick, untrack } from "svelte";
8
8
  import { ListingDataError, loadListing, parseListingEntries } from "@dirwell/core/client";
9
9
  //#region ../shared-theme/src/page.ts
10
10
  const pageAsset = (theme, directory, page = 1) => `${theme}-page-${createHash("sha256").update(directory).digest("hex").slice(0, 24)}-${page}.json`;
@@ -36,7 +36,7 @@ function createThemePage(context, theme, title) {
36
36
  documentBaseHref: context.documentBaseHref,
37
37
  directoryPath
38
38
  };
39
- const dataForDirectory = (logicalPath) => folderDataHref({
39
+ const dataForDirectory = (logicalPath) => context.hasPageForDirectory?.(logicalPath) === false ? null : folderDataHref({
40
40
  id: logicalPath,
41
41
  name: logicalPath,
42
42
  kind: "directory",
@@ -67,6 +67,10 @@ function createThemePage(context, theme, title) {
67
67
  manifestHref: context.listing?.manifestHref ?? null,
68
68
  ...context.listing ? { listingRevision: context.listing.revision } : {},
69
69
  updatesHref: context.updatesHref ?? null,
70
+ ...context.searchQueryHref ? {
71
+ searchQueryHref: context.searchQueryHref,
72
+ searchRefreshIntervalMs: context.searchRefreshIntervalMs
73
+ } : {},
70
74
  total: context.listing?.page.total ?? entries.length,
71
75
  entries,
72
76
  sort: context.sort,
@@ -79,6 +83,139 @@ function createThemePage(context, theme, title) {
79
83
  };
80
84
  }
81
85
  //#endregion
86
+ //#region ../shared-theme/src/view-state.ts
87
+ const supportedTypes = [
88
+ "directory",
89
+ "file",
90
+ "link"
91
+ ];
92
+ function validSort(value) {
93
+ if (typeof value !== "object" || value === null) return false;
94
+ const sort = value;
95
+ return [
96
+ "name",
97
+ "size",
98
+ "modified"
99
+ ].includes(String(sort.field)) && ["asc", "desc"].includes(String(sort.direction)) && [
100
+ "natural",
101
+ "locale",
102
+ "unicode"
103
+ ].includes(String(sort.nameMode)) && typeof sort.directoriesFirst === "boolean";
104
+ }
105
+ function position(value) {
106
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.min(value, 1e9)) : 0;
107
+ }
108
+ /** URL controls are shareable; selection and scroll belong only to one history entry. */
109
+ function readView(href, theme, path, defaults, categories = [], historyState = null) {
110
+ const params = new URL(href).searchParams;
111
+ const result = {
112
+ ...defaults,
113
+ query: (params.get("dw.q") ?? "").slice(0, 4096),
114
+ selected: null,
115
+ windowY: 0,
116
+ listTop: 0
117
+ };
118
+ if (theme === "ledger") {
119
+ const parts = params.get("dw.sort")?.split(":");
120
+ if (parts?.length === 4) {
121
+ const sort = {
122
+ field: parts[0],
123
+ direction: parts[1],
124
+ nameMode: parts[2],
125
+ directoriesFirst: parts[3] === "folders"
126
+ };
127
+ if (["folders", "mixed"].includes(parts[3]) && validSort(sort)) result.sort = sort;
128
+ }
129
+ if (params.has("dw.types")) {
130
+ const types = params.get("dw.types").split(",").filter(Boolean);
131
+ if (types.every((type) => supportedTypes.includes(type))) result.types = [...new Set(types)];
132
+ }
133
+ } else {
134
+ const category = params.get("dw.category");
135
+ if (category !== null && categories.includes(category)) result.category = category;
136
+ }
137
+ if (typeof historyState === "object" && historyState !== null && "dirwell" in historyState) {
138
+ const state = historyState.dirwell;
139
+ if (typeof state === "object" && state !== null && "version" in state && state.version === 1 && "theme" in state && state.theme === theme && "path" in state && state.path === path && "view" in state && typeof state.view === "object" && state.view !== null) {
140
+ const view = state.view;
141
+ result.selected = typeof view.selected === "string" && view.selected.length <= 4096 ? view.selected : null;
142
+ result.windowY = position(view.windowY);
143
+ result.listTop = position(view.listTop);
144
+ }
145
+ }
146
+ return result;
147
+ }
148
+ function viewHref(href, theme, view, defaults) {
149
+ const url = new URL(href);
150
+ for (const key of [
151
+ "dw.q",
152
+ "dw.sort",
153
+ "dw.types",
154
+ "dw.category"
155
+ ]) url.searchParams.delete(key);
156
+ if (view.query) url.searchParams.set("dw.q", view.query.slice(0, 4096));
157
+ if (theme === "ledger") {
158
+ if (view.sort && [
159
+ "field",
160
+ "direction",
161
+ "nameMode",
162
+ "directoriesFirst"
163
+ ].some((key) => view.sort[key] !== defaults.sort?.[key])) {
164
+ const sort = view.sort;
165
+ url.searchParams.set("dw.sort", `${sort.field}:${sort.direction}:${sort.nameMode}:${sort.directoriesFirst ? "folders" : "mixed"}`);
166
+ }
167
+ if (view.types && supportedTypes.some((type) => view.types.includes(type) !== (defaults.types ?? supportedTypes).includes(type))) url.searchParams.set("dw.types", view.types.join(","));
168
+ } else if (view.category && view.category !== defaults.category) url.searchParams.set("dw.category", view.category);
169
+ return url.href;
170
+ }
171
+ /** History failures never require discarding a successfully loaded view. */
172
+ function createViewHistory(theme) {
173
+ let committedHref = location.href;
174
+ let committedState = history.state;
175
+ const stateFor = (dataHref, path, view) => ({
176
+ ...typeof history.state === "object" && history.state !== null ? history.state : {},
177
+ dataHref,
178
+ dirwell: {
179
+ version: 1,
180
+ theme,
181
+ path,
182
+ ...view === void 0 ? {} : { view: {
183
+ query: view.query,
184
+ ...view.sort === void 0 ? {} : { sort: { ...view.sort } },
185
+ ...view.types === void 0 ? {} : { types: [...view.types] },
186
+ category: view.category ?? null,
187
+ selected: view.selected ?? null,
188
+ windowY: view.windowY ?? 0,
189
+ listTop: view.listTop ?? 0
190
+ } }
191
+ }
192
+ });
193
+ return {
194
+ save(dataHref, path, view, defaults) {
195
+ if (location.href !== committedHref) return;
196
+ const href = viewHref(committedHref, theme, view, defaults);
197
+ const state = stateFor(dataHref, path, view);
198
+ try {
199
+ history.replaceState(state, "", href);
200
+ committedHref = href;
201
+ committedState = state;
202
+ } catch {}
203
+ },
204
+ commit(dataHref, path, href, push) {
205
+ const state = stateFor(dataHref, path);
206
+ if (push) history.pushState(state, "", href);
207
+ else history.replaceState(state, "", href);
208
+ committedHref = href;
209
+ committedState = state;
210
+ },
211
+ rollback() {
212
+ if (location.href !== committedHref) try {
213
+ history.replaceState(committedState, "", committedHref);
214
+ } catch {}
215
+ }
216
+ };
217
+ }
218
+ //#endregion
82
219
  //#region ../shared-theme/src/icons.ts
83
220
  const paths = {
84
221
  directory: "<path d=\"M3 7V5h6l2 3h10v12H3Z\"/><path d=\"M3 10h18\"/>",
@@ -90,6 +227,250 @@ function entryIcon(kind) {
90
227
  return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.45" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths[kind]}</svg>`;
91
228
  }
92
229
  //#endregion
230
+ //#region ../shared-theme/src/customization.ts
231
+ /** Small serializable inputs shared by official themes; layout stays theme-owned. */
232
+ const defaultLabels = {
233
+ other: "other",
234
+ crosswaveKeyboardHelp: "Arrow keys navigate · Enter confirms · Backspace returns",
235
+ brokenLink: "broken link",
236
+ unavailableLink: "unavailable link",
237
+ cycleLink: "cycle link",
238
+ azure: "azure",
239
+ violet: "violet",
240
+ amber: "amber",
241
+ rose: "rose",
242
+ jade: "jade",
243
+ graphite: "graphite",
244
+ allFiles: "All files",
245
+ categoryFolders: "Folders",
246
+ categoryPhotos: "Photos",
247
+ categoryMusic: "Music",
248
+ categoryVideos: "Videos",
249
+ categoryDocuments: "Documents",
250
+ categoryOther: "Other",
251
+ indexOf: "Index of {path}",
252
+ home: "Home",
253
+ breadcrumb: "Breadcrumb",
254
+ archive: "File archive",
255
+ searchDirectory: "Search this directory",
256
+ searchFiles: "Search files",
257
+ search: "Search",
258
+ types: "Types",
259
+ directory: "directory",
260
+ file: "file",
261
+ link: "link",
262
+ folder: "Folder",
263
+ sort: "Sort",
264
+ name: "Name",
265
+ size: "Size",
266
+ modified: "Modified",
267
+ names: "Names",
268
+ natural: "Natural",
269
+ locale: "Locale",
270
+ unicode: "Unicode",
271
+ order: "Order",
272
+ ascending: "Ascending",
273
+ descending: "Descending",
274
+ directoriesFirst: "Folders first",
275
+ appearance: "Appearance",
276
+ system: "System",
277
+ light: "Light",
278
+ dark: "Dark",
279
+ searchArchive: "Search archive",
280
+ archiveSearchInput: "Search archive files",
281
+ entries: "{count} entries",
282
+ pageEntries: "{total} entries · {count} on this page",
283
+ loading: "Loading…",
284
+ noMatches: "No matching entries.",
285
+ empty: "This directory is empty.",
286
+ pagination: "Pagination",
287
+ previous: "Previous",
288
+ next: "Next",
289
+ page: "Page {page} of {pages}",
290
+ keyboardHelp: "Arrow keys select files · Home / End jump · / searches · Backspace opens parent",
291
+ modifiedTime: "Modified time",
292
+ close: "Close",
293
+ unknown: "Unknown",
294
+ unavailable: "Target unavailable",
295
+ beginArchive: "Enter a search to begin.",
296
+ searchingArchive: "Searching archive…",
297
+ archiveResults: "{count} results",
298
+ truncatedResults: "Showing {shown} of {count} results",
299
+ archiveError: "Could not load archive search. Try again.",
300
+ listingError: "Could not load complete directory data. Native page links remain available.",
301
+ folderError: "Could not open the folder. Its native link remains available.",
302
+ updatesError: "Live updates disconnected. Refresh to retry.",
303
+ retry: "Retry",
304
+ openNative: "Open folder directly",
305
+ categories: "File categories",
306
+ items: "{count} items",
307
+ filesIn: "Files in {path}",
308
+ controller: "Controller · Confirm / Back",
309
+ color: "Color",
310
+ pauseMotion: "Pause motion",
311
+ resumeMotion: "Resume motion",
312
+ copyLink: "Copy link",
313
+ copyPath: "Copy path",
314
+ download: "Download",
315
+ copied: "Copied.",
316
+ copyError: "Could not copy. Use the native link or select the path.",
317
+ repository: "Repository",
318
+ author: "by",
319
+ target: "Target",
320
+ newTab: "opens in a new tab"
321
+ };
322
+ function resolveCustomization(options = {}) {
323
+ const language = options.language === void 0 ? "en" : options.language, locale = options.locale === void 0 ? "en-US" : options.locale, timeZone = options.timeZone === void 0 ? "UTC" : options.timeZone;
324
+ for (const [key, value] of Object.entries({
325
+ language,
326
+ locale,
327
+ timeZone
328
+ })) if (typeof value !== "string" || !value) throw new TypeError(`${key} must be a non-empty string`);
329
+ new Intl.DateTimeFormat(locale, { timeZone });
330
+ const direction = options.direction ?? "ltr";
331
+ if (direction !== "ltr" && direction !== "rtl") throw new TypeError("direction must be ltr or rtl");
332
+ const labels = { ...defaultLabels };
333
+ if (options.labels !== void 0) {
334
+ if (typeof options.labels !== "object" || options.labels === null || Array.isArray(options.labels)) throw new TypeError("labels must be an object");
335
+ for (const [key, value] of Object.entries(options.labels)) {
336
+ if (!Object.hasOwn(labels, key) || typeof value !== "string") throw new TypeError(`Unsupported label or value: ${key}`);
337
+ Object.assign(labels, { [key]: value });
338
+ }
339
+ }
340
+ const brand = options.brand ?? {};
341
+ if (typeof brand !== "object" || brand === null || Array.isArray(brand)) throw new TypeError("brand must be an object");
342
+ for (const [key, value] of Object.entries(brand)) {
343
+ if (![
344
+ "name",
345
+ "logoUrl",
346
+ "logoAlt",
347
+ "footerText",
348
+ "footerUrl",
349
+ "faviconUrl"
350
+ ].includes(key) || typeof value !== "string") throw new TypeError(`Unsupported brand option or value: ${key}`);
351
+ if (key.endsWith("Url") && !/^https?:\/\//i.test(value) && !(value.startsWith("/") && !value.startsWith("//"))) throw new TypeError(`brand.${key} must be an HTTP(S) or root-relative URL`);
352
+ }
353
+ const columns = options.columns ?? [
354
+ "name",
355
+ "size",
356
+ "modified"
357
+ ];
358
+ if (!Array.isArray(columns) || !columns.includes("name")) throw new TypeError("columns must contain name and unique supported columns");
359
+ const columnIds = /* @__PURE__ */ new Set();
360
+ for (const column of columns) {
361
+ let id;
362
+ if (typeof column === "string") {
363
+ if (![
364
+ "name",
365
+ "size",
366
+ "modified"
367
+ ].includes(column)) throw new TypeError("Unsupported column");
368
+ id = column;
369
+ } else {
370
+ if (typeof column !== "object" || column === null || Object.keys(column).some((key) => !["dataKey", "label"].includes(key)) || typeof column.dataKey !== "string" || !column.dataKey || typeof column.label !== "string" || !column.label) throw new TypeError("Data columns require dataKey and label");
371
+ id = "data:" + column.dataKey;
372
+ }
373
+ if (columnIds.has(id)) throw new TypeError("columns must contain unique supported columns");
374
+ columnIds.add(id);
375
+ }
376
+ const actions = options.actions ?? [];
377
+ if (!Array.isArray(actions) || new Set(actions).size !== actions.length || actions.some((key) => ![
378
+ "copy-link",
379
+ "copy-path",
380
+ "download"
381
+ ].includes(key))) throw new TypeError("actions must contain unique supported actions");
382
+ const cssVariables = options.cssVariables ?? {};
383
+ if (typeof cssVariables !== "object" || cssVariables === null || Array.isArray(cssVariables)) throw new TypeError("cssVariables must be an object");
384
+ for (const [key, value] of Object.entries(cssVariables)) if (!/^--[a-zA-Z][a-zA-Z0-9_-]*$/.test(key) || typeof value !== "string" || /[{};<>]/.test(value)) throw new TypeError(`Invalid CSS variable: ${key}`);
385
+ return {
386
+ language,
387
+ dateOptions: {
388
+ ...options.locale === void 0 ? {} : { locale: options.locale },
389
+ ...options.timeZone === void 0 ? {} : { timeZone: options.timeZone }
390
+ },
391
+ direction,
392
+ locale,
393
+ timeZone,
394
+ labels,
395
+ brand: { ...brand },
396
+ columns: columns.map((column) => typeof column === "string" ? column : { ...column }),
397
+ actions: [...actions],
398
+ cssVariables: { ...cssVariables }
399
+ };
400
+ }
401
+ resolveCustomization();
402
+ function label(ui, key, values = {}) {
403
+ return ui.labels[key].replace(/\{([a-zA-Z]+)\}/g, (match, name) => Object.hasOwn(values, name) ? String(values[name]) : match);
404
+ }
405
+ const dateFormats = /* @__PURE__ */ new Map();
406
+ function formattedDate(value, ui, local = false) {
407
+ const date = new Date(value);
408
+ if (Number.isNaN(date.getTime())) return ui.labels.unknown;
409
+ if (ui.dateOptions.locale === void 0 && ui.dateOptions.timeZone === void 0) return local ? date.toLocaleString(void 0, {
410
+ dateStyle: "short",
411
+ timeStyle: "short"
412
+ }) : date.toISOString().slice(0, 16).replace("T", " ") + " UTC";
413
+ const locale = ui.dateOptions.locale, timeZone = ui.dateOptions.timeZone ?? (local ? void 0 : "UTC");
414
+ const key = String(locale) + "\0" + String(timeZone);
415
+ let formatter = dateFormats.get(key);
416
+ if (formatter === void 0) {
417
+ formatter = new Intl.DateTimeFormat(locale, {
418
+ timeZone,
419
+ dateStyle: "short",
420
+ timeStyle: "short"
421
+ });
422
+ if (dateFormats.size >= 32) dateFormats.delete(dateFormats.keys().next().value);
423
+ dateFormats.set(key, formatter);
424
+ }
425
+ return formatter.format(date);
426
+ }
427
+ function customizationCss(ui) {
428
+ const declarations = Object.entries(ui.cssVariables).map(([key, value]) => `${key}:${value}`).join(";");
429
+ return declarations ? `:root:root{${declarations}}${ui.cssVariables["--dw-font-family"] ? "body{font-family:var(--dw-font-family)}" : ""}` : "";
430
+ }
431
+ /** Publish only supplied customization instead of repeating defaults in every page. */
432
+ function pickCustomization(options) {
433
+ const result = {};
434
+ for (const key of [
435
+ "language",
436
+ "direction",
437
+ "locale",
438
+ "timeZone",
439
+ "labels",
440
+ "brand",
441
+ "columns",
442
+ "actions",
443
+ "cssVariables"
444
+ ]) if (options[key] !== void 0) {
445
+ const value = options[key];
446
+ Object.assign(result, { [key]: Array.isArray(value) ? value.map((item) => typeof item === "object" ? { ...item } : item) : typeof value === "object" && value !== null ? { ...value } : value });
447
+ }
448
+ return Object.keys(result).length ? result : void 0;
449
+ }
450
+ /** Built-in factories fail on typos instead of silently ignoring configuration. */
451
+ function assertThemeOptions(value, extraKeys) {
452
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("Theme options must be an object");
453
+ const allowed = [
454
+ "language",
455
+ "direction",
456
+ "locale",
457
+ "timeZone",
458
+ "labels",
459
+ "brand",
460
+ "columns",
461
+ "actions",
462
+ "cssVariables",
463
+ ...extraKeys
464
+ ];
465
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) throw new TypeError(`Unsupported theme option: ${key}`);
466
+ }
467
+ function columnLabel(column, ui) {
468
+ return typeof column === "string" ? ui.labels[column] : column.label;
469
+ }
470
+ function publicDataValue(value) {
471
+ return value === void 0 || value === null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
472
+ }
473
+ //#endregion
93
474
  //#region ../shared-theme/src/navigation.ts
94
475
  /** Folder snapshots resolve relative URLs before History changes the document URL. */
95
476
  async function fetchThemePage(dataHref, pageHref, theme, signal, fresh = false, attempt = 0) {
@@ -105,6 +486,25 @@ async function fetchThemePage(dataHref, pageHref, theme, signal, fresh = false,
105
486
  const data = value;
106
487
  if (data.version !== 1 || data.theme !== theme || typeof data.title !== "string" || typeof data.path !== "string" || !Number.isSafeInteger(data.total) || Number(data.total) < 0 || !Array.isArray(data.breadcrumbs)) throw new ListingDataError("Unsupported folder snapshot");
107
488
  if (data.metadataHead !== void 0 && typeof data.metadataHead !== "string") throw new ListingDataError("Invalid folder metadata");
489
+ const sort = data.sort;
490
+ if (!sort || typeof sort !== "object" || ![
491
+ "name",
492
+ "size",
493
+ "modified"
494
+ ].includes(String(sort.field)) || !["asc", "desc"].includes(String(sort.direction)) || ![
495
+ "natural",
496
+ "locale",
497
+ "unicode"
498
+ ].includes(String(sort.nameMode)) || typeof sort.directoriesFirst !== "boolean") throw new ListingDataError("Invalid folder order");
499
+ if (theme === "crosswave") {
500
+ const categories = data.categories;
501
+ const ids = /* @__PURE__ */ new Set();
502
+ if (!Array.isArray(categories) || categories.length === 0 || categories.some((category) => {
503
+ if (typeof category !== "object" || category === null || typeof category.id !== "string" || !category.id || ids.has(category.id) || typeof category.label !== "string" || typeof category.svg !== "string") return true;
504
+ ids.add(category.id);
505
+ return false;
506
+ })) throw new ListingDataError("Invalid folder categories");
507
+ }
108
508
  const base = document.querySelector("base")?.href ?? pageHref;
109
509
  const link = (raw) => {
110
510
  if (raw === null) return null;
@@ -121,8 +521,20 @@ async function fetchThemePage(dataHref, pageHref, theme, signal, fresh = false,
121
521
  } } : {}
122
522
  }));
123
523
  if (entries.length > Number(data.total)) throw new ListingDataError("Invalid folder count");
524
+ if (data.page !== null) {
525
+ const page = data.page;
526
+ if (!page || typeof page !== "object" || !Number.isSafeInteger(page.number) || Number(page.number) < 1 || !Number.isSafeInteger(page.count) || Number(page.count) < Number(page.number) || !Number.isSafeInteger(page.offset) || Number(page.offset) < 0 || Number(page.offset) > Number(data.total) || page.total !== data.total) throw new ListingDataError("Invalid folder pagination");
527
+ data.page = {
528
+ ...page,
529
+ href: link(page.href),
530
+ previousHref: link(page.previousHref),
531
+ nextHref: link(page.nextHref)
532
+ };
533
+ }
534
+ if (data.ui !== void 0) resolveCustomization(data.ui);
124
535
  const result = {
125
536
  ...data,
537
+ searchQueryHref: link(data.searchQueryHref ?? null),
126
538
  updatesHref: link(data.updatesHref ?? null),
127
539
  entries,
128
540
  dataHref: source.href,
@@ -140,10 +552,10 @@ async function fetchThemePage(dataHref, pageHref, theme, signal, fresh = false,
140
552
  };
141
553
  })
142
554
  };
143
- if (fresh && result.manifestHref) {
555
+ if (result.manifestHref) {
144
556
  const loaded = await loadListing(result.manifestHref, {
145
557
  signal,
146
- cache: "no-store"
558
+ cache: fresh ? "no-store" : "default"
147
559
  });
148
560
  if (loaded.manifest.total !== data.total || data.listingRevision !== void 0 && loaded.manifest.revision !== data.listingRevision) {
149
561
  if (attempt < 2) return fetchThemePage(dataHref, pageHref, theme, signal, true, attempt + 1);
@@ -266,19 +678,11 @@ function size(bytes) {
266
678
  }
267
679
  return `${value.toFixed(index === 0 || value >= 100 ? 0 : 1)} ${units[index]}`;
268
680
  }
269
- function modified(value, local = false) {
270
- const date = new Date(value);
271
- if (Number.isNaN(date.getTime())) return "Unknown";
272
- return local ? date.toLocaleString(void 0, {
273
- dateStyle: "short",
274
- timeStyle: "short"
275
- }) : `${date.toISOString().slice(0, 16).replace("T", " ")} UTC`;
276
- }
277
681
  //#endregion
278
682
  //#region .generated/App-server.mjs
279
683
  function Ledger($$renderer, $$props) {
280
684
  $$renderer.component(($$renderer) => {
281
- let { model: initialModel, header, row, footer } = $$props;
685
+ let { model: initialModel, header, row, actions, footer } = $$props;
282
686
  const snapshot = untrack(() => restoreThemePage(initialModel, Boolean(import.meta.hot)));
283
687
  let model = snapshot;
284
688
  let entries = snapshot.entries;
@@ -294,7 +698,15 @@ function Ledger($$renderer, $$props) {
294
698
  let ready = false;
295
699
  let busy = false;
296
700
  let error = "";
701
+ let failure = null;
297
702
  let scrollTop = 0;
703
+ let viewportHeight = 800;
704
+ resolveCustomization(snapshot.ui).labels.beginArchive;
705
+ let ui = $.derived(() => resolveCustomization(model.ui));
706
+ let countParts = $.derived(() => label(ui(), complete() ? "entries" : "pageEntries", {
707
+ count: "__COUNT__",
708
+ total: model.total
709
+ }).split("__COUNT__"));
298
710
  let heights = /* @__PURE__ */ new Map();
299
711
  $.derived(() => ready && model.updatesHref ? new URL(model.updatesHref, document.baseURI).href : null);
300
712
  let complete = $.derived(() => entries.length === model.total);
@@ -315,9 +727,40 @@ function Ledger($$renderer, $$props) {
315
727
  }
316
728
  return Math.max(0, low - 5);
317
729
  });
318
- let end = $.derived(() => virtual() ? Math.min(rows().length, start() + 35) : rows().length);
730
+ let end = $.derived(() => {
731
+ if (!virtual()) return rows().length;
732
+ let low = 0, high = rows().length;
733
+ while (low < high) {
734
+ const middle = low + high >> 1;
735
+ if (offsets()[middle + 1] <= scrollTop + viewportHeight) low = middle + 1;
736
+ else high = middle;
737
+ }
738
+ return Math.min(rows().length, Math.max(start() + 35, low + 5));
739
+ });
319
740
  let navigation = 0;
320
741
  let abort;
742
+ let viewHistory;
743
+ let retryRequest = null;
744
+ const viewDefaults = (page) => ({
745
+ query: "",
746
+ sort: page.sort,
747
+ types: [
748
+ "directory",
749
+ "file",
750
+ "link"
751
+ ]
752
+ });
753
+ function saveView() {
754
+ if (!viewHistory) return;
755
+ viewHistory.save(new URL(model.dataHref, document.baseURI).href, model.path, {
756
+ query,
757
+ types,
758
+ sort,
759
+ selected,
760
+ windowY: window.scrollY,
761
+ listTop: scrollTop
762
+ }, viewDefaults(model));
763
+ }
321
764
  const isDirectory = directoryLike;
322
765
  function safeHref(record) {
323
766
  if (typeof location !== "undefined" && location.protocol === "file:" && isDirectory(record) && !record.exitsExplorer && record.href?.endsWith("/")) return record.href + "index.html";
@@ -334,38 +777,64 @@ function Ledger($$renderer, $$props) {
334
777
  });
335
778
  if (token === navigation && !local.signal.aborted) entries = loaded.entries;
336
779
  } catch (cause) {
337
- if (token === navigation && !local.signal.aborted) error = "Could not load complete directory data. Native page links remain available.";
780
+ if (token === navigation && !local.signal.aborted) {
781
+ error = ui().labels.listingError;
782
+ failure = "listing";
783
+ }
338
784
  } finally {
339
785
  if (token === navigation) busy = false;
340
786
  }
341
787
  }
342
- async function openFolder(dataHref, url, push, refresh = false) {
788
+ async function openFolder(dataHref, url, push, refresh = false, restoreState = null, fresh = false) {
789
+ if (push) saveView();
343
790
  const token = ++navigation;
344
791
  abort?.abort();
345
792
  const local = abort = new AbortController();
346
793
  busy = true;
347
794
  error = "";
795
+ failure = null;
348
796
  try {
349
- const next = await fetchThemePage(dataHref, url, "ledger", local.signal, refresh);
797
+ const next = await fetchThemePage(dataHref, url, "ledger", local.signal, refresh || fresh);
350
798
  let nextEntries = next.entries;
351
799
  if (refresh && next.manifestHref && nextEntries.length !== next.total) nextEntries = (await loadListing(next.manifestHref, {
352
800
  signal: local.signal,
353
801
  cache: "no-store"
354
802
  })).entries;
355
803
  if (token !== navigation || local.signal.aborted) return;
804
+ if (!refresh) viewHistory.commit(next.dataHref, next.path, url, push);
356
805
  model = next;
357
806
  entries = nextEntries;
807
+ retryRequest = null;
358
808
  if (!refresh) {
359
- query = "";
360
- selected = null;
361
- scrollTop = 0;
809
+ const view = readView(url, "ledger", next.path, viewDefaults(next), [], restoreState);
810
+ query = view.query;
811
+ types = view.types;
812
+ sort = view.sort;
813
+ selected = view.selected;
814
+ scrollTop = view.listTop;
362
815
  }
363
- if (push) history.pushState({ dataHref: next.dataHref }, "", url);
364
816
  applyThemeMetadata(next, url);
365
817
  await hydrateData();
366
- if (!refresh) window.scrollTo({ top: 0 });
818
+ if (!refresh) {
819
+ const view = readView(url, "ledger", next.path, viewDefaults(next), [], restoreState);
820
+ await tick();
821
+ if (selected && !rows().some((entry) => entry.id === selected)) selected = null;
822
+ window.scrollTo({ top: view.windowY });
823
+ if (restoreState && selected);
824
+ }
367
825
  } catch (cause) {
368
- if (token === navigation && !local.signal.aborted) error = "Could not open this folder. Open its native link or try again.";
826
+ if (token === navigation && !local.signal.aborted) {
827
+ error = ui().labels.folderError;
828
+ failure = "folder";
829
+ retryRequest = [
830
+ dataHref,
831
+ url,
832
+ push,
833
+ refresh,
834
+ restoreState
835
+ ];
836
+ viewHistory?.rollback();
837
+ }
369
838
  } finally {
370
839
  if (token === navigation) busy = false;
371
840
  }
@@ -377,6 +846,7 @@ function Ledger($$renderer, $$props) {
377
846
  } catch {}
378
847
  }
379
848
  onMount(() => {
849
+ viewportHeight = innerHeight;
380
850
  ready = true;
381
851
  abort = new AbortController();
382
852
  try {
@@ -387,9 +857,16 @@ function Ledger($$renderer, $$props) {
387
857
  "dark"
388
858
  ].includes(stored)) scheme = stored;
389
859
  } catch {}
390
- history.replaceState({ dataHref: new URL(model.dataHref, document.baseURI).href }, "");
860
+ viewHistory = createViewHistory("ledger");
861
+ const view = readView(location.href, "ledger", model.path, viewDefaults(model), [], history.state);
862
+ query = view.query;
863
+ types = view.types;
864
+ sort = view.sort;
865
+ selected = view.selected;
866
+ scrollTop = view.listTop;
867
+ saveView();
391
868
  const onPop = (event) => {
392
- if (event.state?.dataHref) openFolder(event.state.dataHref, location.href, false);
869
+ if (event.state?.dataHref) openFolder(event.state.dataHref, location.href, false, false, event.state);
393
870
  else location.reload();
394
871
  };
395
872
  window.addEventListener("popstate", onPop);
@@ -404,14 +881,19 @@ function Ledger($$renderer, $$props) {
404
881
  $$renderer.push("<!--[0-->");
405
882
  header($$renderer, { model });
406
883
  $$renderer.push(`<!---->`);
407
- } else $$renderer.push(`<!--[-1--><h1>${$.escape(model.title)}</h1> <span>File archive</span>`);
408
- $$renderer.push(`<!--]--></div> <nav aria-label="Breadcrumb"><a${$.attr("href", model.rootHref)}>Home</a><!--[-->`);
884
+ } else {
885
+ $$renderer.push("<!--[-1-->");
886
+ if (ui().brand.logoUrl) $$renderer.push(`<!--[0--><img${$.attr("src", ui().brand.logoUrl)}${$.attr("alt", ui().brand.logoAlt ?? "")} width="40" height="40"/>`);
887
+ else $$renderer.push("<!--[-1-->");
888
+ $$renderer.push(`<!--]--> <h1>${$.escape(ui().brand.name ?? model.title)}</h1> <span>${$.escape(ui().labels.archive)}</span>`);
889
+ }
890
+ $$renderer.push(`<!--]--></div> <nav${$.attr("aria-label", ui().labels.breadcrumb)}><a${$.attr("href", model.rootHref)}>${$.escape(ui().labels.home)}</a><!--[-->`);
409
891
  const each_array = $.ensure_array_like(model.breadcrumbs);
410
892
  for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
411
893
  let crumb = each_array[$$index];
412
894
  $$renderer.push(`<!---->/ <a${$.attr("href", crumb.href)}>${$.escape(crumb.name)}</a>`);
413
895
  }
414
- $$renderer.push(`<!--]--></nav></header> <main data-explorer="" data-framework="svelte"${$.attr("aria-busy", busy)}><div class="toolbar"><label class="search-field">Search this directory<input type="search" placeholder="Search files"${$.attr("value", query)}${$.attr("disabled", !ready || !complete(), true)}/></label> <fieldset${$.attr("disabled", !ready || !complete(), true)}><legend>Types</legend><!--[-->`);
896
+ $$renderer.push(`<!--]--></nav></header> <main data-explorer="" data-framework="svelte"${$.attr("aria-busy", busy)}><div class="toolbar"><label class="search-field">${$.escape(ui().labels.searchDirectory)}<input type="search"${$.attr("placeholder", ui().labels.searchFiles)}${$.attr("value", query)}${$.attr("disabled", !ready || !complete(), true)}/></label> <fieldset${$.attr("disabled", !ready || !complete(), true)}><legend>${$.escape(ui().labels.types)}</legend><!--[-->`);
415
897
  const each_array_1 = $.ensure_array_like([
416
898
  "directory",
417
899
  "file",
@@ -419,9 +901,9 @@ function Ledger($$renderer, $$props) {
419
901
  ]);
420
902
  for (let $$index_1 = 0, $$length = each_array_1.length; $$index_1 < $$length; $$index_1++) {
421
903
  let type = each_array_1[$$index_1];
422
- $$renderer.push(`<label><input type="checkbox"${$.attr("checked", types.includes(type), true)}/>${$.escape(type)}</label>`);
904
+ $$renderer.push(`<label><input type="checkbox"${$.attr("checked", types.includes(type), true)}/>${$.escape(ui().labels[type])}</label>`);
423
905
  }
424
- $$renderer.push(`<!--]--></fieldset> <label>Sort`);
906
+ $$renderer.push(`<!--]--></fieldset> <label>${$.escape(ui().labels.sort)}`);
425
907
  $$renderer.select({
426
908
  value: sort.field,
427
909
  disabled: !ready || !complete(),
@@ -431,16 +913,16 @@ function Ledger($$renderer, $$props) {
431
913
  }
432
914
  }, ($$renderer) => {
433
915
  $$renderer.option({ value: "name" }, ($$renderer) => {
434
- $$renderer.push(`Name`);
916
+ $$renderer.push(`${$.escape(ui().labels.name)}`);
435
917
  });
436
918
  $$renderer.option({ value: "size" }, ($$renderer) => {
437
- $$renderer.push(`Size`);
919
+ $$renderer.push(`${$.escape(ui().labels.size)}`);
438
920
  });
439
921
  $$renderer.option({ value: "modified" }, ($$renderer) => {
440
- $$renderer.push(`Modified`);
922
+ $$renderer.push(`${$.escape(ui().labels.modified)}`);
441
923
  });
442
924
  });
443
- $$renderer.push(`</label> <label>Names`);
925
+ $$renderer.push(`</label> <label>${$.escape(ui().labels.names)}`);
444
926
  $$renderer.select({
445
927
  value: sort.nameMode,
446
928
  disabled: !ready || !complete(),
@@ -450,16 +932,16 @@ function Ledger($$renderer, $$props) {
450
932
  }
451
933
  }, ($$renderer) => {
452
934
  $$renderer.option({ value: "natural" }, ($$renderer) => {
453
- $$renderer.push(`Natural`);
935
+ $$renderer.push(`${$.escape(ui().labels.natural)}`);
454
936
  });
455
937
  $$renderer.option({ value: "locale" }, ($$renderer) => {
456
- $$renderer.push(`Locale`);
938
+ $$renderer.push(`${$.escape(ui().labels.locale)}`);
457
939
  });
458
940
  $$renderer.option({ value: "unicode" }, ($$renderer) => {
459
- $$renderer.push(`Unicode`);
941
+ $$renderer.push(`${$.escape(ui().labels.unicode)}`);
460
942
  });
461
943
  });
462
- $$renderer.push(`</label> <label>Order`);
944
+ $$renderer.push(`</label> <label>${$.escape(ui().labels.order)}`);
463
945
  $$renderer.select({
464
946
  value: sort.direction,
465
947
  disabled: !ready || !complete(),
@@ -469,85 +951,134 @@ function Ledger($$renderer, $$props) {
469
951
  }
470
952
  }, ($$renderer) => {
471
953
  $$renderer.option({ value: "asc" }, ($$renderer) => {
472
- $$renderer.push(`Ascending`);
954
+ $$renderer.push(`${$.escape(ui().labels.ascending)}`);
473
955
  });
474
956
  $$renderer.option({ value: "desc" }, ($$renderer) => {
475
- $$renderer.push(`Descending`);
957
+ $$renderer.push(`${$.escape(ui().labels.descending)}`);
476
958
  });
477
959
  });
478
- $$renderer.push(`</label> <label><input type="checkbox"${$.attr("checked", sort.directoriesFirst, true)}${$.attr("disabled", !ready || !complete(), true)}/>Folders first</label> <label>Appearance`);
960
+ $$renderer.push(`</label> <label><input type="checkbox"${$.attr("checked", sort.directoriesFirst, true)}${$.attr("disabled", !ready || !complete(), true)}/>${$.escape(ui().labels.directoriesFirst)}</label> <label>${$.escape(ui().labels.appearance)}`);
479
961
  $$renderer.select({
480
962
  value: scheme,
481
963
  disabled: !ready,
482
964
  onchange: (event) => updateScheme(event.currentTarget.value)
483
965
  }, ($$renderer) => {
484
966
  $$renderer.option({ value: "system" }, ($$renderer) => {
485
- $$renderer.push(`System`);
967
+ $$renderer.push(`${$.escape(ui().labels.system)}`);
486
968
  });
487
969
  $$renderer.option({ value: "light" }, ($$renderer) => {
488
- $$renderer.push(`Light`);
970
+ $$renderer.push(`${$.escape(ui().labels.light)}`);
489
971
  });
490
972
  $$renderer.option({ value: "dark" }, ($$renderer) => {
491
- $$renderer.push(`Dark`);
973
+ $$renderer.push(`${$.escape(ui().labels.dark)}`);
492
974
  });
493
975
  });
494
- $$renderer.push(`</label> <button${$.attr("disabled", !ready || !model.searchIndexHref, true)}>Search archive</button></div> <section class="listing-card"><div class="listing-summary"><span data-visible-count="">${$.escape(complete() ? rows().length : entries.length)}</span> entries`);
495
- if (!complete()) $$renderer.push(`<!--[0-->on this page · ${$.escape(model.total)} in directory`);
976
+ $$renderer.push(`</label> `);
977
+ if (model.searchIndexHref || model.searchQueryHref) $$renderer.push(`<!--[0--><button${$.attr("disabled", !ready, true)}>${$.escape(ui().labels.searchArchive)}</button>`);
978
+ else $$renderer.push("<!--[-1-->");
979
+ $$renderer.push(`<!--]--></div> <section class="listing-card"><div class="listing-summary">${$.escape(countParts()[0])}<span data-visible-count="">${$.escape(complete() ? rows().length : entries.length)}</span>${$.escape(countParts().slice(1).join("__COUNT__"))}<span role="status" aria-live="polite">${$.escape(busy ? ui().labels.loading : error)}</span></div> `);
980
+ if (failure && failure !== "copy") $$renderer.push(`<!--[0--><button${$.attr("disabled", busy, true)}>${$.escape(ui().labels.retry)}</button>`);
496
981
  else $$renderer.push("<!--[-1-->");
497
- $$renderer.push(`<!--]--><span role="status" aria-live="polite">${$.escape(busy ? "Loading…" : error)}</span></div> <div class="entry-head"><span>Name</span><span>Size</span><span>Modified</span></div> <ul class="entry-list">`);
982
+ $$renderer.push(`<!--]--> `);
983
+ if (failure === "folder" && retryRequest) $$renderer.push(`<!--[0--><a${$.attr("href", retryRequest[1])}>${$.escape(ui().labels.openNative)}</a>`);
984
+ else $$renderer.push("<!--[-1-->");
985
+ $$renderer.push(`<!--]--> <div class="entry-head"${$.attr("data-custom-columns", Boolean(model.ui?.columns))}${$.attr_style("", { "grid-template-columns": model.ui?.columns ? ui().columns.map((column) => column === "name" ? "minmax(0,1fr)" : typeof column === "object" ? "minmax(0,12rem)" : "minmax(6rem,max-content)").join(" ") : void 0 })}><!--[-->`);
986
+ const each_array_2 = $.ensure_array_like(ui().columns);
987
+ for (let $$index_2 = 0, $$length = each_array_2.length; $$index_2 < $$length; $$index_2++) {
988
+ let column = each_array_2[$$index_2];
989
+ $$renderer.push(`<span>${$.escape(columnLabel(column, ui()))}</span>`);
990
+ }
991
+ $$renderer.push(`<!--]--></div> <ul class="entry-list">`);
498
992
  if (model.parentHref) $$renderer.push(`<!--[0--><li class="entry"><a${$.attr("href", model.parentHref)}>../</a></li>`);
499
993
  else $$renderer.push("<!--[-1-->");
500
994
  $$renderer.push(`<!--]--> `);
501
995
  if (start()) $$renderer.push(`<!--[0--><li aria-hidden="true"${$.attr_style("", { height: offsets()[start()] + "px" })}></li>`);
502
996
  else $$renderer.push("<!--[-1-->");
503
997
  $$renderer.push(`<!--]--> <!--[-->`);
504
- const each_array_2 = $.ensure_array_like(rows().slice(start(), end()));
505
- for (let index = 0, $$length = each_array_2.length; index < $$length; index++) {
506
- let entry = each_array_2[index];
507
- $$renderer.push(`<li class="entry" data-entry=""${$.attr("data-id", entry.id)}${$.attr("data-order", start() + index)}${$.attr("data-active", selected === entry.id)}><span class="identity">`);
508
- if (entry.href) {
509
- $$renderer.push(`<!--[0--><a class="name"${$.attr("href", safeHref(entry))}${$.attr("target", entry.exitsExplorer ? "_blank" : void 0)}${$.attr("rel", entry.exitsExplorer ? "noopener" : void 0)}>`);
510
- if (row) {
511
- $$renderer.push("<!--[0-->");
512
- row($$renderer, {
513
- model,
514
- entry,
515
- selected: selected === entry.id
516
- });
517
- $$renderer.push(`<!---->`);
518
- } else $$renderer.push(`<!--[-1--><span class="file-icon" aria-hidden="true">${$.html(entryIcon(entryType(entry)))}</span> ${$.escape(entry.name)}${$.escape(isDirectory(entry) ? "/" : "")}`);
519
- $$renderer.push(`<!--]--></a>`);
520
- } else {
521
- $$renderer.push(`<!--[-1--><span>`);
522
- if (row) {
998
+ const each_array_3 = $.ensure_array_like(rows().slice(start(), end()));
999
+ for (let index = 0, $$length = each_array_3.length; index < $$length; index++) {
1000
+ let entry = each_array_3[index];
1001
+ $$renderer.push(`<li class="entry" data-entry=""${$.attr("data-custom-columns", Boolean(model.ui?.columns))}${$.attr("data-id", entry.id)}${$.attr("data-order", start() + index)}${$.attr("data-active", selected === entry.id)}${$.attr_style("", { "grid-template-columns": model.ui?.columns ? ui().columns.map((column) => column === "name" ? "minmax(0,1fr)" : "minmax(6rem,max-content)").join(" ") : void 0 })}><!--[-->`);
1002
+ const each_array_4 = $.ensure_array_like(ui().columns);
1003
+ for (let $$index_3 = 0, $$length = each_array_4.length; $$index_3 < $$length; $$index_3++) {
1004
+ let column = each_array_4[$$index_3];
1005
+ if (column === "name") {
1006
+ $$renderer.push(`<!--[0--><span class="identity">`);
1007
+ if (entry.href) {
1008
+ $$renderer.push(`<!--[0--><a class="name"${$.attr("href", safeHref(entry))}${$.attr("target", entry.exitsExplorer ? "_blank" : void 0)}${$.attr("rel", entry.exitsExplorer ? "noopener" : void 0)}>`);
1009
+ if (row) {
1010
+ $$renderer.push("<!--[0-->");
1011
+ row($$renderer, {
1012
+ model,
1013
+ entry,
1014
+ selected: selected === entry.id
1015
+ });
1016
+ $$renderer.push(`<!---->`);
1017
+ } else $$renderer.push(`<!--[-1--><span class="file-icon" aria-hidden="true">${$.html(entryIcon(entryType(entry)))}</span> ${$.escape(entry.name)}${$.escape(isDirectory(entry) ? "/" : "")}`);
1018
+ $$renderer.push(`<!--]--></a>`);
1019
+ } else {
1020
+ $$renderer.push(`<!--[-1--><span>`);
1021
+ if (row) {
1022
+ $$renderer.push("<!--[0-->");
1023
+ row($$renderer, {
1024
+ model,
1025
+ entry,
1026
+ selected: selected === entry.id
1027
+ });
1028
+ $$renderer.push(`<!---->`);
1029
+ } else $$renderer.push(`<!--[-1-->${$.escape(entry.name)}`);
1030
+ $$renderer.push(`<!--]--></span>`);
1031
+ }
1032
+ $$renderer.push(`<!--]-->`);
1033
+ if (entry.link) $$renderer.push(`<!--[0--><small class="target">${$.escape(entry.link.unavailable ? ui().labels.unavailable + " · " : "→ ")}${$.escape(entry.link.target)}</small>`);
1034
+ else $$renderer.push("<!--[-1-->");
1035
+ $$renderer.push(`<!--]--></span>`);
1036
+ } else if (column === "size") $$renderer.push(`<!--[1--><span class="kind">${$.escape(isDirectory(entry) ? ui().labels.folder : size(entry.size))}</span>`);
1037
+ else if (column === "modified") $$renderer.push(`<!--[2--><button class="timestamp-button"${$.attr("disabled", !ready, true)}><time${$.attr("datetime", entry.modifiedAt)}>${$.escape(formattedDate(entry.modifiedAt, ui(), ready))}</time></button>`);
1038
+ else $$renderer.push(`<!--[-1--><span class="entry-data">${$.escape(publicDataValue(entry.data?.[column.dataKey]))}</span>`);
1039
+ $$renderer.push(`<!--]-->`);
1040
+ }
1041
+ $$renderer.push(`<!--]--> `);
1042
+ if (actions || ui().actions.length) {
1043
+ $$renderer.push(`<!--[0--><span data-dirwell-actions="" style="grid-column:1/-1;display:flex;flex-wrap:wrap;gap:.5rem">`);
1044
+ if (actions) {
523
1045
  $$renderer.push("<!--[0-->");
524
- row($$renderer, {
1046
+ actions($$renderer, {
525
1047
  model,
526
1048
  entry,
527
1049
  selected: selected === entry.id
528
1050
  });
529
1051
  $$renderer.push(`<!---->`);
530
- } else $$renderer.push(`<!--[-1-->${$.escape(entry.name)}`);
1052
+ } else $$renderer.push("<!--[-1-->");
1053
+ $$renderer.push(`<!--]--> <!--[-->`);
1054
+ const each_array_5 = $.ensure_array_like(ui().actions);
1055
+ for (let $$index_4 = 0, $$length = each_array_5.length; $$index_4 < $$length; $$index_4++) {
1056
+ let action = each_array_5[$$index_4];
1057
+ if (action === "download" && entry.href && !isDirectory(entry) && !entry.link?.unavailable) $$renderer.push(`<!--[0--><a${$.attr("href", safeHref(entry))} download="">${$.escape(ui().labels.download)}</a>`);
1058
+ else if (action !== "download") $$renderer.push(`<!--[1--><button${$.attr("disabled", !ready || action === "copy-link" && !entry.href, true)}${$.attr("aria-label", (action === "copy-link" ? ui().labels.copyLink : ui().labels.copyPath) + ": " + entry.name)}>${$.escape(action === "copy-link" ? ui().labels.copyLink : ui().labels.copyPath)}</button>`);
1059
+ else $$renderer.push("<!--[-1-->");
1060
+ $$renderer.push(`<!--]-->`);
1061
+ }
531
1062
  $$renderer.push(`<!--]--></span>`);
532
- }
533
- $$renderer.push(`<!--]-->`);
534
- if (entry.link) $$renderer.push(`<!--[0--><small class="target">${$.escape(entry.link.unavailable ? "Target unavailable · " : "→ ")}${$.escape(entry.link.target)}</small>`);
535
- else $$renderer.push("<!--[-1-->");
536
- $$renderer.push(`<!--]--></span> <span class="kind">${$.escape(isDirectory(entry) ? "Folder" : size(entry.size))}</span> <button class="timestamp-button"${$.attr("disabled", !ready, true)}><time${$.attr("datetime", entry.modifiedAt)}>${$.escape(modified(entry.modifiedAt, ready))}</time></button></li>`);
1063
+ } else $$renderer.push("<!--[-1-->");
1064
+ $$renderer.push(`<!--]--></li>`);
537
1065
  }
538
1066
  $$renderer.push(`<!--]--> `);
539
1067
  if (end() < rows().length) $$renderer.push(`<!--[0--><li aria-hidden="true"${$.attr_style("", { height: offsets()[rows().length] - offsets()[end()] + "px" })}></li>`);
540
1068
  else $$renderer.push("<!--[-1-->");
541
1069
  $$renderer.push(`<!--]--></ul> `);
542
- if (rows().length === 0) $$renderer.push(`<!--[0--><p>No matching entries.</p>`);
1070
+ if (rows().length === 0 && !busy) $$renderer.push(`<!--[0--><p>${$.escape(model.total === 0 ? ui().labels.empty : ui().labels.noMatches)}</p>`);
543
1071
  else $$renderer.push("<!--[-1-->");
544
1072
  $$renderer.push(`<!--]--></section> `);
545
1073
  if (model.page?.count > 1 && !complete()) {
546
- $$renderer.push(`<!--[0--><nav aria-label="Pagination">`);
547
- if (model.page.previousHref) $$renderer.push(`<!--[0--><a rel="prev"${$.attr("href", model.page.previousHref)}>Previous</a>`);
1074
+ $$renderer.push(`<!--[0--><nav${$.attr("aria-label", ui().labels.pagination)}>`);
1075
+ if (model.page.previousHref) $$renderer.push(`<!--[0--><a rel="prev"${$.attr("href", model.page.previousHref)}>${$.escape(ui().labels.previous)}</a>`);
548
1076
  else $$renderer.push("<!--[-1-->");
549
- $$renderer.push(`<!--]--> Page ${$.escape(model.page.number)} of ${$.escape(model.page.count)} `);
550
- if (model.page.nextHref) $$renderer.push(`<!--[0--><a rel="next"${$.attr("href", model.page.nextHref)}>Next</a>`);
1077
+ $$renderer.push(`<!--]--> ${$.escape(label(ui(), "page", {
1078
+ page: model.page.number,
1079
+ pages: model.page.count
1080
+ }))} `);
1081
+ if (model.page.nextHref) $$renderer.push(`<!--[0--><a rel="next"${$.attr("href", model.page.nextHref)}>${$.escape(ui().labels.next)}</a>`);
551
1082
  else $$renderer.push("<!--[-1-->");
552
1083
  $$renderer.push(`<!--]--></nav>`);
553
1084
  } else $$renderer.push("<!--[-1-->");
@@ -556,8 +1087,12 @@ function Ledger($$renderer, $$props) {
556
1087
  $$renderer.push("<!--[0-->");
557
1088
  footer($$renderer, { model });
558
1089
  $$renderer.push(`<!---->`);
559
- } else $$renderer.push(`<!--[-1--><p class="keyboard-help">Arrow keys select files · Home / End jump · / searches · Backspace opens
560
- parent</p>`);
1090
+ } else if (ui().brand.footerText) {
1091
+ $$renderer.push(`<!--[1--><p class="brand-footer">`);
1092
+ if (ui().brand.footerUrl) $$renderer.push(`<!--[0--><a${$.attr("href", ui().brand.footerUrl)}>${$.escape(ui().brand.footerText)}</a>`);
1093
+ else $$renderer.push(`<!--[-1-->${$.escape(ui().brand.footerText)}`);
1094
+ $$renderer.push(`<!--]--></p>`);
1095
+ } else $$renderer.push(`<!--[-1--><p class="keyboard-help">${$.escape(ui().labels.keyboardHelp)}</p>`);
561
1096
  $$renderer.push(`<!--]--></main> `);
562
1097
  $$renderer.push("<!--[-1-->");
563
1098
  $$renderer.push(`<!--]--> `);
@@ -571,6 +1106,11 @@ const renderBody = (model) => render(Ledger, { props: { model } }).body;
571
1106
  //#endregion
572
1107
  //#region src/index.ts
573
1108
  function createLedgerTheme(options = {}, renderer) {
1109
+ assertThemeOptions(options, ["title", "archiveSearch"]);
1110
+ if (options.title !== void 0 && (typeof options.title !== "string" || !options.title)) throw new TypeError("title must be a non-empty string");
1111
+ if (options.archiveSearch !== void 0 && typeof options.archiveSearch !== "boolean") throw new TypeError("archiveSearch must be a boolean");
1112
+ const ui = resolveCustomization(options);
1113
+ const publishedUi = pickCustomization(options);
574
1114
  let resources;
575
1115
  const loadResources = () => resources ??= Promise.all([renderer?.clientScript ?? readFile(new URL("./client.js", import.meta.url), "utf8"), readFile(new URL("./styles.css", import.meta.url), "utf8")]).then(([script, styles]) => ({
576
1116
  script,
@@ -579,17 +1119,20 @@ function createLedgerTheme(options = {}, renderer) {
579
1119
  }));
580
1120
  const theme = {
581
1121
  name: "ledger",
1122
+ searchIndex: options.archiveSearch ?? true,
582
1123
  listing: { pagination: true },
583
1124
  entryData: (entry, context) => ({ folderDataHref: folderDataHref(entry, context, "ledger") }),
584
1125
  async render(context) {
585
1126
  const model = {
586
- ...createThemePage(context, theme, options.title ?? "Dirwell"),
587
- searchIndexHref: context.searchIndexHref
1127
+ ...createThemePage(context, theme, options.title ?? ui.brand.name ?? "Dirwell"),
1128
+ ...publishedUi === void 0 ? {} : { ui: publishedUi },
1129
+ searchIndexHref: theme.searchIndex === false ? null : context.searchIndexHref,
1130
+ searchQueryHref: theme.searchIndex === false ? null : context.searchQueryHref ?? null
588
1131
  };
589
1132
  const { script, scriptName, styles } = await loadResources();
590
1133
  const json = JSON.stringify(model).replaceAll("<", "\\u003c");
591
1134
  return {
592
- html: `<!doctype html><html lang="en" data-theme="system"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${context.documentBaseHref === null ? "" : `<base href="${escapeHtml(context.documentBaseHref)}">`}${context.metadata?.head ?? `<title>${escapeHtml(model.title)}</title>`}<style>${styles}${renderer?.styles ?? ""}</style></head><body><div data-dirwell-theme="ledger">${await (renderer?.renderBody ?? renderBody)(model)}</div><script type="application/json" id="dirwell-page-data">${json}<\/script><script ${renderer?.clientType === "module" ? "type=\"module\"" : "defer"} src="${escapeHtml(context.assetHref(scriptName))}"><\/script></body></html>`,
1135
+ html: `<!doctype html><html lang="${escapeHtml(ui.language)}" dir="${ui.direction}" data-theme="system"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${context.documentBaseHref === null ? "" : `<base href="${escapeHtml(context.documentBaseHref)}">`}${context.metadata?.head ?? `<title>${escapeHtml(model.title)}</title>`}${ui.brand.faviconUrl ? `<link rel="icon" href="${escapeHtml(ui.brand.faviconUrl)}">` : ""}<style>${styles}${renderer?.styles ?? ""}${customizationCss(ui)}</style></head><body><div data-dirwell-theme="ledger">${await (renderer?.renderBody ?? renderBody)(model)}</div><script type="application/json" id="dirwell-page-data">${json}<\/script><script ${renderer?.clientType === "module" ? "type=\"module\"" : "defer"} src="${escapeHtml(context.assetHref(scriptName))}"><\/script></body></html>`,
593
1136
  assets: {
594
1137
  [scriptName]: script,
595
1138
  [pageAsset("ledger", model.path, model.page?.number ?? 1)]: JSON.stringify(model)