@brftech/filex-core 0.41.4 → 0.42.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.
Files changed (33) hide show
  1. package/dist/{ArchiveViewer-oqaEyqcB.js → ArchiveViewer-CLNevfPE.js} +2 -2
  2. package/dist/{ArchiveViewer-oqaEyqcB.js.map → ArchiveViewer-CLNevfPE.js.map} +1 -1
  3. package/dist/{CsvViewer-BfT-4WpB.js → CsvViewer-K8XA3z7N.js} +2 -2
  4. package/dist/{CsvViewer-BfT-4WpB.js.map → CsvViewer-K8XA3z7N.js.map} +1 -1
  5. package/dist/{DrawioViewer-Cu2ZquNO.js → DrawioViewer-BuhZOAx6.js} +2 -2
  6. package/dist/{DrawioViewer-Cu2ZquNO.js.map → DrawioViewer-BuhZOAx6.js.map} +1 -1
  7. package/dist/{EpubViewer-yfAfnrYF.js → EpubViewer-DS3mb3k8.js} +2 -2
  8. package/dist/{EpubViewer-yfAfnrYF.js.map → EpubViewer-DS3mb3k8.js.map} +1 -1
  9. package/dist/{IpynbViewer-DLSOs3yU.js → IpynbViewer-BTk7p4Dn.js} +2 -2
  10. package/dist/{IpynbViewer-DLSOs3yU.js.map → IpynbViewer-BTk7p4Dn.js.map} +1 -1
  11. package/dist/{MermaidViewer-D2AsT14x.js → MermaidViewer-DqucH_io.js} +2 -2
  12. package/dist/{MermaidViewer-D2AsT14x.js.map → MermaidViewer-DqucH_io.js.map} +1 -1
  13. package/dist/{PsdViewer-DOza4QJt.js → PsdViewer-CwiVENmq.js} +2 -2
  14. package/dist/{PsdViewer-DOza4QJt.js.map → PsdViewer-CwiVENmq.js.map} +1 -1
  15. package/dist/{TiffViewer-24MCOn1t.js → TiffViewer-CpewSVmQ.js} +2 -2
  16. package/dist/{TiffViewer-24MCOn1t.js.map → TiffViewer-CpewSVmQ.js.map} +1 -1
  17. package/dist/{Viewer3D-BVjtbhhc.js → Viewer3D-CIrOn63I.js} +2 -2
  18. package/dist/{Viewer3D-BVjtbhhc.js.map → Viewer3D-CIrOn63I.js.map} +1 -1
  19. package/dist/filex-core.js +1 -1
  20. package/dist/filex-core.umd.cjs +46 -46
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/{index-B8PLGR-X.js → index-Dx0IbBcI.js} +8607 -8579
  23. package/dist/index-Dx0IbBcI.js.map +1 -0
  24. package/dist/index.d.ts +59 -4
  25. package/package.json +1 -1
  26. package/src/FileExplorer.vue +32 -0
  27. package/src/components/FilePane.vue +22 -7
  28. package/src/components/HomeView.vue +6 -3
  29. package/src/components/SideNav.vue +22 -0
  30. package/src/locales/en.ts +1 -0
  31. package/src/locales/tr.ts +1 -0
  32. package/src/types/ExplorerConfig.ts +32 -0
  33. package/dist/index-B8PLGR-X.js.map +0 -1
package/dist/index.d.ts CHANGED
@@ -484,6 +484,7 @@ readonly authHeaders?: (() => Record<string, string> | Promise<Record<string, st
484
484
  readonly authCredentials?: RequestCredentials | undefined;
485
485
  readonly e2eActive?: boolean | undefined;
486
486
  readonly bodyOverride?: boolean | undefined;
487
+ readonly openTrigger?: "single" | "double" | undefined;
487
488
  } & VNodeProps & AllowedComponentProps & ComponentCustomProps;
488
489
  $attrs: Attrs;
