vitepress-plugin-filelist 0.2.2 → 0.3.0

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.
@@ -6,6 +6,7 @@ interface FileListLabels {
6
6
  filter: string;
7
7
  refresh: string;
8
8
  batch: string;
9
+ /** @deprecated No longer shown; the selection count lives in the toolbar. */
9
10
  batchCount: string;
10
11
  batching: string;
11
12
  passwordPrompt: string;
@@ -33,6 +34,23 @@ interface FileListLabels {
33
34
  previewFailed: string;
34
35
  batchFailed: string;
35
36
  defaultZip: string;
37
+ /** Text button that clears the selection (wide layout). */
38
+ clear: string;
39
+ /** Accessible name of the clear-selection button. */
40
+ clearSelection: string;
41
+ /** `{size}` total, used for the selection and the footer. */
42
+ totalSize: string;
43
+ /** `{count}` files inside a loaded folder. */
44
+ folderItems: string;
45
+ /** Label of the compact sort select shown in narrow containers. */
46
+ sortBy: string;
47
+ selectAll: string;
48
+ /** Accessible name of a row checkbox, `{name}`. */
49
+ selectItem: string;
50
+ /** Accessible name of the expand button, `{name}`. */
51
+ expand: string;
52
+ /** Accessible name of the collapse button, `{name}`. */
53
+ collapse: string;
36
54
  }
37
55
  interface FileListSource {
38
56
  /** OpenList path, directory or single file. */
@@ -171,9 +189,28 @@ export interface SortableNode {
171
189
  export type SortKey = 'name' | 'size' | 'modified';
172
190
  export declare function joinPath(parent: string, name: string): string;
173
191
  export declare function encodePath(path: string): string;
192
+ /**
193
+ * Make one path segment safe as a ZIP entry segment: no separators,
194
+ * no `.` / `..` traversal, no empty names.
195
+ */
196
+ export declare function zipSegment(name: string): string;
197
+ /** Join path segments into a ZIP entry name, sanitising each segment. */
198
+ export declare function zipEntryName(segments: string[]): string;
199
+ /**
200
+ * Return `name`, or `name (2)`, `name (3)`... when it is already in `used`.
201
+ * The suffix goes before the extension of the last segment. Adds the result to `used`.
202
+ */
203
+ export declare function uniqueEntryName(name: string, used: Set<string>): string;
174
204
  export declare function formatSize(size: number): string;
175
205
  export declare function formatDate(iso: string): string;
176
206
  export declare function extOf(name: string): string;
207
+ export type FileKind = 'doc' | 'sheet' | 'slide' | 'pdf' | 'image' | 'media' | 'archive' | 'code' | 'other';
208
+ /** Rough file family, used to colour the type badge. */
209
+ export declare function fileKind(name: string): FileKind;
210
+ /** Sum of the `size` of the given entries, ignoring missing or invalid sizes. */
211
+ export declare function totalSize(items: Array<{
212
+ size?: number;
213
+ }>): number;
177
214
  export declare function compareNodes(a: SortableNode, b: SortableNode, key: SortKey, ascending?: boolean): number;
178
215
  /**
179
216
  * Register `<FileList>` and `<VPFileList>` globally.
@@ -1,5 +1,5 @@
1
- import { computed, defineComponent, mergeProps, onMounted, ref, unref, useSSRContext } from "vue";
2
- import { ssrIncludeBooleanAttr, ssrInterpolate, ssrRenderAttr, ssrRenderAttrs, ssrRenderClass, ssrRenderList, ssrRenderStyle } from "vue/server-renderer";
1
+ import { computed, defineComponent, mergeProps, onBeforeUnmount, onMounted, ref, unref, useSSRContext } from "vue";
2
+ import { ssrIncludeBooleanAttr, ssrInterpolate, ssrLooseContain, ssrLooseEqual, ssrRenderAttr, ssrRenderAttrs, ssrRenderClass, ssrRenderList, ssrRenderStyle } from "vue/server-renderer";
3
3
  // src/client/locales.ts
4
4
  const fileListLocales = {
5
5
  "zh-CN": {
@@ -7,7 +7,7 @@ const fileListLocales = {
7
7
  demoBadge: "演示数据",
8
8
  filter: "筛选文件名",
9
9
  refresh: "刷新",
10
- batch: "批量下载",
10
+ batch: "下载 ZIP",
11
11
  batchCount: "批量下载({count})",
12
12
  batching: "打包中 {percent}%",
13
13
  passwordPrompt: "该目录受密码保护:",
@@ -34,7 +34,16 @@ const fileListLocales = {
34
34
  downloadFailed: "下载失败:{message}",
35
35
  previewFailed: "预览失败:{message}",
36
36
  batchFailed: "打包失败:{message}",
37
- defaultZip: "资源"
37
+ defaultZip: "资源",
38
+ clear: "清除",
39
+ clearSelection: "取消选择",
40
+ totalSize: "共 {size}",
41
+ folderItems: "{count} 项",
42
+ sortBy: "排序",
43
+ selectAll: "全选",
44
+ selectItem: "选择 {name}",
45
+ expand: "展开 {name}",
46
+ collapse: "收起 {name}"
38
47
  },
39
48
  "en-US": {
40
49
  allFiles: "All files",
@@ -68,7 +77,16 @@ const fileListLocales = {
68
77
  downloadFailed: "Download failed: {message}",
69
78
  previewFailed: "Preview failed: {message}",
70
79
  batchFailed: "Failed to create ZIP: {message}",
71
- defaultZip: "resources"
80
+ defaultZip: "resources",
81
+ clear: "Clear",
82
+ clearSelection: "Clear selection",
83
+ totalSize: "{size} total",
84
+ folderItems: "{count} items",
85
+ sortBy: "Sort",
86
+ selectAll: "Select all",
87
+ selectItem: "Select {name}",
88
+ expand: "Expand {name}",
89
+ collapse: "Collapse {name}"
72
90
  }
73
91
  };
74
92
  function formatLabel(template, values = {}) {
@@ -87,6 +105,41 @@ function joinPath(parent, name) {
87
105
  function encodePath(path) {
88
106
  return path.split("/").map(encodeURIComponent).join("/");
89
107
  }
108
+ /**
109
+ * Make one path segment safe as a ZIP entry segment: no separators,
110
+ * no `.` / `..` traversal, no empty names.
111
+ */
112
+ function zipSegment(name) {
113
+ const cleaned = name.replace(/[\\/]/g, "_").trim();
114
+ if (!cleaned || cleaned === "." || cleaned === "..") return "_";
115
+ return cleaned;
116
+ }
117
+ /** Join path segments into a ZIP entry name, sanitising each segment. */
118
+ function zipEntryName(segments) {
119
+ return segments.map(zipSegment).join("/");
120
+ }
121
+ /**
122
+ * Return `name`, or `name (2)`, `name (3)`... when it is already in `used`.
123
+ * The suffix goes before the extension of the last segment. Adds the result to `used`.
124
+ */
125
+ function uniqueEntryName(name, used) {
126
+ let candidate = name;
127
+ if (used.has(candidate)) {
128
+ const slash = name.lastIndexOf("/");
129
+ const dir = name.slice(0, slash + 1);
130
+ const base = name.slice(slash + 1);
131
+ const dot = base.lastIndexOf(".");
132
+ const stem = dot > 0 ? base.slice(0, dot) : base;
133
+ const ext = dot > 0 ? base.slice(dot) : "";
134
+ let index = 2;
135
+ do {
136
+ candidate = `${dir}${stem} (${index})${ext}`;
137
+ index += 1;
138
+ } while (used.has(candidate));
139
+ }
140
+ used.add(candidate);
141
+ return candidate;
142
+ }
90
143
  function formatSize(size) {
91
144
  if (!size || !Number.isFinite(size)) return "-";
92
145
  const units = [
@@ -116,6 +169,97 @@ function extOf(name) {
116
169
  const match = name.match(/\.([a-z0-9]+)$/i);
117
170
  return match ? match[1].toUpperCase() : "";
118
171
  }
172
+ const FILE_KINDS = {};
173
+ for (const [kind, exts] of Object.entries({
174
+ doc: [
175
+ "doc",
176
+ "docx",
177
+ "odt",
178
+ "rtf",
179
+ "txt",
180
+ "md",
181
+ "pages",
182
+ "wps"
183
+ ],
184
+ sheet: [
185
+ "xls",
186
+ "xlsx",
187
+ "ods",
188
+ "csv",
189
+ "tsv",
190
+ "numbers",
191
+ "et"
192
+ ],
193
+ slide: [
194
+ "ppt",
195
+ "pptx",
196
+ "odp",
197
+ "key",
198
+ "dps"
199
+ ],
200
+ pdf: ["pdf"],
201
+ image: [
202
+ "png",
203
+ "jpg",
204
+ "jpeg",
205
+ "gif",
206
+ "webp",
207
+ "svg",
208
+ "bmp",
209
+ "ico",
210
+ "avif",
211
+ "heic"
212
+ ],
213
+ media: [
214
+ "mp3",
215
+ "wav",
216
+ "flac",
217
+ "aac",
218
+ "ogg",
219
+ "m4a",
220
+ "mp4",
221
+ "webm",
222
+ "mov",
223
+ "mkv",
224
+ "avi"
225
+ ],
226
+ archive: [
227
+ "zip",
228
+ "rar",
229
+ "7z",
230
+ "tar",
231
+ "gz",
232
+ "tgz",
233
+ "bz2",
234
+ "xz",
235
+ "iso"
236
+ ],
237
+ code: [
238
+ "js",
239
+ "ts",
240
+ "json",
241
+ "html",
242
+ "css",
243
+ "py",
244
+ "java",
245
+ "c",
246
+ "cpp",
247
+ "go",
248
+ "rs",
249
+ "sh",
250
+ "yml",
251
+ "yaml",
252
+ "xml"
253
+ ]
254
+ })) for (const ext of exts) FILE_KINDS[ext] = kind;
255
+ /** Rough file family, used to colour the type badge. */
256
+ function fileKind(name) {
257
+ return FILE_KINDS[extOf(name).toLowerCase()] ?? "other";
258
+ }
259
+ /** Sum of the `size` of the given entries, ignoring missing or invalid sizes. */
260
+ function totalSize(items) {
261
+ return items.reduce((sum, item) => sum + (Number.isFinite(item.size) && (item.size ?? 0) > 0 ? item.size : 0), 0);
262
+ }
119
263
  function compareNodes(a, b, key, ascending = true) {
120
264
  if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
121
265
  let result = 0;
@@ -181,6 +325,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
181
325
  function errorText(error) {
182
326
  return error instanceof Error ? error.message : String(error);
183
327
  }
328
+ function isPasswordError(error) {
329
+ return /password|密码/i.test(errorText(error));
330
+ }
184
331
  const DEMO_TREE = {
185
332
  "/": [
186
333
  {
@@ -364,7 +511,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
364
511
  nodes.sort((a, b) => compareNodes(a, b, sortKey.value, sortAsc.value));
365
512
  for (const node of nodes) if (node.children.length) sortTree(node.children);
366
513
  }
367
- function toNodes(items, parent, depth, password = "") {
514
+ function toNodes(items, parent, depth, password = "", zipParent = []) {
368
515
  return items.map((item) => ({
369
516
  ...item,
370
517
  path: joinPath(parent, item.name),
@@ -373,9 +520,15 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
373
520
  loaded: false,
374
521
  loading: false,
375
522
  children: [],
376
- password
523
+ password,
524
+ zipPath: [...zipParent, item.name]
377
525
  }));
378
526
  }
527
+ function resortAll() {
528
+ if (props.sources.length) {
529
+ for (const root of roots.value) if (root.children.length) sortTree(root.children);
530
+ } else sortTree(roots.value);
531
+ }
379
532
  function sourceName(path) {
380
533
  const parts = path.split("/").filter(Boolean);
381
534
  return parts[parts.length - 1] || path;
@@ -384,11 +537,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
384
537
  if (node.loaded) return true;
385
538
  node.loading = true;
386
539
  try {
387
- node.children = toNodes(await listDir(node.path, node.password), node.path, node.depth + 1, node.password);
540
+ node.children = toNodes(await listDir(node.path, node.password || activePassword.value), node.path, node.depth + 1, node.password, node.zipPath);
388
541
  sortTree(node.children);
389
542
  node.loaded = true;
390
543
  return true;
391
544
  } catch (error) {
545
+ if (isPasswordError(error)) needsPassword.value = true;
392
546
  showToast(t("folderFailed", { message: errorText(error) }));
393
547
  return false;
394
548
  } finally {
@@ -413,16 +567,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
413
567
  } else try {
414
568
  const data = await request("/api/fs/get", {
415
569
  path,
416
- password
570
+ password: password || activePassword.value
417
571
  });
418
572
  item = {
419
573
  ...item,
420
574
  ...data
421
575
  };
422
576
  } catch {}
577
+ const name = source.title || item.name || sourceName(path);
423
578
  return {
424
579
  ...item,
425
- name: source.title || item.name || sourceName(path),
580
+ name,
426
581
  path,
427
582
  depth: 0,
428
583
  expanded: source.expanded ?? props.expanded,
@@ -430,7 +585,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
430
585
  loading: false,
431
586
  children: [],
432
587
  password,
433
- isRoot: true
588
+ isRoot: true,
589
+ zipPath: [item.is_dir ? name : item.name || sourceName(path)]
434
590
  };
435
591
  }
436
592
  async function loadRoot() {
@@ -453,7 +609,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
453
609
  downloadCache.clear();
454
610
  } catch (error) {
455
611
  const message = errorText(error);
456
- if (/password|密码/i.test(message)) needsPassword.value = true;
612
+ if (isPasswordError(error)) needsPassword.value = true;
457
613
  else errorMsg.value = message;
458
614
  } finally {
459
615
  loadingRoot.value = false;
@@ -489,6 +645,36 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
489
645
  const allFiles = computed(() => roots.value.flatMap(descendantFiles));
490
646
  const allChecked = computed(() => allFiles.value.length > 0 && allFiles.value.every((file) => selected.value.has(file.path)));
491
647
  const someChecked = computed(() => !allChecked.value && allFiles.value.some((file) => selected.value.has(file.path)));
648
+ const selectedFiles = computed(() => allFiles.value.filter((file) => selected.value.has(file.path)));
649
+ const selectedBytes = computed(() => totalSize(selectedFiles.value));
650
+ const loadedBytes = computed(() => totalSize(allFiles.value));
651
+ function folderFiles(node) {
652
+ return node.is_dir && node.loaded ? descendantFiles(node) : [];
653
+ }
654
+ /** Compact sort control for narrow containers, where header cells are hidden. */
655
+ const sortOptions = computed(() => [
656
+ "name",
657
+ "size",
658
+ "modified"
659
+ ].flatMap((key) => {
660
+ const label = t(key === "name" ? "columnName" : key === "size" ? "columnSize" : "columnDate");
661
+ return [{
662
+ value: `${key}:asc`,
663
+ label: `${label} ↑`
664
+ }, {
665
+ value: `${key}:desc`,
666
+ label: `${label} ↓`
667
+ }];
668
+ }));
669
+ const sortValue = computed({
670
+ get: () => `${sortKey.value}:${sortAsc.value ? "asc" : "desc"}`,
671
+ set: (value) => {
672
+ const [key, direction] = value.split(":");
673
+ sortKey.value = key;
674
+ sortAsc.value = direction !== "desc";
675
+ resortAll();
676
+ }
677
+ });
492
678
  const unloadedFolders = computed(() => {
493
679
  let count = 0;
494
680
  const walk = (nodes) => {
@@ -505,18 +691,52 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
505
691
  return !node.is_dir && previewable.test(node.name);
506
692
  }
507
693
  const displayTitle = computed(() => props.title || siteTitle.value || (props.path === "/" ? t("allFiles") : props.path));
694
+ const stickyTop = ref(0);
695
+ function measureStickyTop() {
696
+ if (typeof document === "undefined") return;
697
+ let top = 0;
698
+ for (const el of document.querySelectorAll(".VPNav, .VPLocalNav")) {
699
+ const style = getComputedStyle(el);
700
+ if (style.display === "none" || style.position !== "fixed" && style.position !== "sticky") continue;
701
+ top = Math.max(top, (Number.parseFloat(style.top) || 0) + el.offsetHeight);
702
+ }
703
+ stickyTop.value = top;
704
+ }
508
705
  onMounted(() => {
509
706
  loadRoot();
510
707
  loadSiteTitle();
708
+ measureStickyTop();
709
+ window.addEventListener("resize", measureStickyTop, { passive: true });
710
+ });
711
+ onBeforeUnmount(() => {
712
+ window.removeEventListener("resize", measureStickyTop);
713
+ clearTimeout(toastTimer);
511
714
  });
512
715
  return (_ctx, _push, _parent, _attrs) => {
513
- _push(`<div${ssrRenderAttrs(mergeProps({ class: "fl-root" }, _attrs))}><div class="fl-bar"><div class="fl-title"><span class="fl-title-icon">🗂️</span><span class="fl-title-text">${ssrInterpolate(displayTitle.value)}</span>`);
514
- if (demo.value) _push(`<span class="fl-demo-badge">${ssrInterpolate(t("demoBadge"))}</span>`);
716
+ _push(`<div${ssrRenderAttrs(mergeProps({
717
+ class: ["fl-root", { "fl-selecting": selected.value.size > 0 }],
718
+ style: { "--fl-sticky-top-auto": `${stickyTop.value}px` }
719
+ }, _attrs))}><div class="fl-bar">`);
720
+ if (selected.value.size) _push(`<div class="fl-selection" role="status"><span class="fl-selection-count">${ssrInterpolate(t("selected", { count: selected.value.size }))}</span><span class="fl-selection-size">${ssrInterpolate(t("totalSize", { size: unref(formatSize)(selectedBytes.value) }))}</span></div>`);
721
+ else {
722
+ _push(`<div class="fl-title"><span class="fl-title-text">${ssrInterpolate(displayTitle.value)}</span>`);
723
+ if (demo.value) _push(`<span class="fl-demo-badge">${ssrInterpolate(t("demoBadge"))}</span>`);
724
+ else _push(`<!---->`);
725
+ _push(`</div>`);
726
+ }
727
+ if (selected.value.size) _push(`<button class="fl-clear" type="button"${ssrRenderAttr("aria-label", t("clearSelection"))}${ssrRenderAttr("title", t("clearSelection"))}><svg class="fl-clear-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6 6 18"></path></svg><span class="fl-clear-text">${ssrInterpolate(t("clear"))}</span></button>`);
515
728
  else _push(`<!---->`);
516
- _push(`</div><div class="fl-tools"><input${ssrRenderAttr("value", keyword.value)} class="fl-search" type="text"${ssrRenderAttr("placeholder", t("filter"))}><button class="fl-btn" type="button">${ssrInterpolate(t("refresh"))}</button><button class="fl-btn fl-btn-primary" type="button"${ssrIncludeBooleanAttr(selected.value.size === 0 || batchBusy.value) ? " disabled" : ""}>${ssrInterpolate(batchBusy.value ? t("batching", { percent: batchProgress.value }) : selected.value.size ? t("batchCount", { count: selected.value.size }) : t("batch"))}</button></div></div>`);
517
- if (needsPassword.value) _push(`<div class="fl-password"><span>${ssrInterpolate(t("passwordPrompt"))}</span><input${ssrRenderAttr("value", passwordInput.value)} type="password"${ssrRenderAttr("placeholder", t("passwordPlaceholder"))}><button class="fl-btn fl-btn-primary" type="button">${ssrInterpolate(t("open"))}</button></div>`);
729
+ _push(`<div class="fl-tools"><label class="fl-search"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><path d="m20 20-3.5-3.5"></path></svg><input${ssrRenderAttr("value", keyword.value)} type="text"${ssrRenderAttr("placeholder", t("filter"))}${ssrRenderAttr("aria-label", t("filter"))}></label><button class="fl-icon-btn" type="button"${ssrRenderAttr("aria-label", t("refresh"))}${ssrRenderAttr("title", t("refresh"))}><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7"></path><path d="M21 4v5h-5"></path></svg></button></div>`);
730
+ if (selected.value.size) _push(`<button class="fl-btn fl-btn-primary" type="button"${ssrIncludeBooleanAttr(batchBusy.value) ? " disabled" : ""}><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg> ${ssrInterpolate(batchBusy.value ? t("batching", { percent: batchProgress.value }) : t("batch"))}</button>`);
518
731
  else _push(`<!---->`);
519
- _push(`<div class="fl-row fl-head"><span class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(allChecked.value) ? " checked" : ""}${ssrRenderAttr("indeterminate", someChecked.value)}></span><span class="fl-cell fl-cell-name">${ssrInterpolate(t("columnName"))}<span class="fl-sort">${ssrInterpolate(sortKey.value === "name" ? sortAsc.value ? "▲" : "▼" : "")}</span></span><span class="fl-cell fl-cell-size">${ssrInterpolate(t("columnSize"))}<span class="fl-sort">${ssrInterpolate(sortKey.value === "size" ? sortAsc.value ? "▲" : "▼" : "")}</span></span><span class="fl-cell fl-cell-date">${ssrInterpolate(t("columnDate"))}<span class="fl-sort">${ssrInterpolate(sortKey.value === "modified" ? sortAsc.value ? "▲" : "▼" : "")}</span></span><span class="fl-cell fl-cell-op"></span></div>`);
732
+ _push(`</div>`);
733
+ if (needsPassword.value) _push(`<div class="fl-password"><span>${ssrInterpolate(t("passwordPrompt"))}</span><input${ssrRenderAttr("value", passwordInput.value)} type="password"${ssrRenderAttr("placeholder", t("passwordPlaceholder"))}${ssrRenderAttr("aria-label", t("passwordPlaceholder"))}><button class="fl-btn fl-btn-primary" type="button">${ssrInterpolate(t("open"))}</button></div>`);
734
+ else _push(`<!---->`);
735
+ _push(`<div class="fl-row fl-head"><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrRenderAttr("aria-label", t("selectAll"))}${ssrIncludeBooleanAttr(allChecked.value) ? " checked" : ""}${ssrRenderAttr("indeterminate", someChecked.value)}></label><span class="fl-cell fl-cell-name"><button class="fl-sort-btn" type="button"${ssrRenderAttr("aria-sort", sortKey.value === "name" ? sortAsc.value ? "ascending" : "descending" : void 0)}>${ssrInterpolate(t("columnName"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "name" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-size"><button class="fl-sort-btn" type="button">${ssrInterpolate(t("columnSize"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "size" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-date"><button class="fl-sort-btn" type="button">${ssrInterpolate(t("columnDate"))}<span class="fl-sort" aria-hidden="true">${ssrInterpolate(sortKey.value === "modified" ? sortAsc.value ? "▲" : "▼" : "")}</span></button></span><span class="fl-cell fl-cell-op"><label class="fl-sort-select"><span>${ssrInterpolate(t("sortBy"))}</span><span class="fl-sort-current" aria-hidden="true">${ssrInterpolate(sortOptions.value.find((option) => option.value === sortValue.value)?.label)}</span><select${ssrRenderAttr("aria-label", t("sortBy"))}><!--[-->`);
736
+ ssrRenderList(sortOptions.value, (option) => {
737
+ _push(`<option${ssrRenderAttr("value", option.value)}${ssrIncludeBooleanAttr(Array.isArray(sortValue.value) ? ssrLooseContain(sortValue.value, option.value) : ssrLooseEqual(sortValue.value, option.value)) ? " selected" : ""}>${ssrInterpolate(option.label)}</option>`);
738
+ });
739
+ _push(`<!--]--></select></label></span></div>`);
520
740
  if (loadingRoot.value) _push(`<div class="fl-status">${ssrInterpolate(t("loading"))}</div>`);
521
741
  else if (errorMsg.value) {
522
742
  _push(`<div class="fl-status fl-error">${ssrInterpolate(t("loadFailed", { message: errorMsg.value }))} `);
@@ -532,30 +752,39 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
532
752
  _push(`<div class="${ssrRenderClass([{
533
753
  "fl-row-dir": node.is_dir,
534
754
  "fl-row-root": node.isRoot,
535
- "fl-row-selected": selected.value.has(node.path)
536
- }, "fl-row"])}"><span class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(isChecked(node)) ? " checked" : ""}${ssrRenderAttr("indeterminate", isIndeterminate(node))}></span><span class="fl-cell fl-cell-name"${ssrRenderAttr("title", node.isRoot ? node.path : void 0)} style="${ssrRenderStyle({ paddingLeft: `${node.depth * 22}px` })}">`);
537
- if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-arrow"])}">▸</button>`);
538
- else _push(`<span class="fl-arrow-placeholder"></span>`);
539
- _push(`<span class="${ssrRenderClass([{ "fl-name-link": node.is_dir }, "fl-name"])}">`);
540
- if (node.is_dir) _push(`<span class="fl-folder-icon">📁</span>`);
541
- else _push(`<span class="fl-ext-badge">${ssrInterpolate(unref(extOf)(node.name) || "FILE")}</span>`);
542
- _push(` ${ssrInterpolate(node.name)} `);
543
- if (node.loading) _push(`<span class="fl-loading">…</span>`);
755
+ "fl-row-selected": !node.is_dir && selected.value.has(node.path)
756
+ }, "fl-row"])}" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrRenderAttr("aria-label", t("selectItem", { name: node.name }))}${ssrIncludeBooleanAttr(isChecked(node)) ? " checked" : ""}${ssrRenderAttr("indeterminate", isIndeterminate(node))}></label><span class="fl-cell fl-cell-name"${ssrRenderAttr("title", node.isRoot ? node.path : void 0)}><span class="fl-indent" aria-hidden="true"></span>`);
757
+ if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-arrow"])}"${ssrRenderAttr("aria-label", t(node.expanded ? "collapse" : "expand", { name: node.name }))}${ssrRenderAttr("aria-expanded", node.expanded)}><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg></button>`);
758
+ else _push(`<span class="fl-arrow-placeholder" aria-hidden="true"></span>`);
759
+ if (node.is_dir) _push(`<svg class="fl-folder-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path></svg>`);
760
+ else _push(`<span class="fl-type"${ssrRenderAttr("data-kind", unref(fileKind)(node.name))} aria-hidden="true">${ssrInterpolate(unref(extOf)(node.name) || "FILE")}</span>`);
761
+ _push(`<span class="fl-label"><span class="${ssrRenderClass([{ "fl-name-dir": node.is_dir }, "fl-name"])}">${ssrInterpolate(node.name)}</span><span class="fl-meta">`);
762
+ if (node.is_dir) {
763
+ _push(`<!--[-->`);
764
+ if (node.loaded) _push(`<!--[-->${ssrInterpolate(t("folderItems", { count: folderFiles(node).length }))} · ${ssrInterpolate(unref(formatSize)(unref(totalSize)(folderFiles(node))))}<!--]-->`);
765
+ else _push(`<!---->`);
766
+ _push(`<!--]-->`);
767
+ } else _push(`<!--[-->${ssrInterpolate(unref(formatSize)(node.size))} · ${ssrInterpolate(unref(formatDate)(node.modified))}<!--]-->`);
768
+ _push(`</span></span>`);
769
+ if (node.is_dir && node.loaded) _push(`<span class="fl-count">${ssrInterpolate(t("folderItems", { count: folderFiles(node).length }))}</span>`);
770
+ else _push(`<!---->`);
771
+ if (node.loading) _push(`<span class="fl-loading" aria-hidden="true">…</span>`);
544
772
  else _push(`<!---->`);
545
- _push(`</span></span><span class="fl-cell fl-cell-size">${ssrInterpolate(node.is_dir ? "-" : unref(formatSize)(node.size))}</span><span class="fl-cell fl-cell-date">${ssrInterpolate(unref(formatDate)(node.modified))}</span><span class="fl-cell fl-cell-op">`);
546
- if (!node.is_dir) {
773
+ _push(`</span><span class="fl-cell fl-cell-size">${ssrInterpolate(node.is_dir ? "—" : unref(formatSize)(node.size))}</span><span class="fl-cell fl-cell-date">${ssrInterpolate(unref(formatDate)(node.modified))}</span><span class="fl-cell fl-cell-op">`);
774
+ if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-action fl-arrow-end"])}"${ssrRenderAttr("aria-label", t(node.expanded ? "collapse" : "expand", { name: node.name }))}${ssrRenderAttr("aria-expanded", node.expanded)}><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"></path></svg></button>`);
775
+ else {
547
776
  _push(`<!--[-->`);
548
- if (canPreview(node)) _push(`<button class="fl-download" type="button"${ssrRenderAttr("title", t("preview"))}><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg></button>`);
777
+ if (canPreview(node)) _push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("preview")} ${node.name}`)}${ssrRenderAttr("title", t("preview"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg></button>`);
549
778
  else _push(`<!---->`);
550
- _push(`<button class="fl-download" type="button"${ssrRenderAttr("title", t("download"))}><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg></button><!--]-->`);
551
- } else _push(`<!---->`);
779
+ _push(`<button class="fl-action fl-download" type="button"${ssrRenderAttr("aria-label", `${t("download")} ${node.name}`)}${ssrRenderAttr("title", t("download"))}><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg></button><!--]-->`);
780
+ }
552
781
  _push(`</span></div>`);
553
782
  });
554
- _push(`<!--]--><div class="fl-foot"><span>${ssrInterpolate(t("loaded", { count: allFiles.value.length }))} · ${ssrInterpolate(t("selected", { count: selected.value.size }))}</span>`);
555
- if (unloadedFolders.value) _push(`<span> · ${ssrInterpolate(t("unloaded", { count: unloadedFolders.value }))}</span>`);
783
+ _push(`<!--]--><div class="fl-foot"><span>${ssrInterpolate(t("loaded", { count: allFiles.value.length }))} · ${ssrInterpolate(t("totalSize", { size: unref(formatSize)(loadedBytes.value) }))}</span>`);
784
+ if (unloadedFolders.value) _push(`<span class="fl-foot-hint">${ssrInterpolate(t("unloaded", { count: unloadedFolders.value }))}</span>`);
556
785
  else _push(`<!---->`);
557
786
  _push(`</div>`);
558
- if (toast.value) _push(`<div class="fl-toast">${ssrInterpolate(toast.value)}</div>`);
787
+ if (toast.value) _push(`<div class="fl-toast" role="status">${ssrInterpolate(toast.value)}</div>`);
559
788
  else _push(`<!---->`);
560
789
  _push(`</div>`);
561
790
  };
@@ -588,4 +817,4 @@ function enhanceAppWithFileList({ app }) {
588
817
  if (!app._context.components.FileList) app.component("FileList", _sfc_main);
589
818
  if (!app._context.components.VPFileList) app.component("VPFileList", _sfc_main);
590
819
  }
591
- export { _sfc_main as FileList, _sfc_main as VPFileList, compareNodes, encodePath, enhanceAppWithFileList, extOf, fileListLocales, formatDate, formatLabel, formatSize, joinPath, resolveLabels };
820
+ export { _sfc_main as FileList, _sfc_main as VPFileList, compareNodes, encodePath, enhanceAppWithFileList, extOf, fileKind, fileListLocales, formatDate, formatLabel, formatSize, joinPath, resolveLabels, totalSize, uniqueEntryName, zipEntryName, zipSegment };