vitepress-plugin-filelist 0.7.2 → 0.9.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,8 +6,6 @@ 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. */
10
- batchCount: string;
11
9
  batching: string;
12
10
  passwordPrompt: string;
13
11
  passwordPlaceholder: string;
@@ -27,15 +25,11 @@ interface FileListLabels {
27
25
  download: string;
28
26
  loaded: string;
29
27
  selected: string;
30
- /** @deprecated No longer shown; the footer only shows loaded files and size. */
31
- unloaded: string;
32
28
  folderFailed: string;
33
29
  demoDownload: string;
34
30
  demoPreview: string;
35
31
  demoBatch: string;
36
32
  downloadFailed: string;
37
- /** @deprecated Preview errors are shown in the dialog (`previewUnavailable`). */
38
- previewFailed: string;
39
33
  batchFailed: string;
40
34
  defaultZip: string;
41
35
  /** Text button that clears the selection (wide layout). */
@@ -90,8 +84,6 @@ interface FileListLabels {
90
84
  forgetPassword: string;
91
85
  /** Lock button on a folder that needs a password. `{name}` */
92
86
  unlockFolder: string;
93
- /** @deprecated Network failures now append `corsHint`. */
94
- networkHint: string;
95
87
  /** Deep search: the row at the end of a filtered list. `{count}` where noted */
96
88
  deepHint: string;
97
89
  deepSearch: string;
@@ -136,12 +128,23 @@ interface FileListLabels {
136
128
  /** `{limit}` */
137
129
  zipBlockText: string;
138
130
  gotIt: string;
139
- /** Last row of a long folder. `{count}` */
131
+ /** Last row of a long folder: how many are not shown yet. `{count}` */
140
132
  moreRemaining: string;
141
- showMore: string;
133
+ /** Pinned folder row: `{shown}` of `{total}`. */
134
+ moreShown: string;
135
+ /** “Show all”, short form for narrow lists. */
136
+ showAllShort: string;
137
+ /** Show the next page. `{count}` */
138
+ showNext: string;
139
+ /** Show the next page, short form for narrow lists. `{count}` */
140
+ showNextShort: string;
142
141
  showAll: string;
143
142
  /** Fewer than a page left. `{count}` */
144
143
  showRest: string;
144
+ /** Pinned folder row: back to the first page. */
145
+ showLess: string;
146
+ /** Pinned folder row once everything is shown. `{count}` */
147
+ allShown: string;
145
148
  }
146
149
  interface FileListSource {
147
150
  /** OpenList path, directory or single file. */
@@ -174,7 +177,7 @@ interface FileListProps {
174
177
  */
175
178
  rememberPassword?: boolean;
176
179
  /**
177
- * Rows shown per folder before “Show more”. Defaults to `200`; `0` shows
180
+ * Rows shown per folder before “Show more”. Defaults to `15`; `0` shows
178
181
  * everything. Data is still loaded in full; only rendering is limited.
179
182
  */
180
183
  pageSize?: number;
@@ -203,17 +206,6 @@ interface FileListProps {
203
206
  /** Per-key label overrides on top of the locale preset. */
204
207
  labels?: Partial<FileListLabels>;
205
208
  }
206
- /** Shape of one entry returned by `POST /api/fs/list`. */
207
- interface OpenListEntry {
208
- name: string;
209
- size: number;
210
- is_dir: boolean;
211
- modified: string;
212
- created?: string;
213
- sign?: string;
214
- thumb?: string;
215
- type?: number;
216
- }
217
209
  declare const _default: import("vue").DefineComponent<FileListProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<FileListProps> & Readonly<{}>, {
218
210
  base: string;
219
211
  path: string;
@@ -305,66 +297,6 @@ declare global {
305
297
  export declare const fileListLocales: Record<FileListLocale, FileListLabels>;
306
298
  export declare function formatLabel(template: string, values?: Record<string, string | number>): string;
307
299
  export declare function resolveLabels(locale?: FileListLocale, overrides?: Partial<FileListLabels>): FileListLabels;
308
- interface SortableNode {
309
- name: string;
310
- size: number;
311
- is_dir: boolean;
312
- modified: string;
313
- }
314
- type SortKey = 'name' | 'size' | 'modified';
315
- export declare function joinPath(parent: string, name: string): string;
316
- export declare function encodePath(path: string): string;
317
- /**
318
- * Make one path segment safe as a ZIP entry segment: no separators,
319
- * no `.` / `..` traversal, no empty names.
320
- */
321
- export declare function zipSegment(name: string): string;
322
- /** Join path segments into a ZIP entry name, sanitising each segment. */
323
- export declare function zipEntryName(segments: string[]): string;
324
- /**
325
- * Return `name`, or `name (2)`, `name (3)`... when it is already in `used`.
326
- * The suffix goes before the extension of the last segment. Adds the result to `used`.
327
- */
328
- export declare function uniqueEntryName(name: string, used: Set<string>): string;
329
- /** Human-readable size. `0` reads `0 B`; missing, negative or invalid sizes read `-`. */
330
- export declare function formatSize(size: number): string;
331
- export declare function formatDate(iso: string): string;
332
- /** Upper-case extension for the type badge, e.g. `PDF`. */
333
- export declare function extOf(name: string): string;
334
- type FileKind = 'doc' | 'sheet' | 'slide' | 'pdf' | 'image' | 'media' | 'archive' | 'code' | 'other';
335
- type PreviewKind = 'image' | 'video' | 'audio' | 'text' | 'pdf' | 'none';
336
- /** Rough file family, used to colour the type badge. */
337
- export declare function fileKind(name: string): FileKind;
338
- /**
339
- * How a file is shown in the preview dialog. Office documents and anything
340
- * else the browser cannot render are `none` (info + download).
341
- */
342
- export declare function previewKind(name: string): PreviewKind;
343
- /** Sum of the `size` of the given entries, ignoring missing or invalid sizes. */
344
- export declare function totalSize(items: Array<{
345
- size?: number;
346
- }>): number;
347
- export declare function compareNodes(a: SortableNode, b: SortableNode, key: SortKey, ascending?: boolean, locale?: string): number;
348
- interface IframePreview {
349
- label: string;
350
- scheme: string;
351
- }
352
- /**
353
- * Viewers from OpenList's public `iframe_previews` setting that match a file,
354
- * in the order OpenList lists them. Keys are comma-separated extensions or a
355
- * `/regex/flags` tested against the extension, as in OpenList's frontend.
356
- */
357
- export declare function iframePreviewsFor(name: string, setting: string | undefined): IframePreview[];
358
- /**
359
- * Fill an OpenList preview scheme: `$name`, `$url` (raw URL) and `$durl`
360
- * (signed download URL), each optionally prefixed with `e` (URI-encode) or
361
- * `b` (base64), e.g. `$e_url`, `$b_url`, `$e_durl`.
362
- */
363
- export declare function fillPreviewUrl(scheme: string, args: {
364
- name: string;
365
- rawUrl: string;
366
- downloadUrl: string;
367
- }): string;
368
300
  /**
369
301
  * Register `<FileList>` and `<VPFileList>` globally.
370
302
  *
@@ -382,4 +314,4 @@ export declare function fillPreviewUrl(scheme: string, args: {
382
314
  * ```
383
315
  */
384
316
  export declare function enhanceAppWithFileList({ app }: EnhanceAppContext): void;
385
- export { type FileKind, _default as FileList, _default as VPFileList, type FileListLabels, type FileListLocale, type FileListProps, type FileListSource, type IframePreview, type OpenListEntry, type PreviewKind, type SortKey, type SortableNode };
317
+ export { _default as FileList, _default as VPFileList, type FileListLabels, type FileListLocale, type FileListProps, type FileListSource };
@@ -353,6 +353,56 @@ function createOpenListApi(base, token) {
353
353
  }
354
354
  };
355
355
  }
356
+ // src/client/demoSamples.ts
357
+ const SAMPLE_FOLDER = "/示例文件";
358
+ const SAMPLES = {
359
+ "使用说明.md": {
360
+ type: "text/markdown",
361
+ body: "# 使用说明\n\n这是 **vitepress-plugin-filelist** 演示数据自带的 Markdown 文件。\n\n## 试一试\n\n- 点右上角“渲染”,按 VitePress 正文样式显示\n- 用 ← / → 切换到同一目录里的其它文件\n- 勾选“示例文件”文件夹,点“下载 ZIP”打包\n\n| 文件 | 类型 |\n| --- | --- |\n| 讲义.pdf | PDF |\n| 课程海报.svg | 图片 |\n| 网页示例.html | 网页 |\n\n> 接入 OpenList 后,这里显示的就是网盘里的真实文件。\n\n更多说明见 [文档站](https://nrjycyd.github.io/vitepress-plugin-filelist/)。\n"
362
+ },
363
+ "网页示例.html": {
364
+ type: "text/html",
365
+ body: "<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\">\n<title>网页示例</title>\n<style>\n body { font-family: system-ui, sans-serif; margin: 24px; line-height: 1.6; color: #1f2937; }\n h1 { color: #3451b2; }\n .card { padding: 12px 16px; border: 1px solid #d1d5db; border-radius: 8px; }\n</style>\n</head>\n<body>\n<h1>网页示例</h1>\n<p>切换到“渲染”后,这个网页在沙盒中显示:带样式,但脚本不会运行。</p>\n<div class=\"card\">下面的按钮由脚本控制,在沙盒里点了没有反应。</div>\n<p><button onclick=\"alert('脚本运行了')\">点我</button></p>\n<script>document.body.dataset.ran = 'yes'<\/script>\n</body>\n</html>\n"
366
+ },
367
+ "课程海报.svg": {
368
+ type: "image/svg+xml",
369
+ body: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"800\" height=\"450\" viewBox=\"0 0 800 450\">\n<defs><linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\"><stop offset=\"0\" stop-color=\"#3451b2\"/><stop offset=\"1\" stop-color=\"#5672cd\"/></linearGradient></defs>\n<rect width=\"800\" height=\"450\" fill=\"url(#g)\"/>\n<circle cx=\"660\" cy=\"90\" r=\"120\" fill=\"#ffffff\" opacity=\"0.08\"/>\n<circle cx=\"120\" cy=\"420\" r=\"160\" fill=\"#ffffff\" opacity=\"0.06\"/>\n<text x=\"64\" y=\"190\" font-family=\"system-ui, sans-serif\" font-size=\"56\" font-weight=\"700\" fill=\"#ffffff\">课程海报</text>\n<text x=\"64\" y=\"250\" font-family=\"system-ui, sans-serif\" font-size=\"26\" fill=\"#dfe5ff\">线性代数 · 2026 秋季学期</text>\n<text x=\"64\" y=\"380\" font-family=\"system-ui, sans-serif\" font-size=\"18\" fill=\"#dfe5ff\">vitepress-plugin-filelist 演示图片</text>\n</svg>\n"
370
+ },
371
+ "讲义.pdf": {
372
+ type: "application/pdf",
373
+ body: "%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n5 0 obj\n<< /Length 257 >>\nstream\nBT\n/F1 20 Tf\n72 760 Td\n(FileList sample PDF) Tj\n/F1 12 Tf\n0 -22 Td\n() Tj\n0 -22 Td\n(This small PDF ships with the demo data of) Tj\n0 -22 Td\n(vitepress-plugin-filelist, so preview, download) Tj\n0 -22 Td\n(and ZIP can be tried without an OpenList server.) Tj\nET\nendstream\nendobj\nxref\n0 6\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n0000000115 00000 n \n0000000241 00000 n \n0000000311 00000 n \ntrailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n619\n%%EOF\n"
374
+ },
375
+ "课程表.txt": {
376
+ type: "text/plain",
377
+ body: "课程表(演示文本)\n\n周一 08:00 线性代数 教 301\n周三 10:00 线性代数习题课 教 205\n周五 14:00 答疑 图书馆 2 楼\n\n纯文本文件只读取前 1 MB,按原样显示。\n"
378
+ }
379
+ };
380
+ const SAMPLE_NAMES = Object.keys(SAMPLES);
381
+ /** Size in bytes of a sample, as UTF-8. */
382
+ function sampleSize(name) {
383
+ return new TextEncoder().encode(SAMPLES[name].body).byteLength;
384
+ }
385
+ function nameOf(path) {
386
+ const name = path.slice(6);
387
+ return path.startsWith(`/示例文件/`) && name in SAMPLES ? name : void 0;
388
+ }
389
+ /** Whether a demo path is one of the sample files (which have a real URL). */
390
+ function isSample(path) {
391
+ return nameOf(path) !== void 0;
392
+ }
393
+ const urls = /* @__PURE__ */ new Map();
394
+ /** A blob: URL for a sample file, `''` for any other path or outside a browser. */
395
+ function sampleUrl(path) {
396
+ const name = nameOf(path);
397
+ if (!name || typeof URL.createObjectURL !== "function") return "";
398
+ let url = urls.get(name);
399
+ if (!url) {
400
+ const { type, body } = SAMPLES[name];
401
+ url = URL.createObjectURL(new Blob([body], { type: `${type};charset=utf-8` }));
402
+ urls.set(name, url);
403
+ }
404
+ return url;
405
+ }
356
406
  // src/client/demo.ts
357
407
  /** Built-in sample data, used when no `base` is given. */
358
408
  const TREE = {
@@ -392,8 +442,32 @@ const TREE = {
392
442
  size: 0,
393
443
  is_dir: true,
394
444
  modified: "2026-10-20T18:00:00+08:00"
445
+ },
446
+ {
447
+ name: "示例文件",
448
+ size: 0,
449
+ is_dir: true,
450
+ modified: "2026-11-11T09:00:00+08:00"
451
+ },
452
+ {
453
+ name: "照片(260 张)",
454
+ size: 0,
455
+ is_dir: true,
456
+ modified: "2026-10-01T12:00:00+08:00"
395
457
  }
396
458
  ],
459
+ [SAMPLE_FOLDER]: SAMPLE_NAMES.map((name) => ({
460
+ name,
461
+ size: sampleSize(name),
462
+ is_dir: false,
463
+ modified: "2026-11-11T09:00:00+08:00"
464
+ })),
465
+ "/照片(260 张)": Array.from({ length: 260 }, (_, index) => ({
466
+ name: `IMG_${String(index + 1).padStart(4, "0")}.jpg`,
467
+ size: 2e6 + index * 7919 % 15e5,
468
+ is_dir: false,
469
+ modified: "2026-10-01T12:00:00+08:00"
470
+ })),
397
471
  "/assignments": [{
398
472
  name: "线代作业参考答案",
399
473
  size: 0,
@@ -514,7 +588,7 @@ function guard(path, password) {
514
588
  function parentOf(path) {
515
589
  return path.slice(0, path.lastIndexOf("/")) || "/";
516
590
  }
517
- /** Demo OpenList: same API, no network; files have no URL. */
591
+ /** Demo OpenList: same API, no network; only the sample files have a URL. */
518
592
  function createDemoApi(delay = 220) {
519
593
  return {
520
594
  async list(path, password) {
@@ -533,12 +607,46 @@ function createDemoApi(delay = 220) {
533
607
  title: "",
534
608
  iframePreviews: ""
535
609
  }),
536
- fileUrl: async () => "",
610
+ fileUrl: async (file) => sampleUrl(file.path),
537
611
  directUrl: () => "",
538
- isAttachmentUrl: () => false,
612
+ isAttachmentUrl: (url) => url.startsWith("blob:"),
539
613
  clearCache: () => {}
540
614
  };
541
615
  }
616
+ /** Whether a demo file can really be previewed, downloaded and zipped. */
617
+ const isDemoFile = isSample;
618
+ // src/client/devWarnings.ts
619
+ const SECURITY_DOC = "https://nrjycyd.github.io/vitepress-plugin-filelist/guide/security";
620
+ /** Props a page wrote that end up, readable by anyone, in the built site. */
621
+ function publicSecrets(props) {
622
+ const found = [];
623
+ if (props.token) found.push("token");
624
+ if (props.password || props.sources?.some((source) => source.password)) found.push("password");
625
+ return found;
626
+ }
627
+ /**
628
+ * Development builds only. The site's Vite replaces `import.meta.env` in
629
+ * dependencies too (`DEV` is true under `vitepress dev`, false in a build);
630
+ * without a bundler there is no `env` and this is not a development build.
631
+ */
632
+ function isDevBuild() {
633
+ return import.meta.env?.DEV === true;
634
+ }
635
+ let warned = false;
636
+ /**
637
+ * While the site runs in development, say once per page load that `token` and
638
+ * `password` written in Markdown are public. Production builds stay silent.
639
+ * Returns whether it warned.
640
+ */
641
+ function warnPublicSecrets(props, locale, dev = isDevBuild()) {
642
+ if (warned || !dev) return false;
643
+ const found = publicSecrets(props);
644
+ if (!found.length) return false;
645
+ warned = true;
646
+ const names = found.join(" / ");
647
+ console.warn(locale === "en-US" ? `[vitepress-plugin-filelist] This page sets ${names} on <FileList>. It is copied as is into the built site, where every visitor can read it. Prefer a guest account limited to a base path. ${SECURITY_DOC} (shown in development only)` : `[vitepress-plugin-filelist] 本页的 <FileList> 写了 ${names}:它会原样进入构建后的网页,所有访客都能看到。建议改用限定基础路径的 guest 账户。${SECURITY_DOC}(仅开发时提示)`);
648
+ return true;
649
+ }
542
650
  // src/client/ConfirmDialog.vue
543
651
  const _sfc_main$3 = /*@__PURE__*/ defineComponent({
544
652
  __name: "ConfirmDialog",
@@ -754,6 +862,14 @@ const ICONS = {
754
862
  stroke: 2.2,
755
863
  paths: ["m15 6-6 6 6 6"]
756
864
  },
865
+ down: {
866
+ stroke: 2.4,
867
+ paths: ["m6 9 6 6 6-6"]
868
+ },
869
+ up: {
870
+ stroke: 2.4,
871
+ paths: ["m18 15-6-6-6 6"]
872
+ },
757
873
  zoomIn: {
758
874
  stroke: 2,
759
875
  paths: [
@@ -813,7 +929,6 @@ const fileListLocales = {
813
929
  filter: "筛选文件名",
814
930
  refresh: "刷新",
815
931
  batch: "下载 ZIP",
816
- batchCount: "批量下载({count})",
817
932
  batching: "打包中 {percent}%",
818
933
  passwordPrompt: "该目录受密码保护:",
819
934
  passwordPlaceholder: "输入密码",
@@ -831,13 +946,11 @@ const fileListLocales = {
831
946
  download: "下载",
832
947
  loaded: "{count} 个已加载文件",
833
948
  selected: "已选 {count} 项",
834
- unloaded: "{count} 个目录未展开,未计入全选",
835
949
  folderFailed: "打开文件夹失败:{message}",
836
950
  demoDownload: "演示模式:接入 OpenList 后会下载「{name}」",
837
951
  demoPreview: "演示模式:接入 OpenList 后可预览「{name}」",
838
952
  demoBatch: "演示模式:将打包已勾选的 {count} 个文件",
839
953
  downloadFailed: "下载失败:{message}",
840
- previewFailed: "预览失败:{message}",
841
954
  batchFailed: "打包失败:{message}",
842
955
  defaultZip: "资源",
843
956
  clear: "清除",
@@ -868,7 +981,6 @@ const fileListLocales = {
868
981
  remembered: "已记住",
869
982
  forgetPassword: "忘记",
870
983
  unlockFolder: "输入 {name} 的密码",
871
- networkHint: "可能被跨域拦截:请检查 OpenList 的 cors.allow_origins,或 OpenList-Worker 的 ALLOW_URLS。",
872
984
  deepHint: "只筛选了已加载的内容,",
873
985
  deepSearch: "尝试深度搜索",
874
986
  deepRunning: "正在深度搜索 · 已查找 {count} 个文件夹",
@@ -899,9 +1011,14 @@ const fileListLocales = {
899
1011
  zipBlockText: "当前浏览器一次最多打包 {limit},请分批勾选下载。",
900
1012
  gotIt: "知道了",
901
1013
  moreRemaining: "还有 {count} 项",
902
- showMore: "显示更多",
903
1014
  showAll: "全部显示",
904
- showRest: "显示剩余 {count} 项"
1015
+ showRest: "显示剩余 {count} 项",
1016
+ showLess: "收起",
1017
+ allShown: "已显示全部 {count} 项",
1018
+ moreShown: "已显示 {shown} / {total} 项",
1019
+ showAllShort: "全部",
1020
+ showNext: "再显示 {count} 项",
1021
+ showNextShort: "+{count}"
905
1022
  },
906
1023
  "en-US": {
907
1024
  allFiles: "All files",
@@ -909,7 +1026,6 @@ const fileListLocales = {
909
1026
  filter: "Filter files",
910
1027
  refresh: "Refresh",
911
1028
  batch: "Download ZIP",
912
- batchCount: "Download ZIP ({count})",
913
1029
  batching: "Zipping {percent}%",
914
1030
  passwordPrompt: "This folder is password protected:",
915
1031
  passwordPlaceholder: "Folder password",
@@ -927,13 +1043,11 @@ const fileListLocales = {
927
1043
  download: "Download",
928
1044
  loaded: "{count} loaded file(s)",
929
1045
  selected: "{count} selected",
930
- unloaded: "{count} folder(s) not expanded, excluded from select-all",
931
1046
  folderFailed: "Failed to open folder: {message}",
932
1047
  demoDownload: "Demo mode: \"{name}\" will download once OpenList is connected.",
933
1048
  demoPreview: "Demo mode: \"{name}\" will preview once OpenList is connected.",
934
1049
  demoBatch: "Demo mode: {count} selected file(s) would be zipped.",
935
1050
  downloadFailed: "Download failed: {message}",
936
- previewFailed: "Preview failed: {message}",
937
1051
  batchFailed: "Failed to create ZIP: {message}",
938
1052
  defaultZip: "resources",
939
1053
  clear: "Clear",
@@ -964,7 +1078,6 @@ const fileListLocales = {
964
1078
  remembered: "Remembered",
965
1079
  forgetPassword: "Forget",
966
1080
  unlockFolder: "Enter the password for {name}",
967
- networkHint: "Possibly blocked by CORS: check cors.allow_origins in OpenList, or ALLOW_URLS in OpenList-Worker.",
968
1081
  deepHint: "Only loaded folders are filtered. ",
969
1082
  deepSearch: "Try deep search",
970
1083
  deepRunning: "Searching · {count} folders checked",
@@ -995,9 +1108,14 @@ const fileListLocales = {
995
1108
  zipBlockText: "This browser can zip at most {limit} at once. Select fewer files.",
996
1109
  gotIt: "OK",
997
1110
  moreRemaining: "{count} more",
998
- showMore: "Show more",
999
1111
  showAll: "Show all",
1000
- showRest: "Show the remaining {count}"
1112
+ showRest: "Show the remaining {count}",
1113
+ showLess: "Show less",
1114
+ allShown: "All {count} shown",
1115
+ moreShown: "Showing {shown} of {total}",
1116
+ showAllShort: "All",
1117
+ showNext: "Show {count} more",
1118
+ showNextShort: "+{count}"
1001
1119
  }
1002
1120
  };
1003
1121
  function formatLabel(template, values = {}) {
@@ -1565,7 +1683,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1565
1683
  type: Boolean,
1566
1684
  default: true
1567
1685
  },
1568
- pageSize: { default: 200 },
1686
+ pageSize: { default: 15 },
1569
1687
  deepSearchLimit: { default: 500 },
1570
1688
  deepSearch: { default: "manual" },
1571
1689
  zipWarnSize: {},
@@ -1831,18 +1949,23 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1831
1949
  applySort(key, direction !== "desc");
1832
1950
  }
1833
1951
  });
1834
- /** A “show more” row closing a folder whose children are cut at `pageSize`. */
1952
+ /** The row closing a long folder while its children are cut at `pageSize`. */
1835
1953
  const shown = reactive(/* @__PURE__ */ new Map());
1954
+ function pageSizeOf() {
1955
+ return Math.max(0, Math.floor(props.pageSize ?? 15));
1956
+ }
1836
1957
  function limitOf(key) {
1837
- const size = Math.max(0, Math.floor(props.pageSize ?? 200));
1958
+ const size = pageSizeOf();
1838
1959
  return size ? shown.get(key) ?? size : Number.POSITIVE_INFINITY;
1839
1960
  }
1840
1961
  function isMore(row) {
1841
1962
  return "more" in row;
1842
1963
  }
1843
- const rows = computed(() => {
1964
+ const layout = computed(() => {
1844
1965
  const out = [];
1966
+ const long = [];
1845
1967
  const query = keyword.value.trim().toLowerCase();
1968
+ const size = pageSizeOf();
1846
1969
  const matches = (node) => !query || node.name.toLowerCase().includes(query) || node.is_dir && (node.children.some(matches) || deepMode.value && !!node.lock);
1847
1970
  const walk = (nodes, key, depth) => {
1848
1971
  const list = nodes.filter(matches);
@@ -1857,10 +1980,70 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
1857
1980
  depth,
1858
1981
  remaining: list.length - limit
1859
1982
  });
1983
+ let end = out.length - 1;
1984
+ while (end >= 0 && isMore(out[end])) end -= 1;
1985
+ const last = out[end];
1986
+ if (!query && size > 0 && limit > size && last && !isMore(last)) long.push({
1987
+ key,
1988
+ depth: depth - 1,
1989
+ shown: limit,
1990
+ total: list.length,
1991
+ last: last.path
1992
+ });
1860
1993
  };
1861
1994
  walk(roots.value, props.sources.length ? "" : props.path, 0);
1862
- return out;
1995
+ return {
1996
+ rows: out,
1997
+ long
1998
+ };
1863
1999
  });
2000
+ const rows = computed(() => layout.value.rows);
2001
+ const pinned = ref(null);
2002
+ /** Height of the sticky selection bar above the pinned row (narrow lists only). */
2003
+ const pinOffset = ref(0);
2004
+ function rowElement(path) {
2005
+ return [...rootEl.value?.querySelectorAll("[data-path]") ?? []].find((el) => el.dataset.path === path);
2006
+ }
2007
+ /** The pinned folder's name: its own, or for a single path's top level, the list's title. */
2008
+ const pinnedName = computed(() => {
2009
+ const folder = pinned.value;
2010
+ if (!folder) return "";
2011
+ if (folder.depth < 0) return props.title || folder.key.split("/").filter(Boolean).pop() || "/";
2012
+ const find = (nodes) => {
2013
+ for (const node of nodes) {
2014
+ if (node.path === folder.key) return node;
2015
+ const inner = isWithin(folder.key, node.path) ? find(node.children) : void 0;
2016
+ if (inner) return inner;
2017
+ }
2018
+ };
2019
+ return find(roots.value)?.name ?? "";
2020
+ });
2021
+ /** Pin the innermost long folder whose own row has scrolled away while its files are still on screen. */
2022
+ function updatePin() {
2023
+ const root = rootEl.value;
2024
+ if (!root) return;
2025
+ const bar = root.querySelector(".fl-bar");
2026
+ pinOffset.value = bar && getComputedStyle(bar).position === "sticky" ? bar.offsetHeight : 0;
2027
+ const top = stickyTop.value + pinOffset.value;
2028
+ const rowHeight = root.querySelector(".fl-head")?.offsetHeight ?? 40;
2029
+ let found = null;
2030
+ for (const folder of layout.value.long) {
2031
+ const head = folder.depth < 0 ? root.querySelector(".fl-head") : rowElement(folder.key);
2032
+ const last = rowElement(folder.last);
2033
+ if (!head || !last) continue;
2034
+ if (head.getBoundingClientRect().bottom <= top && last.getBoundingClientRect().bottom > top + rowHeight && (!found || folder.depth > found.depth)) found = folder;
2035
+ }
2036
+ pinned.value = found;
2037
+ }
2038
+ let pinFrame = 0;
2039
+ function schedulePin() {
2040
+ if (pinFrame) return;
2041
+ pinFrame = requestAnimationFrame(() => {
2042
+ pinFrame = 0;
2043
+ updatePin();
2044
+ });
2045
+ }
2046
+ watch(layout, () => void nextTick(schedulePin));
1864
2047
  const visible = computed(() => rows.value.filter((row) => !isMore(row)));
1865
2048
  /** A name split around the filter keyword, for highlighting. */
1866
2049
  function nameParts(name) {
@@ -2047,7 +2230,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2047
2230
  }
2048
2231
  async function download(node) {
2049
2232
  if (node.is_dir) return;
2050
- if (demo.value) {
2233
+ if (demo.value && !isDemoFile(node.path)) {
2051
2234
  showToast(t("demoDownload", { name: node.name }));
2052
2235
  return;
2053
2236
  }
@@ -2151,7 +2334,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2151
2334
  previewViewer.value = "";
2152
2335
  previewError.value = "";
2153
2336
  previewResolving.value = false;
2154
- if (demo.value || !canPreview(node)) return;
2337
+ if (demo.value && !isDemoFile(node.path) || !canPreview(node)) return;
2155
2338
  previewResolving.value = true;
2156
2339
  try {
2157
2340
  const url = await fileUrlOf(node);
@@ -2195,15 +2378,21 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2195
2378
  stickyTop.value = top;
2196
2379
  }
2197
2380
  onMounted(() => {
2381
+ warnPublicSecrets(props, props.locale);
2198
2382
  passwords.sync();
2199
2383
  loadRoot();
2200
2384
  loadSettings();
2201
2385
  measureStickyTop();
2202
2386
  window.addEventListener("resize", measureStickyTop, { passive: true });
2387
+ window.addEventListener("scroll", schedulePin, { passive: true });
2388
+ window.addEventListener("resize", schedulePin, { passive: true });
2203
2389
  });
2204
2390
  onBeforeUnmount(() => {
2205
2391
  zipAbort?.abort();
2206
2392
  window.removeEventListener("resize", measureStickyTop);
2393
+ window.removeEventListener("scroll", schedulePin);
2394
+ window.removeEventListener("resize", schedulePin);
2395
+ cancelAnimationFrame(pinFrame);
2207
2396
  clearTimeout(toastTimer);
2208
2397
  });
2209
2398
  return (_ctx, _push, _parent, _attrs) => {
@@ -2287,7 +2476,30 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2287
2476
  ssrRenderList(sortOptions.value, (option) => {
2288
2477
  _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>`);
2289
2478
  });
2290
- _push(`<!--]--></select></label></span></div>`);
2479
+ _push(`<!--]--></select></label></span></div><!-- While scrolling through a long folder, its row stays at the top with “show less”. --><div class="fl-pin-anchor" style="${ssrRenderStyle({ "--fl-pin-offset": `${pinOffset.value}px` })}">`);
2480
+ if (pinned.value) {
2481
+ _push(`<div class="fl-pin" style="${ssrRenderStyle({ "--fl-depth": Math.max(pinned.value.depth, 0) })}"><span class="fl-cell fl-cell-check"></span><span class="fl-cell fl-cell-name">`);
2482
+ if (pinned.value.depth >= 0) _push(`<span class="fl-indent" aria-hidden="true"></span>`);
2483
+ else _push(`<!---->`);
2484
+ if (pinned.value.depth >= 0) {
2485
+ _push(`<span class="fl-pin-arrow" aria-hidden="true">`);
2486
+ _push(ssrRenderComponent(_sfc_main$2, {
2487
+ name: "next",
2488
+ size: 14
2489
+ }, null, _parent));
2490
+ _push(`</span>`);
2491
+ } else _push(`<!---->`);
2492
+ _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><span class="fl-pin-name">${ssrInterpolate(pinnedName.value)}</span><span class="fl-pin-count">${ssrInterpolate(pinned.value.shown < pinned.value.total ? t("moreShown", {
2493
+ shown: pinned.value.shown,
2494
+ total: pinned.value.total
2495
+ }) : t("allShown", { count: pinned.value.total }))}</span></span><button class="fl-pin-less" type="button">${ssrInterpolate(t("showLess"))}`);
2496
+ _push(ssrRenderComponent(_sfc_main$2, {
2497
+ name: "up",
2498
+ size: 12
2499
+ }, null, _parent));
2500
+ _push(`</button></div>`);
2501
+ } else _push(`<!---->`);
2502
+ _push(`</div>`);
2291
2503
  if (loadingRoot.value) _push(`<div class="fl-status">${ssrInterpolate(t("loading"))}</div>`);
2292
2504
  else if (rootLock.value) {
2293
2505
  _push(`<form class="${ssrRenderClass([{ "fl-unlock-error": rootLock.value.error }, "fl-unlock-root"])}">`);
@@ -2318,10 +2530,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2318
2530
  ssrRenderList(rows.value, (node, index) => {
2319
2531
  _push(`<!--[-->`);
2320
2532
  if (isMore(node)) {
2321
- _push(`<div class="fl-row fl-more" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><span class="fl-cell fl-cell-check"></span><span class="fl-cell fl-more-cell"><span class="fl-indent" aria-hidden="true"></span><span class="fl-more-count">${ssrInterpolate(t("moreRemaining", { count: node.remaining }))}</span>`);
2322
- if (node.remaining > (__props.pageSize || 0)) _push(`<!--[--><button class="fl-more-btn" type="button">${ssrInterpolate(t("showMore"))}<span class="fl-more-page">${ssrInterpolate(` ${t("folderItems", { count: __props.pageSize })}`)}</span></button><button class="fl-more-btn fl-more-all" type="button">${ssrInterpolate(t("showAll"))}</button><!--]-->`);
2323
- else _push(`<button class="fl-more-btn" type="button">${ssrInterpolate(t("showRest", { count: node.remaining }))}</button>`);
2324
- _push(`</span></div>`);
2533
+ _push(`<div class="fl-row fl-more" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><span class="fl-cell fl-cell-check"></span><span class="fl-cell fl-more-cell"><!-- Reads as one more row of the folder: “⋯” in the type column, the count where names start. --><span class="fl-indent" aria-hidden="true"></span><span class="fl-arrow-placeholder" aria-hidden="true"></span><span class="fl-more-tag" aria-hidden="true">⋯</span><span class="fl-more-count">${ssrInterpolate(t("moreRemaining", { count: node.remaining }))}</span><span class="fl-more-actions">`);
2534
+ if (node.remaining > (__props.pageSize || 0)) _push(`<!--[--><button class="fl-more-link" type="button"${ssrRenderAttr("aria-label", t("showNext", { count: __props.pageSize }))}><span class="fl-more-long">${ssrInterpolate(t("showNext", { count: __props.pageSize }))}</span><span class="fl-more-short" aria-hidden="true">${ssrInterpolate(t("showNextShort", { count: __props.pageSize }))}</span></button><button class="fl-more-link fl-more-all" type="button"${ssrRenderAttr("aria-label", t("showAll"))}><span class="fl-more-long">${ssrInterpolate(t("showAll"))}</span><span class="fl-more-short" aria-hidden="true">${ssrInterpolate(t("showAllShort"))}</span></button><!--]-->`);
2535
+ else _push(`<button class="fl-more-link fl-more-all" type="button">${ssrInterpolate(t("showRest", { count: node.remaining }))}</button>`);
2536
+ _push(`</span></span></div>`);
2325
2537
  } else {
2326
2538
  _push(`<div class="${ssrRenderClass([{
2327
2539
  "fl-row-dir": node.is_dir,
@@ -2330,7 +2542,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2330
2542
  "fl-row-locked": !!node.lock,
2331
2543
  "fl-row-prompting": node.lock?.prompting,
2332
2544
  "fl-row-error": node.lock?.prompting && !!node.lock.error
2333
- }, "fl-row"])}" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(!!node.lock || batchBusy.value) ? " disabled" : ""}${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>`);
2545
+ }, "fl-row"])}" style="${ssrRenderStyle({ "--fl-depth": node.depth })}"${ssrRenderAttr("data-path", node.path)}><label class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(!!node.lock || batchBusy.value) ? " disabled" : ""}${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>`);
2334
2546
  if (node.lock) {
2335
2547
  _push(`<button class="fl-arrow fl-lock" type="button"${ssrRenderAttr("aria-label", t("unlockFolder", { name: node.name }))}${ssrRenderAttr("title", t("unlockFolder", { name: node.name }))}>`);
2336
2548
  _push(ssrRenderComponent(_sfc_main$2, {
@@ -2463,7 +2675,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
2463
2675
  viewer: previewViewer.value,
2464
2676
  resolving: previewResolving.value,
2465
2677
  error: previewError.value,
2466
- demo: demo.value,
2678
+ demo: demo.value && !!previewNode.value && !unref(isDemoFile)(previewNode.value.path),
2467
2679
  position: previewPosition.value,
2468
2680
  total: previewPosition.value ? previewList.value.length : 0,
2469
2681
  onClose: closePreview,
@@ -2502,4 +2714,4 @@ function enhanceAppWithFileList({ app }) {
2502
2714
  if (!app._context.components.FileList) app.component("FileList", _sfc_main);
2503
2715
  if (!app._context.components.VPFileList) app.component("VPFileList", _sfc_main);
2504
2716
  }
2505
- export { _sfc_main as FileList, _sfc_main as VPFileList, compareNodes, encodePath, enhanceAppWithFileList, extOf, fileKind, fileListLocales, fillPreviewUrl, formatDate, formatLabel, formatSize, iframePreviewsFor, joinPath, previewKind, resolveLabels, totalSize, uniqueEntryName, zipEntryName, zipSegment };
2717
+ export { _sfc_main as FileList, _sfc_main as VPFileList, enhanceAppWithFileList, fileListLocales, formatLabel, resolveLabels };