@dirwell/theme-crosswave 0.0.1-alpha → 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.
@@ -1,4 +1,4 @@
1
1
  import type {DefineComponent, SlotsType, VNode, ComponentOptionsMixin, PublicProps} from 'vue';
2
2
  import type {ThemePage, RowContext} from '../model.mjs';
3
- export interface CrosswaveSlots {header?: (props: {model: ThemePage}) => VNode[]; row?: (props: RowContext) => VNode[]; footer?: (props: {model: ThemePage}) => VNode[];}
3
+ export interface CrosswaveSlots {header?: (props: {model: ThemePage}) => VNode[]; row?: (props: RowContext) => VNode[]; actions?: (props: RowContext) => VNode[]; footer?: (props: {model: ThemePage}) => VNode[];}
4
4
  declare const Crosswave: DefineComponent<{model: ThemePage}, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, {model: ThemePage}, {}, SlotsType<CrosswaveSlots>>; export default Crosswave;
@@ -0,0 +1,255 @@
1
+ // ../shared-theme/src/customization.ts
2
+ var defaultLabels = {
3
+ other: "other",
4
+ crosswaveKeyboardHelp: "Arrow keys navigate \xB7 Enter confirms \xB7 Backspace returns",
5
+ brokenLink: "broken link",
6
+ unavailableLink: "unavailable link",
7
+ cycleLink: "cycle link",
8
+ azure: "azure",
9
+ violet: "violet",
10
+ amber: "amber",
11
+ rose: "rose",
12
+ jade: "jade",
13
+ graphite: "graphite",
14
+ allFiles: "All files",
15
+ categoryFolders: "Folders",
16
+ categoryPhotos: "Photos",
17
+ categoryMusic: "Music",
18
+ categoryVideos: "Videos",
19
+ categoryDocuments: "Documents",
20
+ categoryOther: "Other",
21
+ indexOf: "Index of {path}",
22
+ home: "Home",
23
+ breadcrumb: "Breadcrumb",
24
+ archive: "File archive",
25
+ searchDirectory: "Search this directory",
26
+ searchFiles: "Search files",
27
+ search: "Search",
28
+ types: "Types",
29
+ directory: "directory",
30
+ file: "file",
31
+ link: "link",
32
+ folder: "Folder",
33
+ sort: "Sort",
34
+ name: "Name",
35
+ size: "Size",
36
+ modified: "Modified",
37
+ names: "Names",
38
+ natural: "Natural",
39
+ locale: "Locale",
40
+ unicode: "Unicode",
41
+ order: "Order",
42
+ ascending: "Ascending",
43
+ descending: "Descending",
44
+ directoriesFirst: "Folders first",
45
+ appearance: "Appearance",
46
+ system: "System",
47
+ light: "Light",
48
+ dark: "Dark",
49
+ searchArchive: "Search archive",
50
+ archiveSearchInput: "Search archive files",
51
+ entries: "{count} entries",
52
+ pageEntries: "{total} entries \xB7 {count} on this page",
53
+ loading: "Loading\u2026",
54
+ noMatches: "No matching entries.",
55
+ empty: "This directory is empty.",
56
+ pagination: "Pagination",
57
+ previous: "Previous",
58
+ next: "Next",
59
+ page: "Page {page} of {pages}",
60
+ keyboardHelp: "Arrow keys select files \xB7 Home / End jump \xB7 / searches \xB7 Backspace opens parent",
61
+ modifiedTime: "Modified time",
62
+ close: "Close",
63
+ unknown: "Unknown",
64
+ unavailable: "Target unavailable",
65
+ beginArchive: "Enter a search to begin.",
66
+ searchingArchive: "Searching archive\u2026",
67
+ archiveResults: "{count} results",
68
+ truncatedResults: "Showing {shown} of {count} results",
69
+ archiveError: "Could not load archive search. Try again.",
70
+ listingError: "Could not load complete directory data. Native page links remain available.",
71
+ folderError: "Could not open the folder. Its native link remains available.",
72
+ updatesError: "Live updates disconnected. Refresh to retry.",
73
+ retry: "Retry",
74
+ openNative: "Open folder directly",
75
+ categories: "File categories",
76
+ items: "{count} items",
77
+ filesIn: "Files in {path}",
78
+ controller: "Controller \xB7 Confirm / Back",
79
+ color: "Color",
80
+ pauseMotion: "Pause motion",
81
+ resumeMotion: "Resume motion",
82
+ copyLink: "Copy link",
83
+ copyPath: "Copy path",
84
+ download: "Download",
85
+ copied: "Copied.",
86
+ copyError: "Could not copy. Use the native link or select the path.",
87
+ repository: "Repository",
88
+ author: "by",
89
+ target: "Target",
90
+ newTab: "opens in a new tab"
91
+ };
92
+ function resolveCustomization(options = {}) {
93
+ 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;
94
+ for (const [key, value] of Object.entries({ language, locale, timeZone }))
95
+ if (typeof value !== "string" || !value)
96
+ throw new TypeError(`${key} must be a non-empty string`);
97
+ new Intl.DateTimeFormat(locale, { timeZone });
98
+ const direction = options.direction ?? "ltr";
99
+ if (direction !== "ltr" && direction !== "rtl")
100
+ throw new TypeError("direction must be ltr or rtl");
101
+ const labels = { ...defaultLabels };
102
+ if (options.labels !== void 0) {
103
+ if (typeof options.labels !== "object" || options.labels === null || Array.isArray(options.labels))
104
+ throw new TypeError("labels must be an object");
105
+ for (const [key, value] of Object.entries(options.labels)) {
106
+ if (!Object.hasOwn(labels, key) || typeof value !== "string")
107
+ throw new TypeError(`Unsupported label or value: ${key}`);
108
+ Object.assign(labels, { [key]: value });
109
+ }
110
+ }
111
+ const brand = options.brand ?? {};
112
+ if (typeof brand !== "object" || brand === null || Array.isArray(brand))
113
+ throw new TypeError("brand must be an object");
114
+ for (const [key, value] of Object.entries(brand)) {
115
+ if (!["name", "logoUrl", "logoAlt", "footerText", "footerUrl", "faviconUrl"].includes(key) || typeof value !== "string")
116
+ throw new TypeError(`Unsupported brand option or value: ${key}`);
117
+ if (key.endsWith("Url") && !/^https?:\/\//i.test(value) && !(value.startsWith("/") && !value.startsWith("//")))
118
+ throw new TypeError(`brand.${key} must be an HTTP(S) or root-relative URL`);
119
+ }
120
+ const columns = options.columns ?? ["name", "size", "modified"];
121
+ if (!Array.isArray(columns) || !columns.includes("name"))
122
+ throw new TypeError("columns must contain name and unique supported columns");
123
+ const columnIds = /* @__PURE__ */ new Set();
124
+ for (const column of columns) {
125
+ let id;
126
+ if (typeof column === "string") {
127
+ if (!["name", "size", "modified"].includes(column)) throw new TypeError("Unsupported column");
128
+ id = column;
129
+ } else {
130
+ 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)
131
+ throw new TypeError("Data columns require dataKey and label");
132
+ id = "data:" + column.dataKey;
133
+ }
134
+ if (columnIds.has(id)) throw new TypeError("columns must contain unique supported columns");
135
+ columnIds.add(id);
136
+ }
137
+ const actions = options.actions ?? [];
138
+ if (!Array.isArray(actions) || new Set(actions).size !== actions.length || actions.some((key) => !["copy-link", "copy-path", "download"].includes(key)))
139
+ throw new TypeError("actions must contain unique supported actions");
140
+ const cssVariables = options.cssVariables ?? {};
141
+ if (typeof cssVariables !== "object" || cssVariables === null || Array.isArray(cssVariables))
142
+ throw new TypeError("cssVariables must be an object");
143
+ for (const [key, value] of Object.entries(cssVariables))
144
+ if (!/^--[a-zA-Z][a-zA-Z0-9_-]*$/.test(key) || typeof value !== "string" || /[{};<>]/.test(value))
145
+ throw new TypeError(`Invalid CSS variable: ${key}`);
146
+ return {
147
+ language,
148
+ dateOptions: {
149
+ ...options.locale === void 0 ? {} : { locale: options.locale },
150
+ ...options.timeZone === void 0 ? {} : { timeZone: options.timeZone }
151
+ },
152
+ direction,
153
+ locale,
154
+ timeZone,
155
+ labels,
156
+ brand: { ...brand },
157
+ columns: columns.map((column) => typeof column === "string" ? column : { ...column }),
158
+ actions: [...actions],
159
+ cssVariables: { ...cssVariables }
160
+ };
161
+ }
162
+ var defaultCustomization = resolveCustomization();
163
+ function label(ui, key, values = {}) {
164
+ return ui.labels[key].replace(
165
+ /\{([a-zA-Z]+)\}/g,
166
+ (match, name) => Object.hasOwn(values, name) ? String(values[name]) : match
167
+ );
168
+ }
169
+ var dateFormats = /* @__PURE__ */ new Map();
170
+ function formattedDate(value, ui, local = false) {
171
+ const date = new Date(value);
172
+ if (Number.isNaN(date.getTime())) return ui.labels.unknown;
173
+ if (ui.dateOptions.locale === void 0 && ui.dateOptions.timeZone === void 0)
174
+ return local ? date.toLocaleString(void 0, { dateStyle: "short", timeStyle: "short" }) : date.toISOString().slice(0, 16).replace("T", " ") + " UTC";
175
+ const locale = ui.dateOptions.locale, timeZone = ui.dateOptions.timeZone ?? (local ? void 0 : "UTC");
176
+ const key = String(locale) + "\0" + String(timeZone);
177
+ let formatter = dateFormats.get(key);
178
+ if (formatter === void 0) {
179
+ formatter = new Intl.DateTimeFormat(locale, {
180
+ timeZone,
181
+ dateStyle: "short",
182
+ timeStyle: "short"
183
+ });
184
+ if (dateFormats.size >= 32) dateFormats.delete(dateFormats.keys().next().value);
185
+ dateFormats.set(key, formatter);
186
+ }
187
+ return formatter.format(date);
188
+ }
189
+ function customizationCss(ui) {
190
+ const declarations = Object.entries(ui.cssVariables).map(([key, value]) => `${key}:${value}`).join(";");
191
+ return declarations ? `:root:root{${declarations}}${ui.cssVariables["--dw-font-family"] ? "body{font-family:var(--dw-font-family)}" : ""}` : "";
192
+ }
193
+ async function copyEntryAction(action, entry) {
194
+ const value = action === "copy-link" && entry.href ? new URL(entry.href, document.baseURI).href : entry.id;
195
+ if (!navigator.clipboard?.writeText) throw new Error("Clipboard unavailable");
196
+ await navigator.clipboard.writeText(value);
197
+ }
198
+ function pickCustomization(options) {
199
+ const result = {};
200
+ for (const key of [
201
+ "language",
202
+ "direction",
203
+ "locale",
204
+ "timeZone",
205
+ "labels",
206
+ "brand",
207
+ "columns",
208
+ "actions",
209
+ "cssVariables"
210
+ ])
211
+ if (options[key] !== void 0) {
212
+ const value = options[key];
213
+ Object.assign(result, {
214
+ [key]: Array.isArray(value) ? value.map((item) => typeof item === "object" ? { ...item } : item) : typeof value === "object" && value !== null ? { ...value } : value
215
+ });
216
+ }
217
+ return Object.keys(result).length ? result : void 0;
218
+ }
219
+ function assertThemeOptions(value, extraKeys) {
220
+ if (typeof value !== "object" || value === null || Array.isArray(value))
221
+ throw new TypeError("Theme options must be an object");
222
+ const allowed = [
223
+ "language",
224
+ "direction",
225
+ "locale",
226
+ "timeZone",
227
+ "labels",
228
+ "brand",
229
+ "columns",
230
+ "actions",
231
+ "cssVariables",
232
+ ...extraKeys
233
+ ];
234
+ for (const key of Object.keys(value))
235
+ if (!allowed.includes(key)) throw new TypeError(`Unsupported theme option: ${key}`);
236
+ }
237
+ function columnLabel(column, ui) {
238
+ return typeof column === "string" ? ui.labels[column] : column.label;
239
+ }
240
+ function publicDataValue(value) {
241
+ return value === void 0 || value === null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
242
+ }
243
+ export {
244
+ assertThemeOptions,
245
+ columnLabel,
246
+ copyEntryAction,
247
+ customizationCss,
248
+ defaultCustomization,
249
+ defaultLabels,
250
+ formattedDate,
251
+ label,
252
+ pickCustomization,
253
+ publicDataValue,
254
+ resolveCustomization
255
+ };
@@ -72,6 +72,7 @@ function entry(value, base) {
72
72
  };