489
490
  $refs: {
@@ -548,6 +549,7 @@ authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
548
549
  authCredentials?: RequestCredentials;
549
550
  e2eActive?: boolean;
550
551
  bodyOverride?: boolean;
552
+ openTrigger?: "single" | "double";
551
553
  }> & Readonly<{
552
554
  [x: `on${Capitalize<any>}`]: ((...args: any) => any) | undefined;
553
555
  }>, {
@@ -566,6 +568,7 @@ trashVisible: boolean;
566
568
  showCrumbs: boolean;
567
569
  showFilterBar: boolean;
568
570
  showViewSwitcher: boolean;
571
+ openTrigger: "single" | "double";
569
572
  }, {}, string, {}, GlobalComponents, GlobalDirectives, string, ComponentProvideOptions> & {
570
573
  beforeCreate?: (() => void) | (() => void)[];
571
574
  created?: (() => void) | (() => void)[];
@@ -655,6 +658,7 @@ trashVisible: boolean;
655
658
  showCrumbs: boolean;
656
659
  showFilterBar: boolean;
657
660
  showViewSwitcher: boolean;
661
+ openTrigger: "single" | "double";
658
662
  }> & Omit<Readonly<{
659
663
  paneId: PaneId;
660
664
  api: FileApi;
@@ -706,9 +710,10 @@ authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
706
710
  authCredentials?: RequestCredentials;
707
711
  e2eActive?: boolean;
708
712
  bodyOverride?: boolean;
713
+ openTrigger?: "single" | "double";
709
714
  }> & Readonly<{
710
715
  [x: `on${Capitalize<any>}`]: ((...args: any) => any) | undefined;
711
- }>, "reload" | "dirPerm" | "goUp" | "getPath" | "visibleNodes" | "loadFolder" | "rowCount" | "rowPaths" | ("trashVisible" | "showCrumbs" | "showFilterBar" | "showViewSwitcher")> & {
716
+ }>, "reload" | "dirPerm" | "goUp" | "getPath" | "visibleNodes" | "loadFolder" | "rowCount" | "rowPaths" | ("trashVisible" | "showCrumbs" | "showFilterBar" | "showViewSwitcher" | "openTrigger")> & {
712
717
  reload: () => Promise<void>;
713
718
  goUp: () => void;
714
719
  getPath: () => string;
@@ -780,6 +785,7 @@ readonly authHeaders?: (() => Record<string, string> | Promise<Record<string, st
780
785
  readonly authCredentials?: RequestCredentials | undefined;
781
786
  readonly e2eActive?: boolean | undefined;
782
787
  readonly bodyOverride?: boolean | undefined;
788
+ readonly openTrigger?: "single" | "double" | undefined;
783
789
  } & VNodeProps & AllowedComponentProps & ComponentCustomProps;
784
790
  $attrs: Attrs;
785
791
  $refs: {
@@ -844,6 +850,7 @@ authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
844
850
  authCredentials?: RequestCredentials;
845
851
  e2eActive?: boolean;
846
852
  bodyOverride?: boolean;
853
+ openTrigger?: "single" | "double";
847
854
  }> & Readonly<{
848
855
  [x: `on${Capitalize<any>}`]: ((...args: any) => any) | undefined;
849
856
  }>, {
@@ -862,6 +869,7 @@ trashVisible: boolean;
862
869
  showCrumbs: boolean;
863
870
  showFilterBar: boolean;
864
871
  showViewSwitcher: boolean;
872
+ openTrigger: "single" | "double";
865
873
  }, {}, string, {}, GlobalComponents, GlobalDirectives, string, ComponentProvideOptions> & {
866
874
  beforeCreate?: (() => void) | (() => void)[];
867
875
  created?: (() => void) | (() => void)[];
@@ -951,6 +959,7 @@ trashVisible: boolean;
951
959
  showCrumbs: boolean;
952
960
  showFilterBar: boolean;
953
961
  showViewSwitcher: boolean;
962
+ openTrigger: "single" | "double";
954
963
  }> & Omit<Readonly<{
955
964
  paneId: PaneId;
956
965
  api: FileApi;
@@ -1002,9 +1011,10 @@ authHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
1002
1011
  authCredentials?: RequestCredentials;
1003
1012
  e2eActive?: boolean;
1004
1013
  bodyOverride?: boolean;
1014
+ openTrigger?: "single" | "double";
1005
1015
  }> & Readonly<{
1006
1016
  [x: `on${Capitalize<any>}`]: ((...args: any) => any) | undefined;
1007
- }>, "reload" | "dirPerm" | "goUp" | "getPath" | "visibleNodes" | "loadFolder" | "rowCount" | "rowPaths" | ("trashVisible" | "showCrumbs" | "showFilterBar" | "showViewSwitcher")> & {
1017
+ }>, "reload" | "dirPerm" | "goUp" | "getPath" | "visibleNodes" | "loadFolder" | "rowCount" | "rowPaths" | ("trashVisible" | "showCrumbs" | "showFilterBar" | "showViewSwitcher" | "openTrigger")> & {
1008
1018
  reload: () => Promise<void>;
1009
1019
  goUp: () => void;
1010
1020
  getPath: () => string;
@@ -1081,6 +1091,7 @@ trashVisible: boolean;
1081
1091
  showCrumbs: boolean;
1082
1092
  showFilterBar: boolean;
1083
1093
  showViewSwitcher: boolean;
1094
+ openTrigger: "single" | "double";
1084
1095
  }, {}, {}, {}, string, ComponentProvideOptions, false, {}, HTMLElement>;
1085
1096
 
1086
1097
  declare type __VLS_Props = {
@@ -1284,6 +1295,10 @@ declare type __VLS_Props_15 = {
1284
1295
  * encrypted lock screen) — those are WINDOW states with no listing
1285
1296
  * behind them, so the pane's own state chain must not also run. */
1286
1297
  bodyOverride?: boolean;
1298
+ /** Mouse open gesture — 'double' (single selects, double opens) or
1299
+ * 'single' (first click opens). Touch always opens on tap; the checkbox
1300
+ * always selects. See onViewClick. */
1301
+ openTrigger?: 'single' | 'double';
1287
1302
  };
1288
1303
 
1289
1304
  declare type __VLS_Props_16 = {
@@ -1979,6 +1994,7 @@ declare function __VLS_template(): {
1979
1994
  readonly authCredentials?: RequestCredentials | undefined;
1980
1995
  readonly e2eActive?: boolean | undefined;
1981
1996
  readonly bodyOverride?: boolean | undefined;
1997
+ readonly openTrigger?: "single" | "double" | undefined;
1982
1998
  } & VNodeProps & AllowedComponentProps & ComponentCustomProps;
1983
1999
  $attrs: Attrs;
1984
2000
  $refs: {
@@ -2043,6 +2059,7 @@ declare function __VLS_template(): {
2043
2059
  authCredentials?: RequestCredentials;
2044
2060
  e2eActive?: boolean;
2045
2061
  bodyOverride?: boolean;
2062
+ openTrigger?: "single" | "double";
2046
2063
  }> & Readonly<{
2047
2064
  [x: `on${Capitalize<any>}`]: ((...args: any) => any) | undefined;
2048
2065
  }>, {
@@ -2061,6 +2078,7 @@ declare function __VLS_template(): {
2061
2078
  showCrumbs: boolean;
2062
2079
  showFilterBar: boolean;
2063
2080
  showViewSwitcher: boolean;
2081
+ openTrigger: "single" | "double";
2064
2082
  }, {}, string, {}, GlobalComponents, GlobalDirectives, string, ComponentProvideOptions> & {
2065
2083
  beforeCreate?: (() => void) | (() => void)[];
2066
2084
  created?: (() => void) | (() => void)[];
@@ -2150,6 +2168,7 @@ declare function __VLS_template(): {
2150
2168
  showCrumbs: boolean;
2151
2169
  showFilterBar: boolean;
2152
2170
  showViewSwitcher: boolean;
2171
+ openTrigger: "single" | "double";
2153
2172
  }> & Omit<Readonly<{
2154
2173
  paneId: PaneId;
2155
2174
  api: FileApi;
@@ -2201,9 +2220,10 @@ declare function __VLS_template(): {
2201
2220
  authCredentials?: RequestCredentials;
2202
2221
  e2eActive?: boolean;
2203
2222
  bodyOverride?: boolean;
2223
+ openTrigger?: "single" | "double";
2204
2224
  }> & Readonly<{
2205
2225
  [x: `on${Capitalize<any>}`]: ((...args: any) => any) | undefined;
2206
- }>, "reload" | "dirPerm" | "goUp" | "getPath" | "visibleNodes" | "loadFolder" | "rowCount" | "rowPaths" | ("trashVisible" | "showCrumbs" | "showFilterBar" | "showViewSwitcher")> & {
2226
+ }>, "reload" | "dirPerm" | "goUp" | "getPath" | "visibleNodes" | "loadFolder" | "rowCount" | "rowPaths" | ("trashVisible" | "showCrumbs" | "showFilterBar" | "showViewSwitcher" | "openTrigger")> & {
2207
2227
  reload: () => Promise<void>;
2208
2228
  goUp: () => void;
2209
2229
  getPath: () => string;
@@ -2275,6 +2295,7 @@ declare function __VLS_template(): {
2275
2295
  readonly authCredentials?: RequestCredentials | undefined;
2276
2296
  readonly e2eActive?: boolean | undefined;
2277
2297
  readonly bodyOverride?: boolean | undefined;
2298
+ readonly openTrigger?: "single" | "double" | undefined;
2278
2299
  } & VNodeProps & AllowedComponentProps & ComponentCustomProps;
2279
2300
  $attrs: Attrs;
2280
2301
  $refs: {
@@ -2339,6 +2360,7 @@ declare function __VLS_template(): {
2339
2360
  authCredentials?: RequestCredentials;
2340
2361
  e2eActive?: boolean;
2341
2362
  bodyOverride?: boolean;
2363
+ openTrigger?: "single" | "double";
2342
2364
  }> & Readonly<{
2343
2365
  [x: `on${Capitalize<any>}`]: ((...args: any) => any) | undefined;
2344
2366
  }>, {
@@ -2357,6 +2379,7 @@ declare function __VLS_template(): {
2357
2379
  showCrumbs: boolean;
2358
2380
  showFilterBar: boolean;
2359
2381
  showViewSwitcher: boolean;
2382
+ openTrigger: "single" | "double";
2360
2383
  }, {}, string, {}, GlobalComponents, GlobalDirectives, string, ComponentProvideOptions> & {
2361
2384
  beforeCreate?: (() => void) | (() => void)[];
2362
2385
  created?: (() => void) | (() => void)[];
@@ -2446,6 +2469,7 @@ declare function __VLS_template(): {
2446
2469
  showCrumbs: boolean;
2447
2470
  showFilterBar: boolean;
2448
2471
  showViewSwitcher: boolean;
2472
+ openTrigger: "single" | "double";
2449
2473
  }> & Omit<Readonly<{
2450
2474
  paneId: PaneId;
2451
2475
  api: FileApi;
@@ -2497,9 +2521,10 @@ declare function __VLS_template(): {
2497
2521
  authCredentials?: RequestCredentials;
2498
2522
  e2eActive?: boolean;
2499
2523
  bodyOverride?: boolean;
2524
+ openTrigger?: "single" | "double";
2500
2525
  }> & Readonly<{
2501
2526
  [x: `on${Capitalize<any>}`]: ((...args: any) => any) | undefined;
2502
- }>, "reload" | "dirPerm" | "goUp" | "getPath" | "visibleNodes" | "loadFolder" | "rowCount" | "rowPaths" | ("trashVisible" | "showCrumbs" | "showFilterBar" | "showViewSwitcher")> & {
2527
+ }>, "reload" | "dirPerm" | "goUp" | "getPath" | "visibleNodes" | "loadFolder" | "rowCount" | "rowPaths" | ("trashVisible" | "showCrumbs" | "showFilterBar" | "showViewSwitcher" | "openTrigger")> & {
2503
2528
  reload: () => Promise<void>;
2504
2529
  goUp: () => void;
2505
2530
  getPath: () => string;
@@ -4188,6 +4213,36 @@ export declare interface ExplorerConfig {
4188
4213
  * Starred row that then disappears. A value here wins over the server's.
4189
4214
  */
4190
4215
  callerKind?: 'user' | 'app';
4216
+ /**
4217
+ * How a mouse opens an item.
4218
+ *
4219
+ * 'double' (default) — a single click SELECTS, a double click OPENS
4220
+ * (classic desktop file-manager). This is a
4221
+ * per-viewer preference the host may expose as a
4222
+ * setting.
4223
+ * 'single' — a single click OPENS (the item is opened on the
4224
+ * first click; the checkbox is still the one click
4225
+ * that selects).
4226
+ *
4227
+ * ⚠ TOUCH is not governed by this: a finger tap always opens (the mobile
4228
+ * convention, and there is no hover-to-select on a touchscreen). The
4229
+ * checkbox always selects, on every device and in either mode. See
4230
+ * FilePane.onViewClick + composables/useRowTouch.
4231
+ */
4232
+ openTrigger?: 'single' | 'double';
4233
+ /**
4234
+ * Hand the OPEN of a file to the host instead of opening the in-page
4235
+ * preview/editor overlay. When true, opening a file emits `file-opened`
4236
+ * (path + basename) and the explorer does NOT mount its own PreviewModal —
4237
+ * the host decides what to do (the desktop app opens the file in its own
4238
+ * window per document). Directories still navigate inline, and Space
4239
+ * quick-look still peeks in-page. Default false (web/embeds preview inline).
4240
+ *
4241
+ * ⚠ Ignored inside an unlocked E2E-encrypted folder: the host window would
4242
+ * fetch raw server bytes (ciphertext), so those keep the in-page decrypted
4243
+ * preview.
4244
+ */
4245
+ openInHost?: boolean;
4191
4246
  /** Default view. */
4192
4247
  viewMode?: 'list' | 'grid';
4193
4248
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brftech/filex-core",
3
- "version": "0.41.4",
3
+ "version": "0.42.1",
4
4
  "description": "filex core — Vue 3 source of truth for the filex file manager (FileExplorer + ConnectionsPanel SFCs, composables, types)",
5
5
  "type": "module",
6
6
  "main": "./dist/filex-core.umd.cjs",
@@ -332,6 +332,10 @@ const files = ref<FileNode[]>([]);
332
332
  // RBAC effective level for the current directory ('' = ACL not enforced on
333
333
  // this storage → no gating). Drives which write/manage actions are offered.
334
334
  const dirPerm = ref<string>('');
335
+ /** The listed folder sits on a read-only storage (`read_only` on the index
336
+ * response). Folded into permCanEdit so every write affordance — toolbar,
337
+ * sidebar menu, context menu, drop zone, editor mode — reads the same fact. */
338
+ const dirReadOnly = ref(false);
335
339
  // The dead deep-link state: set to the requested path when a listing came
336
340
  // back 404 (folder doesn't exist) or 403 (RBAC-hidden — rendered identically
337
341
  // on purpose so a denied folder doesn't reveal that it exists). '' = none.
@@ -2034,6 +2038,14 @@ function onInspectorManage(n: FileNode) {
2034
2038
  // (the pre-RBAC default). Otherwise 'editor'/'owner' may write; only 'owner'
2035
2039
  // manages permissions. Enforcement is server-side; this just shapes the menu.
2036
2040
  function permCanEdit(p: string | undefined): boolean {
2041
+ // A read-only storage refuses every write on the server (403 "storage is
2042
+ // read-only") whatever level the ACL grants — an owner of a read-only mount
2043
+ // is an owner who cannot write. Say so here rather than after the click:
2044
+ // until this line the toolbar offered New folder / Upload and the sidebar's
2045
+ // "+ New" menu on a read-only mount, and the user found out from the error
2046
+ // (issue #30). The level itself is left alone: 'owner' still opens the
2047
+ // permissions panel, sharing a read-only file is still allowed.
2048
+ if (dirReadOnly.value) return false;
2037
2049
  // undefined = ACL not enforced (dev / unwired) → full access. In production
2038
2050
  // the backend always sends a level; 'none'/'viewer' cannot write, only
2039
2051
  // 'editor'/'owner' can.
@@ -2350,6 +2362,7 @@ async function load(path?: string) {
2350
2362
  adapter.value = resp.adapter;
2351
2363
  dirname.value = resp.dirname;
2352
2364
  dirPerm.value = (resp.perm as string) || '';
2365
+ dirReadOnly.value = resp.read_only === true;
2353
2366
  /* wiring:e2 — backend tells us when this dir sits inside an encrypted
2354
2367
  subtree; '' resets on every plain folder. Drives the lock screen. */
2355
2368
  e2eRoot.value = typeof resp.e2e_root === 'string' ? resp.e2e_root : '';
@@ -3090,6 +3103,16 @@ function openNode(n: FileNode) {
3090
3103
  }
3091
3104
  if (e2eLocked.value && n.type === 'file') return;
3092
3105
  /* /wiring:e2 */
3106
+ // Host-owned open (desktop app): a file opens in the host's OWN window per
3107
+ // document, not in the in-page overlay. Emit and stop — the host listens on
3108
+ // `file-opened` and spawns the window. Directories still navigate inline
3109
+ // (handled above); Space quick-look still peeks in-page. E2E files fell into
3110
+ // the decrypted in-page branch above, so a host window never gets ciphertext.
3111
+ if (props.config.openInHost && n.type === 'file') {
3112
+ emit('file-opened', { path: n.path, basename: n.basename });
3113
+ void markRecent(n);
3114
+ return;
3115
+ }
3093
3116
  // "Aç" / double-click contract: open in a new tab against the
3094
3117
  // standalone editor route, regardless of file type. The editor page
3095
3118
  // picks the right viewer (OnlyOffice for office, Monaco for code/
@@ -3204,6 +3227,13 @@ function openNodeInNewTab(n: FileNode) {
3204
3227
  return;
3205
3228
  }
3206
3229
  /* /wiring:e2 */
3230
+ // Host-owned open (desktop): the context-menu "Aç" opens the host window
3231
+ // too, so Enter/double-click/"Aç" are ONE open path — not a window here and
3232
+ // a system-browser tab there. openNode carries the `file-opened` emit.
3233
+ if (props.config.openInHost) {
3234
+ openNode(n);
3235
+ return;
3236
+ }
3207
3237
  // RBAC: a viewer (no edit on this item) can't use the editable "Aç"
3208
3238
  // surface — drop to the read-only in-page preview instead.
3209
3239
  if (!permCanEdit((n.perm as string) ?? dirPerm.value)) {
@@ -6365,6 +6395,7 @@ function closeRecoveryKey() {
6365
6395
  :order="listingOrder"
6366
6396
  :view-mode="viewMode"
6367
6397
  :view-modes="allowedViewModes"
6398
+ :open-trigger="props.config.openTrigger"
6368
6399
  :show-crumbs="navView !== 'home' /* Home has no address: its cards come
6369
6400
  from every folder in every storage, so a trail would have to name
6370
6401
  one */"
@@ -6843,6 +6874,7 @@ function closeRecoveryKey() {
6843
6874
  :nav-offers-trash="navOffersTrash"
6844
6875
  :view-mode="paneViewMode"
6845
6876
  :view-modes="allowedViewModes"
6877
+ :open-trigger="props.config.openTrigger"
6846
6878
  :clipped="clippedPaths"
6847
6879
  :can-write="canWriteHere"
6848
6880
  :can-paste="!!clipboard.mode"
@@ -217,12 +217,18 @@ const props = withDefaults(
217
217
  * encrypted lock screen) — those are WINDOW states with no listing
218
218
  * behind them, so the pane's own state chain must not also run. */
219
219
  bodyOverride?: boolean;
220
+
221
+ /** Mouse open gesture — 'double' (single selects, double opens) or
222
+ * 'single' (first click opens). Touch always opens on tap; the checkbox
223
+ * always selects. See onViewClick. */
224
+ openTrigger?: 'single' | 'double';
220
225
  }>(),
221
226
  {
222
227
  showCrumbs: true,
223
228
  showFilterBar: true,
224
229
  showViewSwitcher: true,
225
230
  trashVisible: true,
231
+ openTrigger: 'double',
226
232
  },
227
233
  );
228
234
 
@@ -458,22 +464,31 @@ function withinOpenGuard(): boolean {
458
464
 
459
465
  function onViewClick(n: FileNode, mod: ClickMod) {
460
466
  if (withinOpenGuard()) return;
461
- /* issue #26, fourth round the reporter's rule, on every device: "only
462
- * clicking on checkbox selects it, any other click will open". The checkbox
463
- * is the one click that reaches the selection; a click or tap anywhere else
464
- * on the item opens it, Ctrl/Shift held or not, selection or not. See
465
- * composables/useRowTouch. */
467
+ /* The checkbox is the one click that always reaches the selection — issue
468
+ * #26's rule, kept on every device and in either open mode. A shift-tick
469
+ * still extends the range; a plain tick toggles (ctrl: true). */
466
470
  if (mod.check) {
467
471
  emit('activate');
468
472
  emit('click-row', n, { ctrl: true, shift: mod.shift });
469
473
  return;
470
474
  }
471
- openedAt = Date.now();
472
- onRowOpen(n);
475
+ /* A finger tap always OPENS (the mobile convention — there is no
476
+ * hover-to-select on a touchscreen). For a mouse the host's `openTrigger`
477
+ * decides: 'single' opens on this click; 'double' (default) makes a single
478
+ * click SELECT and leaves opening to onViewDbl. issue #26's "any click
479
+ * opens" is `openTrigger: 'single'`. */
480
+ if (mod.touch || props.openTrigger === 'single') {
481
+ openedAt = Date.now();
482
+ onRowOpen(n);
483
+ return;
484
+ }
485
+ emit('activate');
486
+ emit('click-row', n, { ctrl: mod.ctrl, shift: mod.shift });
473
487
  }
474
488
 
475
489
  function onViewDbl(n: FileNode) {
476
490
  if (withinOpenGuard()) return;
491
+ openedAt = Date.now();
477
492
  onRowOpen(n);
478
493
  }
479
494
 
@@ -144,9 +144,12 @@ const storageTile = fileIconTile({ type: 'dir', mime_type: 'inode/storage' });
144
144
  * with the wrong size line says something false.
145
145
  */
146
146
  function storageCaption(s: HomeStorage): string {
147
- return typeof s.usedBytes === 'number' && s.usedBytes >= 0
148
- ? t('drive.storage.used_unlimited', { used: formatSize(s.usedBytes) })
149
- : t('conn.guide.storage');
147
+ const base =
148
+ typeof s.usedBytes === 'number' && s.usedBytes >= 0
149
+ ? t('drive.storage.used_unlimited', { used: formatSize(s.usedBytes) })
150
+ : t('conn.guide.storage');
151
+ // A read-only mount says so on its card too (issue #30).
152
+ return s.readOnly ? `${base} · ${t('sidenav.storage.readOnly')}` : base;
150
153
  }
151
154
 
152
155
  /**
@@ -242,6 +242,16 @@ const views = computed(() => {
242
242
 
243
243
  const writable = computed(() => props.canWrite !== false);
244
244
 
245
+ /**
246
+ * The storage the listing is in is mounted read-only. Its "+ New" menu is not
247
+ * disabled but GONE: on a read-only mount there is no folder where any of
248
+ * its rows could ever work, and a disabled menu invites the question a menu
249
+ * cannot answer. The row itself says why — see the tag on it (issue #30).
250
+ */
251
+ const activeReadOnly = computed(
252
+ () => !!props.activeStorage && props.storages.some((s) => s.name === props.activeStorage && s.readOnly === true),
253
+ );
254
+
245
255
  /**
246
256
  * Which row reads as the one you are standing on.
247
257
  *
@@ -426,6 +436,7 @@ const toggleLabel = computed(() => t('sidenav.close'));
426
436
  stand here behind `v-if="!newMenu"` (Upload + New folder) is gone:
427
437
  both verbs are the first two rows of this menu. -->
428
438
  <button
439
+ v-if="!activeReadOnly"
429
440
  ref="newBtnEl"
430
441
  type="button"
431
442
  class="fe-sidenav__new"
@@ -693,6 +704,17 @@ const toggleLabel = computed(() => t('sidenav.close'));
693
704
  :aria-label="t('sidenav.storage.shared')"
694
705
  >{{ t('sidenav.storage.shared') }}</span
695
706
  >
707
+ <!-- A read-only mount says so on the row, where the user looks
708
+ for it. The admin list had an "RO" badge; the people it
709
+ applied to never saw one (issue #30). -->
710
+ <span
711
+ v-if="showLabels && s.readOnly"
712
+ class="fe-sidenav__tag"
713
+ role="img"
714
+ data-testid="sidenav-storage-readonly"
715
+ :aria-label="t('sidenav.storage.readOnly')"
716
+ >{{ t('sidenav.storage.readOnly') }}</span
717
+ >
696
718
  </button>
697
719
  </li>
698
720
  </ul>
package/src/locales/en.ts CHANGED
@@ -1047,6 +1047,7 @@ export const en: Record<string, string> = {
1047
1047
  // "expand" would promise a width change to something that has no width.
1048
1048
  'sidenav.open': 'Open navigation',
1049
1049
  'sidenav.storage.shared': 'Shared',
1050
+ 'sidenav.storage.readOnly': 'Read-only',
1050
1051
  // ⚠ `toolbar.nav` is GONE, and nothing replaced it. It was the one label on
1051
1052
  // the panel's collapse control, first as the noun "Navigation" — which names
1052
1053
  // the panel and never says the button does anything to it, so the owner
package/src/locales/tr.ts CHANGED
@@ -1047,6 +1047,7 @@ export const tr: Record<string, string> = {
1047
1047
  // genişliği büyütmeyi vaat ederdi.
1048
1048
  'sidenav.open': 'Gezintiyi aç',
1049
1049
  'sidenav.storage.shared': 'Paylaşılan',
1050
+ 'sidenav.storage.readOnly': 'Salt okunur',
1050
1051
  // ⚠ `toolbar.nav` KALDIRILDI, yerine bir şey konmadı — bkz. en.ts'teki not.
1051
1052
  // Paneli daraltan tek denetimin etiketiydi; önce "Gezinti" (panelin adı,
1052
1053
  // düğmenin ne yaptığını söylemiyor), sonra "Gezintiyi göster/gizle" (sabit
@@ -454,6 +454,38 @@ export interface ExplorerConfig {
454
454
  callerKind?: 'user' | 'app';
455
455
 
456
456
 
457
+ /**
458
+ * How a mouse opens an item.
459
+ *
460
+ * 'double' (default) — a single click SELECTS, a double click OPENS
461
+ * (classic desktop file-manager). This is a
462
+ * per-viewer preference the host may expose as a
463
+ * setting.
464
+ * 'single' — a single click OPENS (the item is opened on the
465
+ * first click; the checkbox is still the one click
466
+ * that selects).
467
+ *
468
+ * ⚠ TOUCH is not governed by this: a finger tap always opens (the mobile
469
+ * convention, and there is no hover-to-select on a touchscreen). The
470
+ * checkbox always selects, on every device and in either mode. See
471
+ * FilePane.onViewClick + composables/useRowTouch.
472
+ */
473
+ openTrigger?: 'single' | 'double';
474
+
475
+ /**
476
+ * Hand the OPEN of a file to the host instead of opening the in-page
477
+ * preview/editor overlay. When true, opening a file emits `file-opened`
478
+ * (path + basename) and the explorer does NOT mount its own PreviewModal —
479
+ * the host decides what to do (the desktop app opens the file in its own
480
+ * window per document). Directories still navigate inline, and Space
481
+ * quick-look still peeks in-page. Default false (web/embeds preview inline).
482
+ *
483
+ * ⚠ Ignored inside an unlocked E2E-encrypted folder: the host window would
484
+ * fetch raw server bytes (ciphertext), so those keep the in-page decrypted
485
+ * preview.
486
+ */
487
+ openInHost?: boolean;
488
+
457
489
  /** Default view. */
458
490
  viewMode?: 'list' | 'grid';
459
491