@brftech/filex-core 0.7.0 → 0.7.2

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 (35) hide show
  1. package/dist/{ArchiveViewer-w6GDJJt9.js → ArchiveViewer-DAb5peBU.js} +2 -2
  2. package/dist/{ArchiveViewer-w6GDJJt9.js.map → ArchiveViewer-DAb5peBU.js.map} +1 -1
  3. package/dist/{CsvViewer-Ci5-mke8.js → CsvViewer-_bnmnpjn.js} +2 -2
  4. package/dist/{CsvViewer-Ci5-mke8.js.map → CsvViewer-_bnmnpjn.js.map} +1 -1
  5. package/dist/{DrawioViewer-D2iMuQfD.js → DrawioViewer-Bjns4ob8.js} +2 -2
  6. package/dist/{DrawioViewer-D2iMuQfD.js.map → DrawioViewer-Bjns4ob8.js.map} +1 -1
  7. package/dist/{EpubViewer-D4ydsa9X.js → EpubViewer-DoFuZTtA.js} +2 -2
  8. package/dist/{EpubViewer-D4ydsa9X.js.map → EpubViewer-DoFuZTtA.js.map} +1 -1
  9. package/dist/{IpynbViewer-CWBwiFKX.js → IpynbViewer-eg1ySMD4.js} +2 -2
  10. package/dist/{IpynbViewer-CWBwiFKX.js.map → IpynbViewer-eg1ySMD4.js.map} +1 -1
  11. package/dist/{MermaidViewer-CDhuqQli.js → MermaidViewer-BZ13vA6S.js} +2 -2
  12. package/dist/{MermaidViewer-CDhuqQli.js.map → MermaidViewer-BZ13vA6S.js.map} +1 -1
  13. package/dist/{PsdViewer-C_TkFb2H.js → PsdViewer-qFkE4KCB.js} +2 -2
  14. package/dist/{PsdViewer-C_TkFb2H.js.map → PsdViewer-qFkE4KCB.js.map} +1 -1
  15. package/dist/{TiffViewer-BmWCXR7e.js → TiffViewer-eHJTCYsd.js} +2 -2
  16. package/dist/{TiffViewer-BmWCXR7e.js.map → TiffViewer-eHJTCYsd.js.map} +1 -1
  17. package/dist/{Viewer3D-5kPT1QsY.js → Viewer3D-C4--ljdO.js} +2 -2
  18. package/dist/{Viewer3D-5kPT1QsY.js.map → Viewer3D-C4--ljdO.js.map} +1 -1
  19. package/dist/filex-core.js +1 -1
  20. package/dist/filex-core.umd.cjs +56 -56
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-BSDT0PU8.js +11859 -0
  23. package/dist/index-BSDT0PU8.js.map +1 -0
  24. package/dist/index.d.ts +59 -0
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +106 -12
  28. package/src/components/SecondaryPane.vue +88 -78
  29. package/src/components/Toolbar.vue +112 -3
  30. package/src/composables/useKeyboardShortcuts.ts +6 -0
  31. package/src/composables/useTabs.ts +11 -2
  32. package/src/styles/base.css +50 -4
  33. package/src/styles/variables.css +2 -2
  34. package/dist/index-nc0-oI_l.js +0 -11723
  35. package/dist/index-nc0-oI_l.js.map +0 -1
@@ -1341,9 +1341,11 @@ function openNodeInNewTab(n: FileNode) {
1341
1341
  void markRecent(n);
1342
1342
  }
1343
1343
 
1344
- type ContextMode = 'selection' | 'breadcrumb';
1344
+ type ContextMode = 'selection' | 'breadcrumb' | 'pane' /* ui-fix — yan panel sağ-tık */;
1345
1345
  const ctxMode = ref<ContextMode>('selection');
1346
1346
  const breadcrumbCtxPath = ref<string>('');
1347
+ /* ui-fix — yan panel sağ-tık menüsünün hedef node'ları (pane selection). */
1348
+ const paneCtxTargets = ref<FileNode[]>([]);
1347
1349
  const breadcrumbCtxLabel = ref<string>('');
1348
1350
 
