@dirwell/theme-ledger 0.0.1-alpha.1 → 0.0.1

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