73
73
  }
74
74
  return {
75
+ ...value.data === void 0 ? {} : { data: copyJsonObject(value.data) },
75
76
  ...value.presentation === void 0 ? {} : { presentation: copyJsonObject(value.presentation) },
76
77
  id: string(value.id, "id"),
77
78
  name: string(value.name, "name"),
@@ -175,6 +176,169 @@ function parseListingEntries(value, baseHref, maxEntries = 1e5) {
175
176
  return result;
176
177
  }
177
178
 
179
+ // ../shared-theme/src/customization.ts
180
+ var defaultLabels = {
181
+ other: "other",
182
+ crosswaveKeyboardHelp: "Arrow keys navigate \xB7 Enter confirms \xB7 Backspace returns",
183
+ brokenLink: "broken link",
184
+ unavailableLink: "unavailable link",
185
+ cycleLink: "cycle link",
186
+ azure: "azure",
187
+ violet: "violet",
188
+ amber: "amber",
189
+ rose: "rose",
190
+ jade: "jade",
191
+ graphite: "graphite",
192
+ allFiles: "All files",
193
+ categoryFolders: "Folders",
194
+ categoryPhotos: "Photos",
195
+ categoryMusic: "Music",
196
+ categoryVideos: "Videos",
197
+ categoryDocuments: "Documents",
198
+ categoryOther: "Other",
199
+ indexOf: "Index of {path}",
200
+ home: "Home",
201
+ breadcrumb: "Breadcrumb",
202
+ archive: "File archive",
203
+ searchDirectory: "Search this directory",
204
+ searchFiles: "Search files",
205
+ search: "Search",
206
+ types: "Types",
207
+ directory: "directory",
208
+ file: "file",
209
+ link: "link",
210
+ folder: "Folder",
211
+ sort: "Sort",
212
+ name: "Name",
213
+ size: "Size",
214
+ modified: "Modified",
215
+ names: "Names",
216
+ natural: "Natural",
217
+ locale: "Locale",
218
+ unicode: "Unicode",
219
+ order: "Order",
220
+ ascending: "Ascending",
221
+ descending: "Descending",
222
+ directoriesFirst: "Folders first",
223
+ appearance: "Appearance",
224
+ system: "System",
225
+ light: "Light",
226
+ dark: "Dark",
227
+ searchArchive: "Search archive",
228
+ archiveSearchInput: "Search archive files",
229
+ entries: "{count} entries",
230
+ pageEntries: "{total} entries \xB7 {count} on this page",
231
+ loading: "Loading\u2026",
232
+ noMatches: "No matching entries.",
233
+ empty: "This directory is empty.",
234
+ pagination: "Pagination",
235
+ previous: "Previous",
236
+ next: "Next",
237
+ page: "Page {page} of {pages}",
238
+ keyboardHelp: "Arrow keys select files \xB7 Home / End jump \xB7 / searches \xB7 Backspace opens parent",
239
+ modifiedTime: "Modified time",
240
+ close: "Close",
241
+ unknown: "Unknown",
242
+ unavailable: "Target unavailable",
243
+ beginArchive: "Enter a search to begin.",
244
+ searchingArchive: "Searching archive\u2026",
245
+ archiveResults: "{count} results",
246
+ truncatedResults: "Showing {shown} of {count} results",
247
+ archiveError: "Could not load archive search. Try again.",
248
+ listingError: "Could not load complete directory data. Native page links remain available.",
249
+ folderError: "Could not open the folder. Its native link remains available.",
250
+ updatesError: "Live updates disconnected. Refresh to retry.",
251
+ retry: "Retry",
252
+ openNative: "Open folder directly",
253
+ categories: "File categories",
254
+ items: "{count} items",
255
+ filesIn: "Files in {path}",
256
+ controller: "Controller \xB7 Confirm / Back",
257
+ color: "Color",
258
+ pauseMotion: "Pause motion",
259
+ resumeMotion: "Resume motion",
260
+ copyLink: "Copy link",
261
+ copyPath: "Copy path",
262
+ download: "Download",
263
+ copied: "Copied.",
264
+ copyError: "Could not copy. Use the native link or select the path.",
265
+ repository: "Repository",
266
+ author: "by",
267
+ target: "Target",
268
+ newTab: "opens in a new tab"
269
+ };
270
+ function resolveCustomization(options = {}) {
271
+ 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;
272
+ for (const [key, value] of Object.entries({ language, locale, timeZone }))
273
+ if (typeof value !== "string" || !value)
274
+ throw new TypeError(`${key} must be a non-empty string`);
275
+ new Intl.DateTimeFormat(locale, { timeZone });
276
+ const direction = options.direction ?? "ltr";
277
+ if (direction !== "ltr" && direction !== "rtl")
278
+ throw new TypeError("direction must be ltr or rtl");
279
+ const labels = { ...defaultLabels };
280
+ if (options.labels !== void 0) {
281
+ if (typeof options.labels !== "object" || options.labels === null || Array.isArray(options.labels))
282
+ throw new TypeError("labels must be an object");
283
+ for (const [key, value] of Object.entries(options.labels)) {
284
+ if (!Object.hasOwn(labels, key) || typeof value !== "string")
285
+ throw new TypeError(`Unsupported label or value: ${key}`);
286
+ Object.assign(labels, { [key]: value });
287
+ }
288
+ }
289
+ const brand = options.brand ?? {};
290
+ if (typeof brand !== "object" || brand === null || Array.isArray(brand))
291
+ throw new TypeError("brand must be an object");
292
+ for (const [key, value] of Object.entries(brand)) {
293
+ if (!["name", "logoUrl", "logoAlt", "footerText", "footerUrl", "faviconUrl"].includes(key) || typeof value !== "string")
294
+ throw new TypeError(`Unsupported brand option or value: ${key}`);
295
+ if (key.endsWith("Url") && !/^https?:\/\//i.test(value) && !(value.startsWith("/") && !value.startsWith("//")))
296
+ throw new TypeError(`brand.${key} must be an HTTP(S) or root-relative URL`);
297
+ }
298
+ const columns = options.columns ?? ["name", "size", "modified"];
299
+ if (!Array.isArray(columns) || !columns.includes("name"))
300
+ throw new TypeError("columns must contain name and unique supported columns");
301
+ const columnIds = /* @__PURE__ */ new Set();
302
+ for (const column of columns) {
303
+ let id;
304
+ if (typeof column === "string") {
305
+ if (!["name", "size", "modified"].includes(column)) throw new TypeError("Unsupported column");
306
+ id = column;
307
+ } else {
308
+ 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)
309
+ throw new TypeError("Data columns require dataKey and label");
310
+ id = "data:" + column.dataKey;
311
+ }
312
+ if (columnIds.has(id)) throw new TypeError("columns must contain unique supported columns");
313
+ columnIds.add(id);
314
+ }
315
+ const actions = options.actions ?? [];
316
+ if (!Array.isArray(actions) || new Set(actions).size !== actions.length || actions.some((key) => !["copy-link", "copy-path", "download"].includes(key)))
317
+ throw new TypeError("actions must contain unique supported actions");
318
+ const cssVariables = options.cssVariables ?? {};
319
+ if (typeof cssVariables !== "object" || cssVariables === null || Array.isArray(cssVariables))
320
+ throw new TypeError("cssVariables must be an object");
321
+ for (const [key, value] of Object.entries(cssVariables))
322
+ if (!/^--[a-zA-Z][a-zA-Z0-9_-]*$/.test(key) || typeof value !== "string" || /[{};<>]/.test(value))
323
+ throw new TypeError(`Invalid CSS variable: ${key}`);
324
+ return {
325
+ language,
326
+ dateOptions: {
327
+ ...options.locale === void 0 ? {} : { locale: options.locale },
328
+ ...options.timeZone === void 0 ? {} : { timeZone: options.timeZone }
329
+ },
330
+ direction,
331
+ locale,
332
+ timeZone,
333
+ labels,
334
+ brand: { ...brand },
335
+ columns: columns.map((column) => typeof column === "string" ? column : { ...column }),
336
+ actions: [...actions],
337
+ cssVariables: { ...cssVariables }
338
+ };
339
+ }
340
+ var defaultCustomization = resolveCustomization();
341
+
178
342
  // ../shared-theme/src/navigation.ts
179
343
  async function fetchThemePage(dataHref, pageHref, theme, signal, fresh = false, attempt = 0) {
180
344
  const source = new URL(dataHref, document.baseURI);
@@ -190,6 +354,20 @@ async function fetchThemePage(dataHref, pageHref, theme, signal, fresh = false,
190
354
  throw new ListingDataError("Unsupported folder snapshot");
191
355
  if (data.metadataHead !== void 0 && typeof data.metadataHead !== "string")
192
356
  throw new ListingDataError("Invalid folder metadata");
357
+ const sort = data.sort;
358
+ if (!sort || typeof sort !== "object" || !["name", "size", "modified"].includes(String(sort.field)) || !["asc", "desc"].includes(String(sort.direction)) || !["natural", "locale", "unicode"].includes(String(sort.nameMode)) || typeof sort.directoriesFirst !== "boolean")
359
+ throw new ListingDataError("Invalid folder order");
360
+ if (theme === "crosswave") {
361
+ const categories = data.categories;
362
+ const ids = /* @__PURE__ */ new Set();
363
+ if (!Array.isArray(categories) || categories.length === 0 || categories.some((category) => {
364
+ if (typeof category !== "object" || category === null || typeof category.id !== "string" || !category.id || ids.has(category.id) || typeof category.label !== "string" || typeof category.svg !== "string")
365
+ return true;
366
+ ids.add(category.id);
367
+ return false;
368
+ }))
369
+ throw new ListingDataError("Invalid folder categories");
370
+ }
193
371
  const base = document.querySelector("base")?.href ?? pageHref;
194
372
  const link = (raw) => {
195
373
  if (raw === null) return null;
@@ -209,6 +387,19 @@ async function fetchThemePage(dataHref, pageHref, theme, signal, fresh = false,
209
387
  } : {}
210
388
  }));
211
389
  if (entries.length > Number(data.total)) throw new ListingDataError("Invalid folder count");
390
+ if (data.page !== null) {
391
+ const page = data.page;
392
+ 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)
393
+ throw new ListingDataError("Invalid folder pagination");
394
+ data.page = {
395
+ ...page,
396
+ href: link(page.href),
397
+ previousHref: link(page.previousHref),
398
+ nextHref: link(page.nextHref)
399
+ };
400
+ }
401
+ if (data.ui !== void 0)
402
+ resolveCustomization(data.ui);
212
403
  const result = {
213
404
  ...data,
214
405
  updatesHref: link(data.updatesHref ?? null),
@@ -225,8 +416,11 @@ async function fetchThemePage(dataHref, pageHref, theme, signal, fresh = false,
225
416
  return { name: raw.name, href: link(raw.href), dataHref: link(raw.dataHref) };
226
417
  })