1349
1351
  const selectionMode = computed<SelectionMode>(() => {
@@ -1388,6 +1390,61 @@ function onCrumbContext(payload: { x: number; y: number; adapterPath: string; la
1388
1390
  ctxRef.value?.show({ clientX: payload.x, clientY: payload.y }, []);
1389
1391
  }
1390
1392
 
1393
+ /* ui-fix — yan (ikincil) panelde sağ-tık: pane'i aktifleştir, seçili
1394
+ * node'lara göre menüyü aç. Pane "hafif" olduğundan (delete/rename modal'ları
1395
+ * ana panelde) menü aç/yeni-sekme/indir/kopyala/kes/yapıştır ile sınırlı;
1396
+ * hepsi pane-route'lu (dispatchPaneAction). */
1397
+ function onPaneContext(node: FileNode | null, ev: MouseEvent) {
1398
+ activePane.value = 'split';
1399
+ const sel = splitPaneRef.value?.selectedNodes() ?? [];
1400
+ // node=null (boş alana sağ-tık): seçimsiz menü (yalnız Yapıştır). Aksi
1401
+ // halde pane seçimi (yoksa tıklanan node) hedeftir.
1402
+ paneCtxTargets.value = node ? (sel.length > 0 ? sel : [node]) : [];
1403
+ ctxMode.value = 'pane';
1404
+ ctxRef.value?.show({ clientX: ev.clientX, clientY: ev.clientY }, paneCtxTargets.value);
1405
+ }
1406
+
1407
+ function paneActionList(sel: FileNode[]): ContextAction[] {
1408
+ const single = sel.length === 1;
1409
+ const isFile = single && sel[0]?.type === 'file';
1410
+ const isDir = single && sel[0]?.type === 'dir';
1411
+ const any = sel.length > 0;
1412
+ return [
1413
+ { key: 'open', label: t('ctx.open'), icon: '↗', hidden: !single },
1414
+ { key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉', hidden: !isDir } /* wiring:d1 */,
1415
+ { key: 'download', label: t('ctx.download'), icon: '⬇', hidden: !single, disabled: !isFile },
1416
+ { divider: true, key: 'psep', label: '', hidden: !any },
1417
+ { key: 'copy', label: t('ctx.copy'), icon: '❐', hidden: !any },
1418
+ { key: 'cut', label: t('ctx.cut'), icon: '✂', hidden: !any },
1419
+ { key: 'paste', label: t('ctx.paste'), icon: '📋', disabled: !clipboard.value.mode },
1420
+ ];
1421
+ }
1422
+
1423
+ async function dispatchPaneAction(key: string, targets: FileNode[]) {
1424
+ switch (key) {
1425
+ case 'open':
1426
+ if (targets[0]) openNodeInNewTab(targets[0]);
1427
+ break;
1428
+ case 'open-tab':
1429
+ if (targets[0]) {
1430
+ tabsApi.openTab(paneToUser(targets[0].path), { viewMode: viewMode.value, background: true });
1431
+ }
1432
+ break;
1433
+ case 'download':
1434
+ if (targets[0]) downloadFile(targets[0]);
1435
+ break;
1436
+ case 'copy':
1437
+ paneCopy();
1438
+ break;
1439
+ case 'cut':
1440
+ paneCut();
1441
+ break;
1442
+ case 'paste':
1443
+ await panePaste();
1444
+ break;
1445
+ }
1446
+ }
1447
+
1391
1448
  const contextActions = computed<ContextAction[]>(() => {
1392
1449
  if (ctxMode.value === 'breadcrumb') {
1393
1450
  return [
@@ -1395,6 +1452,9 @@ const contextActions = computed<ContextAction[]>(() => {
1395
1452
  { key: 'copy-path', label: t('breadcrumb.copy_path'), icon: '📋' },
1396
1453
  ];
1397
1454
  }
1455
+ if (ctxMode.value === 'pane' /* ui-fix — yan panel menüsü */) {
1456
+ return paneActionList(paneCtxTargets.value);
1457
+ }
1398
1458
 
1399
1459
  const sel = selection.nodes.value;
1400
1460
  const any = sel.length > 0;
@@ -1517,6 +1577,10 @@ async function onContextAction(action: ContextAction, targets: FileNode[]) {
1517
1577
  }
1518
1578
  return;
1519
1579
  }
1580
+ if (ctxMode.value === 'pane' /* ui-fix — yan panel aksiyonları pane-route */) {
1581
+ await dispatchPaneAction(action.key, paneCtxTargets.value);
1582
+ return;
1583
+ }
1520
1584
  await dispatchItemAction(action.key, targets);
1521
1585
  }
1522
1586
 
@@ -1581,7 +1645,10 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
1581
1645
  flashToast('Kopyala → Yapıştır hazır');
1582
1646
  break;
1583
1647
  case 'paste':
1584
- await paste();
1648
+ /* ui-fix — sağ-klik menüsünden yapıştırma da aktif panele gider
1649
+ * (klavye kısayolu zaten pane-route'luydu; menü değildi). */
1650
+ if (paneIsActive.value) await panePaste();
1651
+ else await paste();
1585
1652
  break;
1586
1653
  case 'delete':
1587
1654
  showDelete.value = true;
@@ -2518,14 +2585,30 @@ function toggleSplit() {
2518
2585
  return;
2519
2586
  }
2520
2587
  if (isNarrow.value) return;
2521
- tabsApi.setSplit({ path: currentPath.value ?? '' });
2588
+ tabsApi.setSplit({ path: currentPath.value ?? '', viewMode: viewMode.value });
2522
2589
  }
2523
2590
  function closeSplit() {
2524
2591
  tabsApi.setSplit(null);
2525
2592
  activePane.value = 'main';
2526
2593
  }
2527
2594
  function onPaneNavigate(p: string) {
2528
- tabsApi.setSplit({ path: p });
2595
+ tabsApi.setSplit({ ...(activeSplit.value ?? {}), path: p });
2596
+ }
2597
+ /* ui-fix — pane'in KENDİ görünüm modu: split açılırken ana panelinkini
2598
+ * devralır, sonrasında bağımsız. Toolbar'ın görünüm değiştiricisi ve palet
2599
+ * toggle'ı AKTİF panele yazar (Burak: "B tıklıyken ikon değiştir dersem
2600
+ * B'nin değişmesi lazım"). */
2601
+ const paneViewMode = computed<ViewMode>(() => activeSplit.value?.viewMode ?? viewMode.value);
2602
+ function setPaneViewMode(v: ViewMode) {
2603
+ if (!activeSplit.value) return;
2604
+ tabsApi.setSplit({ ...activeSplit.value, viewMode: v });
2605
+ }
2606
+ const displayedViewMode = computed<ViewMode>(() =>
2607
+ paneIsActive.value ? paneViewMode.value : viewMode.value,
2608
+ );
2609
+ function setDisplayedViewMode(v: ViewMode) {
2610
+ if (paneIsActive.value) setPaneViewMode(v);
2611
+ else viewMode.value = v;
2529
2612
  }
2530
2613
 
2531
2614
  // Aktif panel: kısayollar aktif panele gider; panel tıklamayla aktifleşir.
@@ -2883,7 +2966,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
2883
2966
  <!-- /wiring:d1 -->
2884
2967
  <Toolbar
2885
2968
  ref="toolbarRef"
2886
- :view-mode="viewMode"
2969
+ :view-mode="displayedViewMode /* ui-fix — aktif panelin modu */"
2887
2970
  :search-query="searchQuery"
2888
2971
  :trash-active="trashActive"
2889
2972
  :actions="toolbarActions"
@@ -2899,7 +2982,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
2899
2982
  :inspector-open="showInspector /* koru:k1 */"
2900
2983
  @toggle-inspector="toggleInspector /* koru:k1 */"
2901
2984
  @open-theme="showThemeGallery = true /* wiring:c1 */"
2902
- @update:view-mode="viewMode = $event"
2985
+ @update:view-mode="setDisplayedViewMode($event) /* ui-fix — aktif panele */"
2903
2986
  @update:search-query="searchQuery = $event"
2904
2987
  @update:density="density = $event"
2905
2988
  @open-shortcut-settings="showShortcutSettings = true /* wiring:c2 */"
@@ -2911,6 +2994,18 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
2911
2994
  @open-recents="showRecents = true"
2912
2995
  />
2913
2996
 
2997
+ <!-- koru:k1 — fe__main lays the listing body and the inspector panel out
2998
+ as flex siblings (row). Without the inspector open it is visually
2999
+ identical to the previous direct-child fe__body. -->
3000
+ <div class="fe__main" :class="{ 'fe__main--split': splitVisible } /* wiring:d1 */">
3001
+ <!-- ui-fix — sol panelin başlığı (breadcrumb + durum şeritleri + body)
3002
+ tek bir sarmalda: split modunda bu sarmal sol yarıya sığar, böylece
3003
+ breadcrumb tüm sayfayı değil kendi panelini kaplar (SecondaryPane'in
3004
+ kendi kırıntısıyla simetrik). Aktif-panel vurgusu da bu sarmalda. -->
3005
+ <div
3006
+ class="fe__primary"
3007
+ :class="{ 'fe-pane--focus': mainPaneFocus } /* wiring:d1 — aktif panel vurgusu */"
3008
+ >
2914
3009
  <Breadcrumb
2915
3010
  :dirname="dirname"
2916
3011
  :adapter="adapter"
@@ -2952,13 +3047,8 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
2952
3047
  </div>
2953
3048
  <!-- /wiring:e2 -->
2954
3049
 
2955
- <!-- koru:k1 — fe__main lays the listing body and the inspector panel out
2956
- as flex siblings (row). Without the inspector open it is visually
2957
- identical to the previous direct-child fe__body. -->
2958
- <div class="fe__main" :class="{ 'fe__main--split': splitVisible } /* wiring:d1 */">
2959
3050
  <div
2960
3051
  class="fe__body"
2961
- :class="{ 'fe-pane--focus': mainPaneFocus } /* wiring:d1 — aktif panel vurgusu */"
2962
3052
  @pointerdown.capture="setPaneMain() /* wiring:d1 */"
2963
3053
  @click.self="selection.clear()"
2964
3054
  >
@@ -3211,6 +3301,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
3211
3301
  />
3212
3302
  <!-- /wiring:d2 -->
3213
3303
  </div>
3304
+ </div><!-- /fe__primary ui-fix -->
3214
3305
 
3215
3306
  <!-- wiring:d1 — tab başına split: sağ ikincil panel (dar modda kapalı).
3216
3307
  :key tab kimliğine bağlı — tab geçişinde pane kendi konumuyla temiz
@@ -3230,11 +3321,14 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
3230
3321
  :multi-root="multiStorageRoot"
3231
3322
  :virtual-rows="virtualStorageRows"
3232
3323
  :active="paneIsActive"
3324
+ :view-mode="paneViewMode /* ui-fix */"
3325
+ :thumb-src="thumbs.src /* ui-fix */"
3233
3326
  @navigate="onPaneNavigate"
3234
3327
  @activate="activePane = 'split'"
3235
3328
  @close="closeSplit"
3236
3329
  @open-tab="(p: string) => tabsApi.openTab(p, { viewMode: viewMode, background: true })"
3237
3330
  @transfer="onPaneTransfer"
3331
+ @context="onPaneContext /* ui-fix — yan panel sağ-tık menüsü */"
3238
3332
  />
3239
3333
  <!-- /wiring:d1 -->
3240
3334
 
@@ -3476,7 +3570,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
3476
3570
  @navigate="(p: string) => load(p)"
3477
3571
  @new-folder="showNewFolder = true"
3478
3572
  @upload="triggerUpload"
3479
- @toggle-view="viewMode = viewMode === 'list' ? 'grid' : viewMode === 'grid' ? 'gallery' : 'list' /* wiring:d2 — 3 mod döngüsü */"
3573
+ @toggle-view="setDisplayedViewMode(displayedViewMode === 'list' ? 'grid' : displayedViewMode === 'grid' ? 'gallery' : 'list') /* wiring:d2 + ui-fix — 3 mod döngüsü, aktif panele */"
3480
3574
  @open-trash="loadTrash"
3481
3575
  @refresh="() => load()"
3482
3576
  @go-up="goUp"
@@ -18,10 +18,12 @@
18
18
  */
19
19
  import { computed, ref } from 'vue';
20
20
  import type { FileApi } from '../composables/useFileApi';
21
- import type { FileNode } from '../types/FileNode';
21
+ import type { FileNode, ViewMode } from '../types/FileNode';
22
22
  import type { LocaleCode } from '../types/ExplorerConfig';
23
23
  import { useLocale } from '../composables/useLocale';
24
- import { fileIconSvg } from '../lib/fileIcons';
24
+ import ListView from './ListView.vue';
25
+ import GridView from './GridView.vue';
26
+ import GalleryView from './GalleryView.vue';
25
27
 
26
28
  // Same literals ListView/GridView already hardcode for the internal DnD
27
29
  // channel; `-src` carries the origin directory so the host can move (and
@@ -50,6 +52,11 @@ const props = defineProps<{
50
52
  virtualRows?: () => FileNode[];
51
53
  /** Active-panel highlight (keyboard target). */
52
54
  active?: boolean;
55
+ /** ui-fix — the pane renders the SAME view components as the main
56
+ * panel (list/grid/gallery) instead of its own flat list. */
57
+ viewMode?: ViewMode;
58
+ /** ui-fix — authenticated thumb resolver, forwarded to grid/gallery. */
59
+ thumbSrc?: (n: FileNode) => string | null;
53
60
  }>();
54
61
 
55
62
  const emit = defineEmits<{
@@ -58,9 +65,12 @@ const emit = defineEmits<{
58
65
  (e: 'close'): void;
59
66
  (e: 'open-tab', path: string): void;
60
67
  (e: 'transfer', p: { sources: string[]; targetWire: string; originWire?: string }): void;
68
+ /* ui-fix — yan panelde sağ-tık: menü ana bileşende (FileExplorer) açılır.
69
+ * node=null → boş alana sağ-tık (seçimsiz, yalnız Yapıştır). */
70
+ (e: 'context', node: FileNode | null, ev: MouseEvent): void;
61
71
  }>();
62
72
 
63
- const { t, formatSize, nodeDisplayName } = useLocale(() => props.locale);
73
+ const { t } = useLocale(() => props.locale);
64
74
 
65
75
  const path = ref<string>('');
66
76
  const files = ref<FileNode[]>([]);
@@ -149,35 +159,52 @@ function crumbGo(target: string) {
149
159
  // Selection + navigation
150
160
  // ------------------------------------------------------------------
151
161
 
152
- function onRowClick(n: FileNode, ev: MouseEvent) {
162
+ /* ui-fix adapter for the shared view components' click contract
163
+ * ({ctrl, shift} mod object instead of a MouseEvent). Shift behaves as
164
+ * ctrl here: the pane keeps a simple Set, no range anchor. */
165
+ function onViewClick(n: FileNode, mod: { ctrl: boolean; shift: boolean }) {
153
166
  emit('activate');
154
- const multi = ev.ctrlKey || ev.metaKey;
167
+ const multi = mod.ctrl || mod.shift;
155
168
  const next = new Set<string>(multi ? selected.value : []);
156
169
  if (multi && next.has(n.path)) next.delete(n.path);
157
170
  else next.add(n.path);
158
171
  selected.value = next;
159
172
  }
160
173
 
161
- function onRowDbl(n: FileNode) {
162
- if (n.type !== 'dir') return;
163
- void loadPane(isStorageRow(n) ? n.path : props.toUser(n.path));
174
+ /* ui-fix — right-click on a pane item: activate + select it, then let the
175
+ * host open the pane context menu (its actions are pane-routed). Selecting
176
+ * first keeps the menu's targets on THIS pane, not the main panel. */
177
+ function onViewContext(n: FileNode, ev: MouseEvent) {
178
+ ev.preventDefault();
179
+ ev.stopPropagation();
180
+ emit('activate');
181
+ if (!selected.value.has(n.path)) selected.value = new Set([n.path]);
182
+ emit('context', n, ev);
164
183
  }
165
184
 
166
- // Middle-button mousedown must be cancelled on rows: in a scrollable body
167
- // Chromium's autoscroll takes over and auxclick is never generated.
168
- function onRowMiddleDown(ev: MouseEvent) {
169
- if (ev.button === 1) ev.preventDefault();
185
+ /* ui-fix boş alana (arka plan) sağ-tık: seçimsiz menü (yalnız Yapıştır)
186
+ * boş klasöre / mevcut klasöre dosya yapıştırma. Satır menüsü stopProp
187
+ * ettiğinden bu yalnız gerçek boş alanda tetiklenir. */
188
+ function onBgContext(ev: MouseEvent) {
189
+ ev.preventDefault();
190
+ ev.stopPropagation(); // FileExplorer kökündeki canvas menüsüne bubble etmesin
191
+ emit('activate');
192
+ selected.value = new Set();
193
+ emit('context', null, ev);
170
194
  }
171
195
 
172
- // Middle-click on a folder opens it in a NEW TAB (same convention as the
173
- // main listing). stopPropagation keeps the host's delegated auxclick
174
- // listener from double-handling it.
175
- function onRowAux(n: FileNode, ev: MouseEvent) {
176
- if (ev.button !== 1) return;
177
- ev.preventDefault();
178
- ev.stopPropagation();
196
+ /* ui-fix drop-into from the shared views: same payload path as the
197
+ * old flat rows (dir check + wire-qualified row paths). */
198
+ function onViewDropInto(target: FileNode, ev: DragEvent) {
199
+ dropBg.value = false;
200
+ if (target.type !== 'dir' || isStorageRow(target)) return;
201
+ if (!acceptDrag(ev)) return;
202
+ handleDropPayload(ev, target.path);
203
+ }
204
+
205
+ function onRowDbl(n: FileNode) {
179
206
  if (n.type !== 'dir') return;
180
- emit('open-tab', isStorageRow(n) ? n.path : props.toUser(n.path));
207
+ void loadPane(isStorageRow(n) ? n.path : props.toUser(n.path));
181
208
  }
182
209
 
183
210
  function clearSelection() {
@@ -212,7 +239,6 @@ function onRowDragStart(n: FileNode, ev: DragEvent) {
212
239
  // Drop target (dir rows + pane background)
213
240
  // ------------------------------------------------------------------
214
241
 
215
- const dropPath = ref<string | null>(null);
216
242
  const dropBg = ref(false);
217
243
 
218
244
  function acceptDrag(ev: DragEvent): boolean {
@@ -238,28 +264,6 @@ function handleDropPayload(ev: DragEvent, targetWire: string) {
238
264
  emit('transfer', { sources, targetWire, originWire: origin });
239
265
  }
240
266
 
241
- function onRowDragOver(n: FileNode, ev: DragEvent) {
242
- if (n.type !== 'dir' || isStorageRow(n)) return;
243
- if (!acceptDrag(ev)) return;
244
- ev.preventDefault();
245
- ev.stopPropagation();
246
- if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
247
- dropPath.value = n.path;
248
- dropBg.value = false;
249
- }
250
-
251
- function onRowDragLeave(n: FileNode) {
252
- if (dropPath.value === n.path) dropPath.value = null;
253
- }
254
-
255
- function onRowDrop(n: FileNode, ev: DragEvent) {
256
- dropPath.value = null;
257
- dropBg.value = false;
258
- if (n.type !== 'dir' || isStorageRow(n)) return;
259
- if (!acceptDrag(ev)) return;
260
- handleDropPayload(ev, n.path); // real rows are wire-qualified
261
- }
262
-
263
267
  function onBgDragOver(ev: DragEvent) {
264
268
  if (!acceptDrag(ev) || atVirtualRoot.value) return;
265
269
  ev.preventDefault();
@@ -274,7 +278,6 @@ function onBgDragLeave() {
274
278
 
275
279
  function onBgDrop(ev: DragEvent) {
276
280
  dropBg.value = false;
277
- dropPath.value = null;
278
281
  if (!acceptDrag(ev) || atVirtualRoot.value) return;
279
282
  handleDropPayload(ev, props.qualify(path.value));
280
283
  }
@@ -313,10 +316,6 @@ function getPath(): string {
313
316
  }
314
317
 
315
318
  defineExpose({ reload, goUp, selectAll, openSelected, selectedNodes, getPath });
316
-
317
- function specialEmojiFor(n: FileNode): string | null {
318
- return isStorageRow(n) ? '💾' : null;
319
- }
320
319
  </script>
321
320
 
322
321
  <template>
@@ -360,6 +359,7 @@ function specialEmojiFor(n: FileNode): string | null {
360
359
  class="fe-split__body"
361
360
  :class="{ 'is-dropover': dropBg }"
362
361
  @click.self="clearSelection"
362
+ @contextmenu="onBgContext"
363
363
  @dragover="onBgDragOver"
364
364
  @dragleave="onBgDragLeave"
365
365
  @drop="onBgDrop"
@@ -376,36 +376,46 @@ function specialEmojiFor(n: FileNode): string | null {
376
376
  <div v-else-if="files.length === 0" class="fe-split__state">
377
377
  {{ t('empty.folder') }}
378
378
  </div>
379
- <div v-else class="fe-split__list" role="listbox" aria-multiselectable="true">
380
- <div
381
- v-for="n in files"
382
- :key="n.path"
383
- class="fe-split__row"
384
- :class="{
385
- 'is-selected': selected.has(n.path),
386
- 'is-dir': n.type === 'dir',
387
- 'is-droptarget': dropPath === n.path,
388
- }"
389
- role="option"
390
- :aria-selected="selected.has(n.path) ? 'true' : 'false'"
391
- tabindex="0"
392
- draggable="true"
393
- @click="onRowClick(n, $event)"
394
- @dblclick="onRowDbl(n)"
395
- @mousedown="onRowMiddleDown($event)"
396
- @auxclick="onRowAux(n, $event)"
397
- @dragstart="onRowDragStart(n, $event)"
398
- @dragover="onRowDragOver(n, $event)"
399
- @dragleave="onRowDragLeave(n)"
400
- @drop="onRowDrop(n, $event)"
401
- >
402
- <span v-if="specialEmojiFor(n)" class="fe-split__icon" aria-hidden="true">{{ specialEmojiFor(n) }}</span>
403
- <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
404
- <span v-else class="fe-split__icon" aria-hidden="true" v-html="fileIconSvg(n)"></span>
405
- <span class="fe-split__name" :title="n.basename">{{ nodeDisplayName(n) }}</span>
406
- <span class="fe-split__size">{{ n.type === 'dir' ? '' : formatSize(n.size) }}</span>
407
- </div>
408
- </div>
379
+ <!-- ui-fix aynı görünüm bileşenleri (list/grid/gallery), pane'in
380
+ kendi viewMode'uyla; eski düz fe-split__list kalktı. -->
381
+ <ListView
382
+ v-else-if="(viewMode ?? 'list') === 'list'"
383
+ :files="files"
384
+ :selected="selected"
385
+ :locale="locale"
386
+ :loading="loading"
387
+ @click-row="onViewClick"
388
+ @dbl-row="onRowDbl"
389
+ @context-row="onViewContext"
390
+ @item-drag-start="onRowDragStart"
391
+ @item-drop-into="onViewDropInto"
392
+ />
393
+ <GridView
394
+ v-else-if="viewMode === 'grid'"
395
+ :files="files"
396
+ :selected="selected"
397
+ :locale="locale"
398
+ :loading="loading"
399
+ :thumb-src="thumbSrc"
400
+ @click-card="onViewClick"
401
+ @dbl-card="onRowDbl"
402
+ @context-card="onViewContext"
403
+ @item-drag-start="onRowDragStart"
404
+ @item-drop-into="onViewDropInto"
405
+ />
406
+ <GalleryView
407
+ v-else
408
+ :files="files"
409
+ :selected="selected"
410
+ :locale="locale"
411
+ :loading="loading"
412
+ :thumb-src="thumbSrc"
413
+ @click-card="onViewClick"
414
+ @dbl-card="onRowDbl"
415
+ @context-card="onViewContext"
416
+ @item-drag-start="onRowDragStart"
417
+ @item-drop-into="onViewDropInto"
418
+ />
409
419
  </div>
410
420
  </section>
411
421
  </template>
@@ -152,6 +152,74 @@ const mode = computed<SelectionMode>(() => props.selectionMode ?? 'none');
152
152
  // context menu uses, the two menus are guaranteed to match.
153
153
  const toolbarItems = computed(() => props.actions.filter((a) => !a.divider && !a.hidden));
154
154
 
155
+ /* === ui-fix — wide-mode action folding ===============================
156
+ * Long selection-action rows used to wrap the whole toolbar and push the
157
+ * search / view controls onto a second line. The row is now single-line:
158
+ * actions that do not fit fold into a "⋯" menu. Widths come from a
159
+ * hidden measurement strip (all actions always rendered there), so the
160
+ * calculation never mutates what it measures. */
161
+ const primaryEl = ref<HTMLElement | null>(null);
162
+ const measureEl = ref<HTMLElement | null>(null);
163
+ const wideMoreBtnEl = ref<HTMLElement | null>(null);
164
+ const wideMoreRef = ref<InstanceType<typeof ContextMenu> | null>(null);
165
+ const visibleActionCount = ref(Number.MAX_SAFE_INTEGER);
166
+ const visibleToolbarItems = computed(() => toolbarItems.value.slice(0, visibleActionCount.value));
167
+ const overflowToolbarItems = computed(() => toolbarItems.value.slice(visibleActionCount.value));
168
+ const WIDE_MORE_BTN_W = 40;
169
+
170
+ function recalcFold() {
171
+ if (props.narrow) return;
172
+ const cont = primaryEl.value;
173
+ const meas = measureEl.value;
174
+ if (!cont || !meas) {
175
+ visibleActionCount.value = toolbarItems.value.length;
176
+ return;
177
+ }
178
+ const gap = parseFloat(getComputedStyle(cont).gap) || 8;
179
+ let fixed = 0;
180
+ for (const child of Array.from(cont.children) as HTMLElement[]) {
181
+ if (child === meas || child === wideMoreBtnEl.value) continue;
182
+ if (child.classList.contains('fe-btn--fold')) continue;
183
+ if (child.offsetWidth > 0) fixed += child.offsetWidth + gap;
184
+ }
185
+ const widths = (Array.from(meas.children) as HTMLElement[]).map((c) => c.offsetWidth + gap);
186
+ const total = widths.reduce((s, w) => s + w, 0);
187
+ const avail = cont.clientWidth - fixed;
188
+ if (total <= avail) {
189
+ visibleActionCount.value = widths.length;
190
+ return;
191
+ }
192
+ let used = WIDE_MORE_BTN_W + gap;
193
+ let count = 0;
194
+ for (const w of widths) {
195
+ if (used + w > avail) break;
196
+ used += w;
197
+ count += 1;
198
+ }
199
+ visibleActionCount.value = count;
200
+ }
201
+
202
+ let foldRo: ResizeObserver | undefined;
203
+ onMounted(() => {
204
+ if (typeof ResizeObserver !== 'undefined' && primaryEl.value) {
205
+ foldRo = new ResizeObserver(() => recalcFold());
206
+ foldRo.observe(primaryEl.value);
207
+ }
208
+ void nextTick(recalcFold);
209
+ });
210
+ onBeforeUnmount(() => foldRo?.disconnect());
211
+ watch(
212
+ () => [toolbarItems.value, props.narrow, props.trashActive, mode.value] as const,
213
+ () => void nextTick(recalcFold),
214
+ { deep: false },
215
+ );
216
+
217
+ function openWideMore() {
218
+ const r = wideMoreBtnEl.value?.getBoundingClientRect();
219
+ wideMoreRef.value?.show({ clientX: r ? r.right : 0, clientY: r ? r.bottom + 4 : 0 } as MouseEvent, []);
220
+ }
221
+ /* === /ui-fix ========================================================= */
222
+
155
223
  function fire(key: string) {
156
224
  emit('action', key);
157
225
  }
@@ -291,7 +359,7 @@ function onMoreSelect(a: ContextAction) {
291
359
  >
292
360
  <!-- bag:b4 — wide layout, untouched; renders exactly as before when not narrow -->
293
361
  <template v-if="!narrow">
294
- <div class="fe-toolbar__primary">
362
+ <div ref="primaryEl" class="fe-toolbar__primary">
295
363
  <button
296
364
  v-if="canGoUp"
297
365
  type="button"
@@ -315,10 +383,10 @@ function onMoreSelect(a: ContextAction) {
315
383
  </button>
316
384
 
317
385
  <button
318
- v-for="a in toolbarItems"
386
+ v-for="a in visibleToolbarItems"
319
387
  :key="a.key"
320
388
  type="button"
321
- class="fe-btn"
389
+ class="fe-btn fe-btn--fold"
322
390
  :class="{ 'fe-btn--danger': a.danger, 'is-disabled': a.disabled }"
323
391
  :disabled="a.disabled"
324
392
  :title="a.label"
@@ -328,6 +396,36 @@ function onMoreSelect(a: ContextAction) {
328
396
  <span class="fe-btn__label">{{ a.label }}</span>
329
397
  </button>
330
398
 
399
+ <!-- ui-fix — sığmayan aksiyonlar tek satırı korumak için ⋯ menüsüne
400
+ katlanır (arama/görünüm kontrolleri artık alt satıra düşmez). -->
401
+ <button
402
+ v-if="overflowToolbarItems.length > 0"
403
+ ref="wideMoreBtnEl"
404
+ type="button"
405
+ class="fe-btn fe-btn--icon-only"
406
+ :title="t('toolbar.more')"
407
+ :aria-label="t('toolbar.more')"
408
+ aria-haspopup="menu"
409
+ @click="openWideMore"
410
+ >
411
+ <span class="fe-icon">⋯</span>
412
+ </button>
413
+
414
+ <!-- Görünmez ölçüm şeridi: TÜM aksiyonlar her zaman burada render
415
+ edilir; katlama hesabı gerçek genişliklerden yapılır. -->
416
+ <div ref="measureEl" class="fe-toolbar__measure" aria-hidden="true">
417
+ <button
418
+ v-for="a in toolbarItems"
419
+ :key="'m-' + a.key"
420
+ type="button"
421
+ class="fe-btn"
422
+ tabindex="-1"
423
+ >
424
+ <span class="fe-icon">{{ a.icon }}</span>
425
+ <span class="fe-btn__label">{{ a.label }}</span>
426
+ </button>
427
+ </div>
428
+
331
429
  <button
332
430
  v-if="pasteEnabled && mode === 'none' && !trashActive && !atVirtualRoot && canWrite !== false"
333
431
  type="button"
@@ -589,5 +687,16 @@ function onMoreSelect(a: ContextAction) {
589
687
  @select="onMoreSelect"
590
688
  />
591
689
  </template>
690
+
691
+ <!-- ui-fix — wide modda katlanan aksiyonların ⋯ menüsü -->
692
+ <ContextMenu
693
+ v-if="!narrow"
694
+ ref="wideMoreRef"
695
+ :locale="locale"
696
+ :theme="theme || 'auto'"
697
+ :sheet="false"
698
+ :actions="overflowToolbarItems"
699
+ @select="(a) => fire(a.key)"
700
+ />
592
701
  </div>
593
702
  </template>
@@ -341,6 +341,12 @@ export function useKeyboardShortcuts(rootEl: Ref<HTMLElement | null>, handlers:
341
341
  const root = rootEl.value;
342
342
  if (!root) return;
343
343
 
344
+ /* ui-fix — while a context menu is open, global shortcuts stay quiet
345
+ * (except Esc, which the menu scopes itself). Otherwise e.g. Delete
346
+ * opens a modal UNDER the menu backdrop and the UI wedges: the
347
+ * backdrop intercepts every click on the new dialog. */
348
+ if (e.key !== 'Escape' && document.querySelector('.fe-ctx-backdrop')) return;
349
+
344
350
  // Skip when the event originates inside a form control — don't
345
351
  // want `Delete` while editing a filename, `/` while typing in the
346
352
  // search box, etc. Escape always goes through so modals can close.
@@ -30,6 +30,9 @@ import type { ViewMode } from '../types/FileNode';
30
30
  /** Per-tab split state — the secondary pane's own location. */
31
31
  export interface TabSplit {
32
32
  path: string;
33
+ /** ui-fix — the pane's OWN view mode (list/grid/gallery); undefined
34
+ * inherits the main panel's mode at split time. */
35
+ viewMode?: ViewMode;
33
36
  }
34
37
 
35
38
  export interface TabState {
@@ -82,10 +85,16 @@ export function useTabs(opts: UseTabsOptions) {
82
85
  // gallery wave) must survive a round-trip through an older schema.
83
86
  const vm =
84
87
  typeof t.viewMode === 'string' && t.viewMode ? (t.viewMode as ViewMode) : 'list';
85
- const rawSplit = t.split as { path?: unknown } | null | undefined;
88
+ const rawSplit = t.split as { path?: unknown; viewMode?: unknown } | null | undefined;
86
89
  const split =
87
90
  rawSplit && typeof rawSplit === 'object' && typeof rawSplit.path === 'string'
88
- ? { path: rawSplit.path }
91
+ ? {
92
+ path: rawSplit.path,
93
+ viewMode:
94
+ typeof rawSplit.viewMode === 'string' && rawSplit.viewMode
95
+ ? (rawSplit.viewMode as ViewMode)
96
+ : undefined,
97
+ }
89
98
  : null;
90
99
  clean.push({
91
100
  id: typeof t.id === 'string' && t.id ? t.id : makeId(),