227
418
  };
228
- if (fresh && result.manifestHref) {
229
- const loaded = await loadListing(result.manifestHref, { signal, cache: "no-store" });
419
+ if (result.manifestHref) {
420
+ const loaded = await loadListing(result.manifestHref, {
421
+ signal,
422
+ cache: fresh ? "no-store" : "default"
423
+ });
230
424
  if (loaded.manifest.total !== data.total || data.listingRevision !== void 0 && loaded.manifest.revision !== data.listingRevision) {
231
425
  if (attempt < 2) return fetchThemePage(dataHref, pageHref, theme, signal, true, attempt + 1);
232
426
  throw new ListingDataError("Folder changed while refreshing");
@@ -0,0 +1,128 @@
1
+ // ../shared-theme/src/view-state.ts
2
+ var supportedTypes = ["directory", "file", "link"];
3
+ function validSort(value) {
4
+ if (typeof value !== "object" || value === null) return false;
5
+ const sort = value;
6
+ return ["name", "size", "modified"].includes(String(sort.field)) && ["asc", "desc"].includes(String(sort.direction)) && ["natural", "locale", "unicode"].includes(String(sort.nameMode)) && typeof sort.directoriesFirst === "boolean";
7
+ }
8
+ function position(value) {
9
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.min(value, 1e9)) : 0;
10
+ }
11
+ function readView(href, theme, path, defaults, categories = [], historyState = null) {
12
+ const params = new URL(href).searchParams;
13
+ const result = {
14
+ ...defaults,
15
+ query: (params.get("dw.q") ?? "").slice(0, 4096),
16
+ selected: null,
17
+ windowY: 0,
18
+ listTop: 0
19
+ };
20
+ if (theme === "ledger") {
21
+ const parts = params.get("dw.sort")?.split(":");
22
+ if (parts?.length === 4) {
23
+ const sort = {
24
+ field: parts[0],
25
+ direction: parts[1],
26
+ nameMode: parts[2],
27
+ directoriesFirst: parts[3] === "folders"
28
+ };
29
+ if (["folders", "mixed"].includes(parts[3]) && validSort(sort)) result.sort = sort;
30
+ }
31
+ if (params.has("dw.types")) {
32
+ const types = params.get("dw.types").split(",").filter(Boolean);
33
+ if (types.every((type) => supportedTypes.includes(type))) result.types = [...new Set(types)];
34
+ }
35
+ } else {
36
+ const category = params.get("dw.category");
37
+ if (category !== null && categories.includes(category)) result.category = category;
38
+ }
39
+ if (typeof historyState === "object" && historyState !== null && "dirwell" in historyState) {
40
+ const state = historyState.dirwell;
41
+ 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) {
42
+ const view = state.view;
43
+ result.selected = typeof view.selected === "string" && view.selected.length <= 4096 ? view.selected : null;
44
+ result.windowY = position(view.windowY);
45
+ result.listTop = position(view.listTop);
46
+ }
47
+ }
48
+ return result;
49
+ }
50
+ function viewHref(href, theme, view, defaults) {
51
+ const url = new URL(href);
52
+ for (const key of ["dw.q", "dw.sort", "dw.types", "dw.category"]) url.searchParams.delete(key);
53
+ if (view.query) url.searchParams.set("dw.q", view.query.slice(0, 4096));
54
+ if (theme === "ledger") {
55
+ if (view.sort && ["field", "direction", "nameMode", "directoriesFirst"].some(
56
+ (key) => view.sort[key] !== defaults.sort?.[key]
57
+ )) {
58
+ const sort = view.sort;
59
+ url.searchParams.set(
60
+ "dw.sort",
61
+ `${sort.field}:${sort.direction}:${sort.nameMode}:${sort.directoriesFirst ? "folders" : "mixed"}`
62
+ );
63
+ }
64
+ if (view.types && supportedTypes.some(
65
+ (type) => view.types.includes(type) !== (defaults.types ?? supportedTypes).includes(type)
66
+ ))
67
+ url.searchParams.set("dw.types", view.types.join(","));
68
+ } else if (view.category && view.category !== defaults.category)
69
+ url.searchParams.set("dw.category", view.category);
70
+ return url.href;
71
+ }
72
+ function createViewHistory(theme) {
73
+ let committedHref = location.href;
74
+ let committedState = history.state;
75
+ const stateFor = (dataHref, path, view) => ({
76
+ ...typeof history.state === "object" && history.state !== null ? history.state : {},
77
+ dataHref,
78
+ dirwell: {
79
+ version: 1,
80
+ theme,
81
+ path,
82
+ ...view === void 0 ? {} : {
83
+ view: {
84
+ query: view.query,
85
+ ...view.sort === void 0 ? {} : { sort: { ...view.sort } },
86
+ ...view.types === void 0 ? {} : { types: [...view.types] },
87
+ category: view.category ?? null,
88
+ selected: view.selected ?? null,
89
+ windowY: view.windowY ?? 0,
90
+ listTop: view.listTop ?? 0
91
+ }
92
+ }
93
+ }
94
+ });
95
+ return {
96
+ save(dataHref, path, view, defaults) {
97
+ if (location.href !== committedHref) return;
98
+ const href = viewHref(committedHref, theme, view, defaults);
99
+ const state = stateFor(dataHref, path, view);
100
+ try {
101
+ history.replaceState(state, "", href);
102
+ committedHref = href;
103
+ committedState = state;
104
+ } catch {
105
+ }
106
+ },
107
+ commit(dataHref, path, href, push) {
108
+ const state = stateFor(dataHref, path);
109
+ if (push) history.pushState(state, "", href);
110
+ else history.replaceState(state, "", href);
111
+ committedHref = href;
112
+ committedState = state;
113
+ },
114
+ rollback() {
115
+ if (location.href !== committedHref) {
116
+ try {
117
+ history.replaceState(committedState, "", committedHref);
118
+ } catch {
119
+ }
120
+ }
121
+ }
122
+ };
123
+ }
124
+ export {
125
+ createViewHistory,
126
+ readView,
127
+ viewHref
128
+ };
package/dist/styles.css CHANGED
@@ -1,11 +1,15 @@
1
1
 
2
+ .cw-custom-footer{min-width:0;overflow-wrap:anywhere;font-size:14px;flex:1 1 auto}.cw-data-value{min-width:0;overflow-wrap:anywhere}.cw-row[data-custom-columns=true] .cw-row-date{display:block}
3
+
4
+ .cw-row-actions{display:flex;flex-wrap:wrap;gap:.5rem;padding:0 12px 12px}.cw-row-actions button,.cw-row-actions a{min-height:44px;padding:8px 12px;border:1px solid currentColor;background:transparent;border-radius:4px}
5
+
2
6
  :root{color-scheme:dark;--cw-top:#2b4f85;--cw-bottom:#091a3a;--cw-text:#f7fbff;--cw-muted:#edf4fc;--cw-focus:#fff;--cw-ease:cubic-bezier(.16,1,.3,1);font-family:"Segoe UI",Helvetica,Arial,sans-serif;font-synthesis:none}
3
7
  :root[data-cw-color=violet]{--cw-top:#5c407d;--cw-bottom:#241532;--cw-muted:#edf4fc}:root[data-cw-color=amber]{--cw-top:#704d1a;--cw-bottom:#30200b;--cw-muted:#edf4fc}:root[data-cw-color=rose]{--cw-top:#704055;--cw-bottom:#351b29;--cw-muted:#edf4fc}:root[data-cw-color=jade]{--cw-top:#215e52;--cw-bottom:#0b2a28;--cw-muted:#edf4fc}:root[data-cw-color=graphite]{--cw-top:#424d61;--cw-bottom:#181d28;--cw-muted:#edf4fc}
4
8
  *{box-sizing:border-box}[hidden]{display:none!important}html{scroll-padding-bottom:110px;min-height:100%;background:var(--cw-bottom)}body{margin:0;color:var(--cw-text);background:var(--cw-bottom);min-height:100svh;font-size:16px;line-height:1.5}::selection{background:#d0e4fc;color:#132848}a{color:inherit;text-decoration:none}button,input,select{font:inherit;color:inherit}button,select{cursor:pointer}button{border:0}a:focus-visible,button:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--cw-focus);outline-offset:6px;border-radius:3px}svg{display:block;width:24px;height:24px;flex:none}.cw-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.cw-skip{position:fixed;left:24px;top:16px;padding:12px 18px;background:#fff;color:#142e53;z-index:20;transform:translateY(-150%)}.cw-skip:focus{transform:none}
5
9
  .cw-background{position:fixed;inset:0;z-index:0;pointer-events:none;background:radial-gradient(ellipse at 28% 4%,#c0d7ff12,transparent 68%),linear-gradient(165deg,var(--cw-top),var(--cw-bottom));view-transition-name:cw-background;overflow:hidden}.cw-background canvas{width:100%;height:100%;display:block}.cw-fallback-wave{position:absolute;inset:0;width:100%;height:100%;fill:none;stroke:#d5e8ff;stroke-width:18;opacity:.06;filter:blur(3px)}.cw-background[data-ready] .cw-fallback-wave{display:none}
6
10
  .cw-top,.cw-stage,.cw-bottom,.cw-noscript{position:relative;z-index:1}.cw-top{display:flex;align-items:center;gap:32px;padding:30px 48px 20px;min-height:92px;view-transition-name:cw-header}.cw-brand{font-size:20px;font-weight:500;letter-spacing:-.02em;white-space:nowrap}.cw-brand>a:hover{text-decoration:underline;text-underline-offset:5px}.cw-location{display:flex;gap:12px;align-items:center;min-width:0;flex-wrap:wrap;font-size:13px;color:var(--cw-muted)}.cw-location a:hover{text-decoration:underline;text-underline-offset:5px}.cw-separator{opacity:.6}.cw-clock{margin-left:auto;white-space:nowrap;font-variant-numeric:tabular-nums;font-size:14px;color:var(--cw-muted)}
7
11
  .cw-stage{max-width:1500px;margin:28px auto 110px;padding:0 72px;display:grid;grid-template-columns:minmax(0,1.55fr) minmax(220px,1fr);column-gap:80px}.cw-rail{grid-column:1/-1;margin:0 -12px 22px;overflow-x:auto;padding:12px 12px 24px;scrollbar-width:none}.cw-rail::-webkit-scrollbar{display:none}.cw-categories{padding-left:104px;padding-right:calc(100vw - 200px);display:flex;gap:32px;min-width:max-content;align-items:flex-start}.cw-category{position:relative;display:flex;flex-direction:column;align-items:center;gap:12px;min-width:94px;min-height:96px;background:none;padding:10px 4px;color:var(--cw-muted);transition:color .2s,transform .3s var(--cw-ease);flex:none}.cw-category svg{width:40px;height:40px;filter:drop-shadow(0 3px 2px #07182a55)}.cw-category span{font-size:14px;white-space:nowrap}.cw-category small{font-size:10px;position:absolute;right:8px;top:0;color:var(--cw-muted)}.cw-category:hover{color:#fff;transform:translateY(-3px)}.cw-category.cw-active{color:#fff;transform:scale(1.1)}.cw-category.cw-active:after{content:"";position:absolute;left:calc(50% - 12px);bottom:-8px;width:24px;height:2px;background:#e6f2ff;box-shadow:0 3px 10px #bddaff55}
8
- .cw-browser{padding-left:104px;min-width:0;view-transition-name:cw-browser}.cw-list-head{display:flex;align-items:center;justify-content:space-between;gap:24px;min-height:58px;margin:0 0 12px;padding:0 4px}.cw-directory{display:flex;align-items:center;gap:16px;min-width:0}.cw-directory h2{font-size:17px;font-weight:400;margin:0;white-space:nowrap}.cw-directory>[data-cw-count]{font-size:12px;color:var(--cw-muted);white-space:nowrap}.cw-parent,.cw-parent-placeholder{width:34px;height:44px;display:flex;align-items:center;justify-content:center;flex:none}.cw-parent:hover{background:#ffffff12;border-radius:5px}.cw-search{display:flex;align-items:center;gap:8px;border-bottom:1px solid #d5e7ff55;min-width:140px;width:220px;padding:6px 0}.cw-search svg{width:17px;height:17px;color:var(--cw-muted)}.cw-search input{background:none;border:0;min-width:0;width:100%;outline-offset:3px;font-size:13px;caret-color:#fff}.cw-search input::placeholder{color:var(--cw-muted);opacity:1}.cw-search button{background:none;font-size:11px;padding:6px 0}.cw-search input::-webkit-search-cancel-button{display:none}
12
+ .cw-browser{padding-left:104px;min-width:0;view-transition-name:cw-browser}.cw-list-head{display:flex;align-items:center;justify-content:space-between;gap:24px;min-height:58px;margin:0 0 12px;padding:0 4px}.cw-directory{display:flex;align-items:center;gap:16px;min-width:0}.cw-directory h2{font-size:17px;font-weight:400;margin:0;white-space:nowrap}.cw-directory>[data-cw-count]{font-size:12px;color:var(--cw-muted);white-space:nowrap}.cw-parent,.cw-parent-placeholder{width:34px;height:44px;display:flex;align-items:center;justify-content:center;flex:none}.cw-parent:hover{background:#ffffff12;border-radius:5px}.cw-search{display:flex;align-items:center;gap:8px;border-bottom:1px solid #d5e7ff55;min-width:140px;width:220px;padding:6px 0}.cw-search>span{white-space:nowrap;flex:none}.cw-search svg{width:17px;height:17px;color:var(--cw-muted)}.cw-search input{background:none;border:0;min-width:0;width:100%;outline-offset:3px;font-size:13px;caret-color:#fff}.cw-search input::placeholder{color:var(--cw-muted);opacity:1}.cw-search button{background:none;font-size:11px;padding:6px 0}.cw-search input::-webkit-search-cancel-button{display:none}
9
13
  .cw-list-scroll{max-height:calc(100svh - 365px);min-height:220px;overflow-y:auto;overflow-x:hidden;scrollbar-width:thin;scrollbar-color:#d6e8fa66 transparent;padding:14px 12px 24px 8px;scroll-padding-block:20px;overscroll-behavior:contain}.cw-list-scroll::-webkit-scrollbar{width:5px}.cw-list-scroll::-webkit-scrollbar-thumb{background:#d6e8fa66;border-radius:4px}.cw-files{list-style:none;padding:0;margin:0}.cw-row{margin:0;min-height:76px;padding:0}.cw-entry{display:flex;align-items:center;gap:25px;padding:14px 18px;min-height:76px;transition:background .2s,opacity .2s,transform .3s var(--cw-ease);border-radius:7px}.cw-entry-icon{display:flex;width:42px;min-width:42px;align-items:center;justify-content:center;transition:transform .3s var(--cw-ease);filter:drop-shadow(0 3px 2px #08192c66)}.cw-entry-icon svg{width:34px;height:34px}.cw-entry-label{min-width:0;display:block}.cw-name{display:block;font-size:18px;font-weight:400;line-height:1.4;overflow-wrap:anywhere;text-shadow:0 2px 3px #07192b66}.cw-row-meta{display:block;font-size:11px;color:var(--cw-muted);margin-top:3px}.cw-row-date{display:none;font-size:11px;color:var(--cw-muted);margin-top:4px}.cw-target{display:block;font-size:11px;color:var(--cw-muted);overflow-wrap:anywhere;margin-top:4px}.cw-row.cw-selected .cw-entry{background:linear-gradient(90deg,#f4f9ff08,#f4f9ff04);transform:translateX(5px)}.cw-row.cw-selected .cw-entry-icon{transform:scale(1.22)}.cw-row.cw-selected .cw-name{color:#fff}.cw-row:hover .cw-entry{background:#eef7ff08}.cw-unavailable{color:var(--cw-muted)}.cw-notice{position:fixed;bottom:calc(var(--cw-footer-height,82px) + 8px);left:48px;right:24px;z-index:3;margin:0;background:var(--cw-bottom);border-radius:4px;font-size:12px;color:var(--cw-muted);padding:12px 24px;max-width:52ch}.cw-empty{padding:36px 28px;color:var(--cw-muted);font-size:15px}.cw-detail{min-width:0;align-self:center;padding:20px 0 28px;view-transition-name:cw-detail}.cw-detail-symbol{margin-bottom:28px;filter:drop-shadow(0 12px 12px #04152e44)}.cw-detail-symbol svg{width:110px;height:110px;stroke-width:.8;opacity:.9}.cw-detail h2{font-size:25px;font-weight:300;margin:0 0 7px;overflow-wrap:anywhere;line-height:1.3;letter-spacing:-.015em}.cw-detail>p{font-size:12px;color:var(--cw-muted);margin:0 0 25px}.cw-detail dl{margin:0;font-size:12px}.cw-detail dl>div{display:grid;grid-template-columns:70px minmax(0,1fr);gap:10px;margin-bottom:10px}.cw-detail dt{color:var(--cw-muted)}.cw-detail dd{margin:0;overflow-wrap:anywhere}.cw-open{display:inline-flex;margin-top:24px;min-height:44px;align-items:center;font-size:14px;border-bottom:1px solid #e8f1ff88;gap:12px}.cw-open:hover{border-bottom-color:#fff}.cw-open:after{content:"";display:block;width:7px;height:7px;border-top:1px solid currentColor;border-right:1px solid currentColor;transform:rotate(45deg)}
10
14
  .cw-bottom{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:22px 48px;position:fixed;bottom:0;left:0;right:0;background:linear-gradient(0deg,var(--cw-bottom),transparent);min-height:82px}.cw-credits{display:flex;align-items:center;gap:6px;margin:0;font-size:12px;font-weight:400;letter-spacing:normal;color:var(--cw-muted)}.cw-credits a{display:inline-flex;align-items:center;min-height:24px;padding-inline:2px;text-decoration:underline;text-underline-offset:4px;text-decoration-color:#edf4fc66}.cw-credits a:hover{text-decoration-color:currentColor}.cw-key-help{display:flex;gap:20px;flex-wrap:wrap;font-size:11px;color:var(--cw-muted)}kbd{font:inherit;color:#fff;margin-right:5px}.cw-appearance{display:flex;align-items:center;gap:16px;flex:none}.cw-appearance select{background:#0b193b70;border:1px solid #d7e9ff33;padding:7px 24px 7px 10px;border-radius:4px;max-width:116px;font-size:12px;min-height:36px}.cw-appearance button{display:flex;align-items:center;gap:8px;background:transparent;font-size:12px;min-height:40px;padding:6px 0}.cw-appearance button svg{width:16px;height:16px}.cw-controller{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--cw-muted)}.cw-controller svg{width:22px;height:22px}.cw-noscript{padding:0 48px 24px;color:var(--cw-muted);font-size:13px}
11
15
  body:not([data-cw-enhanced]) .cw-browser{padding-left:0}body:not([data-cw-enhanced]) .cw-stage{display:block;margin-top:20px}body:not([data-cw-enhanced]) .cw-list-scroll{max-height:none;min-height:0}body:not([data-cw-enhanced]) .cw-parent-placeholder{display:none}body:not([data-cw-enhanced]) .cw-bottom{display:none}
@@ -15,6 +19,7 @@ body:not([data-cw-enhanced]) .cw-browser{padding-left:0}body:not([data-cw-enhanc
15
19
  @media(max-width:1000px){.cw-top{padding:24px 30px;gap:22px}.cw-stage{padding:0 36px;column-gap:35px}.cw-categories{padding-left:44px;gap:20px}.cw-browser{padding-left:44px}.cw-category{min-width:82px}.cw-list-head{flex-wrap:wrap;gap:8px}.cw-search{margin-left:52px;width:calc(100% - 52px)}.cw-list-scroll{max-height:calc(100svh - 390px)}.cw-bottom{padding:20px 30px}.cw-key-help{gap:12px}.cw-detail h2{font-size:21px}.cw-clock{font-size:12px}}
16
20
  @media(max-width:680px){.cw-top{padding:22px 20px 12px;flex-wrap:wrap;gap:16px;min-height:104px}.cw-brand{font-size:18px}.cw-clock{order:1;font-size:11px}.cw-location{order:2;width:100%;font-size:12px;gap:8px}.cw-stage{padding:0 16px;margin:12px auto 100px;display:block}.cw-rail{margin:0 -8px 8px;padding:12px 8px 20px}.cw-categories{padding-left:0;gap:14px}.cw-browser{padding-left:0}.cw-category{min-width:76px;min-height:83px;padding:10px 2px;gap:10px}.cw-category svg{width:33px;height:33px}.cw-category span{font-size:12px}.cw-category small{right:5px}.cw-list-head{padding:0 12px;margin-bottom:8px}.cw-directory{gap:12px}.cw-directory h2{font-size:16px}.cw-search{margin-left:46px;width:calc(100% - 46px);min-height:44px}.cw-search input,.cw-search button{min-height:44px}.cw-list-scroll{max-height:none;min-height:150px;padding:8px 2px 18px}.cw-entry{gap:20px;padding:14px 12px;min-height:78px}.cw-entry-icon{width:36px;min-width:36px}.cw-entry-icon svg{width:29px;height:29px}.cw-name{font-size:17px}.cw-detail{display:none!important}.cw-row-date{display:block}.cw-bottom{padding:16px 20px;min-height:76px;gap:12px}.cw-key-help{display:none!important}.cw-appearance{width:100%;justify-content:space-between}.cw-appearance select,.cw-appearance button{min-height:44px}.cw-controller{position:absolute;bottom:calc(var(--cw-footer-height,76px) + 4px);right:20px;background:var(--cw-bottom);padding:6px 9px;border-radius:4px;max-width:calc(100% - 40px)}.cw-notice{left:20px;right:20px;bottom:calc(var(--cw-footer-height,82px) + 8px)}.cw-noscript{padding:0 24px 24px}}
17
21
  @media(max-height:520px){.cw-stage{grid-template-columns:minmax(0,1fr)}.cw-detail{display:none!important}.cw-row-date{display:block}.cw-top{flex-wrap:nowrap}.cw-location{order:0;width:auto}.cw-clock{display:none!important}.cw-top{min-height:58px;padding-top:10px;padding-bottom:8px}.cw-stage{margin-top:0;margin-bottom:70px}.cw-rail{margin-bottom:0;padding-bottom:12px}.cw-category{min-height:68px;gap:4px;padding-top:4px}.cw-category svg{width:28px;height:28px}.cw-list-scroll{max-height:calc(100svh - 235px);min-height:80px}.cw-list-head{min-height:36px;margin-bottom:0}.cw-detail-symbol{margin-bottom:10px}.cw-detail-symbol svg{width:60px;height:60px}.cw-detail>p{margin-bottom:12px}.cw-bottom{min-height:60px;padding-block:8px}.cw-key-help{display:none!important}.cw-list-head{flex-wrap:nowrap}.cw-search{margin-left:0;width:170px}.cw-detail dl>div{margin-bottom:4px}}
22
+ .cw-list-scroll[data-virtual=true]{max-height:var(--cw-list-height,calc(100svh - 365px));min-height:160px}.cw-stage{margin-bottom:calc(var(--cw-footer-height,82px) + 24px)}
18
23
  @media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation:none!important;transition:none!important}::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation:none!important}.cw-row.cw-selected .cw-entry,.cw-category.cw-active,.cw-category:hover{transform:none}}
19
24
 
20
25
  .cw-directory{flex-wrap:wrap;gap:.35rem}.cw-list-head h2{font-size:17px;font-weight:400;margin:0}.cw-list-head [data-cw-count]{font-size:12px;color:var(--cw-muted)}