@brftech/filex-core 0.5.0 → 0.6.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.
Files changed (45) hide show
  1. package/dist/{ArchiveViewer-DKW-HyXT.js → ArchiveViewer-EGgNqLPE.js} +2 -2
  2. package/dist/{ArchiveViewer-DKW-HyXT.js.map → ArchiveViewer-EGgNqLPE.js.map} +1 -1
  3. package/dist/{CsvViewer-D2HmP2yr.js → CsvViewer-BvriJ42M.js} +2 -2
  4. package/dist/{CsvViewer-D2HmP2yr.js.map → CsvViewer-BvriJ42M.js.map} +1 -1
  5. package/dist/{DrawioViewer-DPNvOFCs.js → DrawioViewer-B_bSPsWX.js} +2 -2
  6. package/dist/{DrawioViewer-DPNvOFCs.js.map → DrawioViewer-B_bSPsWX.js.map} +1 -1
  7. package/dist/{EpubViewer-ByqjNuNL.js → EpubViewer-hl68h4gG.js} +2 -2
  8. package/dist/{EpubViewer-ByqjNuNL.js.map → EpubViewer-hl68h4gG.js.map} +1 -1
  9. package/dist/{IpynbViewer-2iONhbuJ.js → IpynbViewer-lhwOjXK7.js} +2 -2
  10. package/dist/{IpynbViewer-2iONhbuJ.js.map → IpynbViewer-lhwOjXK7.js.map} +1 -1
  11. package/dist/{MermaidViewer-CmiU7hIT.js → MermaidViewer-H2Z6AYXg.js} +2 -2
  12. package/dist/{MermaidViewer-CmiU7hIT.js.map → MermaidViewer-H2Z6AYXg.js.map} +1 -1
  13. package/dist/{PsdViewer-Inhl2yN9.js → PsdViewer-Bsk-11yH.js} +2 -2
  14. package/dist/{PsdViewer-Inhl2yN9.js.map → PsdViewer-Bsk-11yH.js.map} +1 -1
  15. package/dist/{TiffViewer-C2AFFEze.js → TiffViewer-D-j9vycG.js} +2 -2
  16. package/dist/{TiffViewer-C2AFFEze.js.map → TiffViewer-D-j9vycG.js.map} +1 -1
  17. package/dist/{Viewer3D-YdW14q_n.js → Viewer3D-Bcr70rTe.js} +2 -2
  18. package/dist/{Viewer3D-YdW14q_n.js.map → Viewer3D-Bcr70rTe.js.map} +1 -1
  19. package/dist/filex-core.js +42 -39
  20. package/dist/filex-core.umd.cjs +59 -58
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-C_ZSnk_I.js +11109 -0
  23. package/dist/index-C_ZSnk_I.js.map +1 -0
  24. package/dist/index.d.ts +234 -1
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +367 -14
  28. package/src/components/CommandPalette.vue +12 -1
  29. package/src/components/GalleryView.vue +214 -0
  30. package/src/components/GridView.vue +1 -0
  31. package/src/components/InspectorPanel.vue +170 -0
  32. package/src/components/ListView.vue +1 -0
  33. package/src/components/SecondaryPane.vue +411 -0
  34. package/src/components/TabBar.vue +174 -0
  35. package/src/components/Toolbar.vue +23 -5
  36. package/src/composables/useFileApi.ts +53 -0
  37. package/src/composables/useKeyboardShortcuts.ts +19 -0
  38. package/src/composables/useTabs.ts +222 -0
  39. package/src/index.ts +6 -0
  40. package/src/locales/en.ts +33 -0
  41. package/src/locales/tr.ts +33 -0
  42. package/src/styles/base.css +538 -0
  43. package/src/types/FileNode.ts +1 -1
  44. package/dist/index-eARevpz0.js +0 -9952
  45. package/dist/index-eARevpz0.js.map +0 -1
@@ -42,6 +42,7 @@ import RecentlyOpened from './components/RecentlyOpened.vue';
42
42
  import Breadcrumb from './components/Breadcrumb.vue';
43
43
  import ListView from './components/ListView.vue';
44
44
  import GridView from './components/GridView.vue';
45
+ import GalleryView from './components/GalleryView.vue'; /* wiring:d2 */
45
46
  import ContextMenu, { type ContextAction } from './components/ContextMenu.vue';
46
47
  import UploadProgress from './components/UploadProgress.vue';
47
48
  import PendingOpsTray from './components/PendingOpsTray.vue';
@@ -65,6 +66,11 @@ import { useOperations } from './composables/useOperations';
65
66
  /* wiring:c4 */
66
67
  import OnboardingTour from './components/OnboardingTour.vue';
67
68
  /* /wiring:c4 */
69
+ /* wiring:d1 — sekmeler + tab başına split */
70
+ import TabBar from './components/TabBar.vue';
71
+ import SecondaryPane from './components/SecondaryPane.vue';
72
+ import { useTabs, type TabState } from './composables/useTabs';
73
+ /* /wiring:d1 */
68
74
 
69
75
  import NewFolderModal from './modals/NewFolderModal.vue';
70
76
  import RenameModal from './modals/RenameModal.vue';
@@ -167,6 +173,7 @@ const pendingOps = usePendingOps(props.config, api, {
167
173
  flashToast(`${verb} (${op.progress_total})`);
168
174
  }
169
175
  void load();
176
+ void splitPaneRef.value?.reload(); /* wiring:d1 — ikincil panel de tazelenir */
170
177
  },
171
178
  });
172
179
 
@@ -206,7 +213,7 @@ const viewMode = customRef<ViewMode>((track, trigger) => {
206
213
  let value: ViewMode = (() => {
207
214
  try {
208
215
  const stored = localStorage.getItem(VIEW_MODE_KEY);
209
- if (stored === 'list' || stored === 'grid') return stored;
216
+ if (stored === 'list' || stored === 'grid' || stored === 'gallery') return stored; /* wiring:d2 */
210
217
  } catch {
211
218
  /* private mode */
212
219
  }
@@ -1084,8 +1091,9 @@ useKeyboardShortcuts(rootEl, {
1084
1091
  showRename.value = true;
1085
1092
  }
1086
1093
  },
1087
- onSelectAll: () => selection.selectAll(),
1094
+ onSelectAll: () => (paneIsActive.value ? splitPaneRef.value?.selectAll() : selection.selectAll()) /* wiring:d1 pane-route */,
1088
1095
  onOpen: () => {
1096
+ if (paneIsActive.value) return splitPaneRef.value?.openSelected(); /* wiring:d1 pane-route */
1089
1097
  const n = selection.nodes.value[0];
1090
1098
  if (n) openNode(n);
1091
1099
  },
@@ -1102,10 +1110,10 @@ useKeyboardShortcuts(rootEl, {
1102
1110
  if (isNarrow.value) closeInspector();
1103
1111
  },
1104
1112
  onFocusSearch: () => toolbarRef.value?.focusSearch(),
1105
- onCut: () => cut(),
1106
- onCopy: () => copyToClipboard(),
1107
- onPaste: () => paste(),
1108
- onGoUp: () => goUp(),
1113
+ onCut: () => (paneIsActive.value ? paneCut() : cut()) /* wiring:d1 pane-route */,
1114
+ onCopy: () => (paneIsActive.value ? paneCopy() : copyToClipboard()) /* wiring:d1 pane-route */,
1115
+ onPaste: () => (paneIsActive.value ? void panePaste() : void paste()) /* wiring:d1 pane-route */,
1116
+ onGoUp: () => (paneIsActive.value ? splitPaneRef.value?.goUp() : goUp()) /* wiring:d1 pane-route */,
1109
1117
  /* cila:c wiring */
1110
1118
  onPathJump: () => {
1111
1119
  showPalette.value = true;
@@ -1116,6 +1124,12 @@ useKeyboardShortcuts(rootEl, {
1116
1124
  /* /cila:c wiring */
1117
1125
  onQuickLook: () => quickLookToggle() /* wiring:c2 */,
1118
1126
  onToggleInspector: () => toggleInspector() /* koru:k1 */,
1127
+ /* wiring:d1 — sekme aksiyonları (registry: tab-new/close/next/prev) */
1128
+ onTabNew: () => newTabHere(),
1129
+ onTabClose: () => closeTabById(tabsActiveId.value),
1130
+ onTabNext: () => nextTab(),
1131
+ onTabPrev: () => prevTab(),
1132
+ /* /wiring:d1 */
1119
1133
  hasSelection: () => !selection.isEmpty.value,
1120
1134
  });
1121
1135
 
@@ -1370,6 +1384,7 @@ const contextActions = computed<ContextAction[]>(() => {
1370
1384
  if (!single) return [];
1371
1385
  return [
1372
1386
  { key: 'open', label: t('ctx.open'), icon: '↗' },
1387
+ { key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉' } /* wiring:d1 */,
1373
1388
  ];
1374
1389
  }
1375
1390
 
@@ -1410,6 +1425,7 @@ function selectionActionList(sel: FileNode[]): ContextAction[] {
1410
1425
  const accessLabel = locale.value === 'en' ? 'Share / Permissions' : 'Paylaş / İzinler';
1411
1426
  return [
1412
1427
  { key: 'open', label: t('ctx.open'), icon: '↗', hidden: !single },
1428
+ { key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉', hidden: !single || sel[0]?.type !== 'dir' } /* wiring:d1 — klasörü yeni sekmede aç */,
1413
1429
  { key: 'preview', label: t('ctx.preview'), icon: '👁', hidden: !single, disabled: !isFile },
1414
1430
  { key: 'download', label: t('ctx.download'), icon: '⬇', hidden: !single, disabled: !isFile },
1415
1431
  { key: 'convert', label: t('ctx.convert'), icon: '🔄', hidden: !single || !effectiveConvertUrl.value || !w, disabled: !isFile },
@@ -1535,6 +1551,11 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
1535
1551
  case 'duplicate':
1536
1552
  if (targets[0]) await duplicate(targets[0]);
1537
1553
  break;
1554
+ /* wiring:d1 — sağ-tık "Yeni sekmede aç" */
1555
+ case 'open-tab':
1556
+ if (targets[0]) openNodeInTab(targets[0]);
1557
+ break;
1558
+ /* /wiring:d1 */
1538
1559
  }
1539
1560
  }
1540
1561
 
@@ -1881,11 +1902,38 @@ function onDragLeave() {
1881
1902
  if (dragCounter.value === 0) dragOver.value = false;
1882
1903
  }
1883
1904
  function onDragOver(ev: DragEvent) {
1905
+ /* wiring:d1 — iç sürüklemeler kök gövdeye de bırakılabilir olmalı ki split
1906
+ panelinden ana panelin BOŞLUĞUNA bırakmak çalışsın (origin dragover'da
1907
+ okunamaz — karar drop anında verilir; aynı-klasör drop'u no-op kalır). */
1908
+ if (ev.dataTransfer?.types.includes(FE_DND_MIME)) {
1909
+ ev.preventDefault();
1910
+ return;
1911
+ }
1912
+ /* /wiring:d1 */
1884
1913
  if (isExternalFileDrag(ev)) {
1885
1914
  ev.preventDefault();
1886
1915
  }
1887
1916
  }
1888
1917
  function onDropUpload(ev: DragEvent) {
1918
+ /* wiring:d1 — split panelinden ana panelin boşluğuna bırakma = geçerli
1919
+ klasöre aktar (aynı klasörden gelenler no-op, eski davranış korunur). */
1920
+ if (ev.dataTransfer?.types.includes(FE_DND_MIME)) {
1921
+ const d1Origin = ev.dataTransfer.getData(FE_DND_SRC_MIME) || '';
1922
+ const d1Here = qualify(currentPath.value);
1923
+ if (d1Origin && d1Here && d1Origin !== d1Here && !trashMode.value && canWriteHere.value) {
1924
+ ev.preventDefault();
1925
+ dragCounter.value = 0;
1926
+ dragOver.value = false;
1927
+ try {
1928
+ const d1Items = JSON.parse(ev.dataTransfer.getData(FE_DND_MIME)) as Array<{ path: string }>;
1929
+ void transferItems(d1Items.map((i) => i.path), d1Here, d1Origin);
1930
+ } catch {
1931
+ /* bozuk payload — yok say */
1932
+ }
1933
+ return;
1934
+ }
1935
+ }
1936
+ /* /wiring:d1 */
1889
1937
  // Internal row drag — nothing to do here, the row drop handler
1890
1938
  // in GridView/ListView already resolved the move.
1891
1939
  if (ev.dataTransfer?.types.includes(FE_DND_MIME)) {
@@ -1959,13 +2007,14 @@ function onItemDragStart(node: FileNode, ev: DragEvent) {
1959
2007
  .filter((n) => n.basename !== '.trash')
1960
2008
  .map((n) => ({ path: n.path, basename: n.basename, type: n.type })); // qualified
1961
2009
  ev.dataTransfer.setData(FE_DND_MIME, JSON.stringify(items));
2010
+ ev.dataTransfer.setData(FE_DND_SRC_MIME, qualify(currentPath.value)); /* wiring:d1 — paneller arası origin damgası */
1962
2011
  ev.dataTransfer.setData('text/plain', items.map((i) => i.path).join('\n'));
1963
2012
  ev.dataTransfer.effectAllowed = 'move';
1964
2013
  }
1965
2014
 
1966
- async function moveSourcesAsync(sources: string[], targetDir: string, opLabel: string): Promise<void> {
2015
+ async function moveSourcesAsync(sources: string[], targetDir: string, opLabel: string, originOverride?: string): Promise<void> {
1967
2016
  try {
1968
- const originWire = qualify(currentPath.value);
2017
+ const originWire = originOverride ?? qualify(currentPath.value); /* wiring:d1 — split panelinden gelen sürüklemede gerçek kaynak klasör */
1969
2018
  if (api.endpoints.moveAsync) {
1970
2019
  const { op } = await api.moveAsync(sources, targetDir, originWire);
1971
2020
  registerMoveUndo(op.id, sources, targetDir, originWire);
@@ -2006,7 +2055,7 @@ async function onItemDropInto(target: FileNode, ev: DragEvent) {
2006
2055
  flashToast('Aynı klasöre taşınamaz');
2007
2056
  return;
2008
2057
  }
2009
- await moveSourcesAsync(sources, targetDir, 'move-dnd');
2058
+ await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
2010
2059
  }
2011
2060
 
2012
2061
  async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
@@ -2025,7 +2074,7 @@ async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
2025
2074
  .map((i) => i.path)
2026
2075
  .filter((p) => p && p !== targetDir && !targetDir.startsWith(p + '/'));
2027
2076
  if (sources.length === 0) return;
2028
- await moveSourcesAsync(sources, targetDir, 'move-dnd-crumb');
2077
+ await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
2029
2078
  }
2030
2079
 
2031
2080
  function onCancelUpload(job: UploadJob) {
@@ -2245,6 +2294,244 @@ onBeforeUnmount(() => {
2245
2294
  rootEl.value?.removeEventListener('fe:tour-restart', onTourRestartEvent);
2246
2295
  });
2247
2296
  /* === /wiring:c4 === */
2297
+
2298
+ /* === wiring:d1 — sekmeler (tab şeridi) + tab başına split ===
2299
+ *
2300
+ * useTabs, mevcut konum state'inin (currentPath/viewMode) ÜSTÜNDE bir
2301
+ * katmandır: aktif tab gezinmeleri watch ile dinleyip snapshot'ını
2302
+ * günceller; tab geçişi mevcut load(path) yolunu çağırır — yeni fetch
2303
+ * mantığı yok. Şerit tek sekmede hiç render edilmez (embed pixel-aynı).
2304
+ *
2305
+ * Persist: `filex.tabs` — pathPersist scope mantığı izlenir: mode 'none'
2306
+ * ise persist kapalı; rootPath confine'ı anahtara eklenir ki farklı
2307
+ * confine'lı embed'ler birbirinin sekmelerini ezmesin.
2308
+ */
2309
+ const FE_DND_SRC_MIME = 'application/x-brf-files-src';
2310
+
2311
+ const TABS_LS_BASE = 'filex.tabs';
2312
+ function tabsStorageKey(): string | null {
2313
+ if (persistMode() === 'none') return null;
2314
+ return rootPathProp ? `${TABS_LS_BASE}:${rootPathProp}` : TABS_LS_BASE;
2315
+ }
2316
+ const tabsApi = useTabs({ storageKey: tabsStorageKey() });
2317
+ const tabsRestored = tabsApi.restore();
2318
+ const tabsActiveId = tabsApi.activeId;
2319
+
2320
+ const tabsVisible = computed(() => tabsApi.hasMultiple.value);
2321
+ const activeSplit = computed(() => tabsApi.activeTab.value?.split ?? null);
2322
+
2323
+ // Sekme adı OTOMATİK = güncel klasör adı (kök = depo adı / kök etiketi).
2324
+ function tabLabel(path: string): string {
2325
+ const p = (path || '').replace(/^\/+|\/+$/g, '');
2326
+ if (p === '.trash') return t('node.trash');
2327
+ if (!p) return multiStorageRoot.value ? t('breadcrumb.root') : adapter.value || t('breadcrumb.root');
2328
+ return p.split('/').pop() || p;
2329
+ }
2330
+ const tabItems = computed(() =>
2331
+ tabsApi.tabs.value.map((tb) => ({ id: tb.id, label: tabLabel(tb.path), split: !!tb.split })),
2332
+ );
2333
+
2334
+ // Aktif tab kullanıcıyı izler: gezinme + görünüm değişimi snapshot'a yazılır.
2335
+ watch(currentPath, (p) => tabsApi.syncActive({ path: p }));
2336
+ watch(viewMode, (v) => tabsApi.syncActive({ viewMode: v }));
2337
+
2338
+ // İlk sekme, ilk konum belli olur olmaz tohumlanır (restore varsa dokunma —
2339
+ // aktif snapshot ilk load sonrası currentPath watcher'ıyla zaten senkronlanır).
2340
+ onMounted(() => {
2341
+ if (!tabsRestored) tabsApi.seed(currentPath.value ?? '', viewMode.value);
2342
+ });
2343
+
2344
+ function applyTabLocation(tb: TabState) {
2345
+ if (tb.viewMode && tb.viewMode !== viewMode.value) viewMode.value = tb.viewMode;
2346
+ if (tb.path === '.trash') {
2347
+ void loadTrash();
2348
+ return;
2349
+ }
2350
+ void load(tb.path);
2351
+ }
2352
+ function activateTab(id: string) {
2353
+ const tb = tabsApi.activate(id);
2354
+ if (tb) applyTabLocation(tb);
2355
+ }
2356
+ function newTabHere() {
2357
+ // Mevcut konumu klonlar; görünüm zaten oradadır, load gerekmez.
2358
+ tabsApi.openTab(currentPath.value ?? '', { viewMode: viewMode.value, background: false });
2359
+ }
2360
+ function closeTabById(id: string) {
2361
+ const next = tabsApi.closeTab(id);
2362
+ if (next) applyTabLocation(next);
2363
+ }
2364
+ function nextTab() {
2365
+ const tb = tabsApi.step(1);
2366
+ if (tb) applyTabLocation(tb);
2367
+ }
2368
+ function prevTab() {
2369
+ const tb = tabsApi.step(-1);
2370
+ if (tb) applyTabLocation(tb);
2371
+ }
2372
+
2373
+ /** Bir klasörü ARKA PLANDA yeni sekmede aç (orta-tık / sağ-tık / palet). */
2374
+ function openNodeInTab(n: FileNode) {
2375
+ if (n.type !== 'dir' || n.basename === '.trash') return;
2376
+ const target = multiStorageRoot.value ? wireToVirtual(n.path) : stripAdapter(n.path);
2377
+ tabsApi.openTab(target, { viewMode: viewMode.value, background: true });
2378
+ }
2379
+
2380
+ // Orta-tık delegasyonu: ListView/GridView satırları data-fe-path taşır; kendi
2381
+ // keydown/emit zinciri eklemek yerine kökte tek auxclick dinleyicisi yeter.
2382
+ // (SecondaryPane kendi satırlarında stopPropagation ile kendisi halleder.)
2383
+ function onListAuxClick(ev: MouseEvent) {
2384
+ if (ev.button !== 1) return;
2385
+ const host = ev.target as HTMLElement | null;
2386
+ const el = host && typeof host.closest === 'function' ? host.closest('[data-fe-path]') : null;
2387
+ const p = el?.getAttribute('data-fe-path');
2388
+ if (!p) return;
2389
+ const node = files.value.find((f) => f.path === p);
2390
+ if (!node || node.type !== 'dir' || node.basename === '.trash') return;
2391
+ ev.preventDefault();
2392
+ openNodeInTab(node);
2393
+ }
2394
+ // Orta-tuş mousedown'ı satırlar üzerinde iptal: scroll'lu gövdede Chromium'un
2395
+ // autoscroll'u devreye girer ve auxclick HİÇ üretilmez (canlı teşhis) —
2396
+ // preventDefault autoscroll'u bastırır, auxclick yeniden akar.
2397
+ function onListMiddleDown(ev: MouseEvent) {
2398
+ if (ev.button !== 1) return;
2399
+ const host = ev.target as HTMLElement | null;
2400
+ if (host && typeof host.closest === 'function' && host.closest('[data-fe-path]')) {
2401
+ ev.preventDefault();
2402
+ }
2403
+ }
2404
+ onMounted(() => {
2405
+ rootEl.value?.addEventListener('auxclick', onListAuxClick);
2406
+ rootEl.value?.addEventListener('mousedown', onListMiddleDown);
2407
+ });
2408
+ onBeforeUnmount(() => {
2409
+ rootEl.value?.removeEventListener('auxclick', onListAuxClick);
2410
+ rootEl.value?.removeEventListener('mousedown', onListMiddleDown);
2411
+ });
2412
+
2413
+ // ---- split (tab başına ikincil panel) ------------------------------
2414
+
2415
+ const splitPaneRef = ref<InstanceType<typeof SecondaryPane> | null>(null);
2416
+ // Dar modda split devre dışı (state korunur, genişleyince geri gelir).
2417
+ const splitVisible = computed(() => !!activeSplit.value && !isNarrow.value);
2418
+
2419
+ function toggleSplit() {
2420
+ if (activeSplit.value) {
2421
+ tabsApi.setSplit(null);
2422
+ activePane.value = 'main';
2423
+ return;
2424
+ }
2425
+ if (isNarrow.value) return;
2426
+ tabsApi.setSplit({ path: currentPath.value ?? '' });
2427
+ }
2428
+ function closeSplit() {
2429
+ tabsApi.setSplit(null);
2430
+ activePane.value = 'main';
2431
+ }
2432
+ function onPaneNavigate(p: string) {
2433
+ tabsApi.setSplit({ path: p });
2434
+ }
2435
+
2436
+ // Aktif panel: kısayollar aktif panele gider; panel tıklamayla aktifleşir.
2437
+ const activePane = ref<'main' | 'split'>('main');
2438
+ function setPaneMain() {
2439
+ activePane.value = 'main';
2440
+ }
2441
+ watch(splitVisible, (v) => {
2442
+ if (!v) activePane.value = 'main';
2443
+ });
2444
+ const paneIsActive = computed(() => activePane.value === 'split' && splitVisible.value);
2445
+ const mainPaneFocus = computed(() => splitVisible.value && activePane.value === 'main');
2446
+
2447
+ // Pane yardımcıları — hep ana panelin mevcut dönüştürücülerini sarar.
2448
+ function paneToUser(wire: string): string {
2449
+ return multiStorageRoot.value ? wireToVirtual(wire) : stripAdapter(wire);
2450
+ }
2451
+ function paneClamp(p: string): string {
2452
+ const clean = String(p ?? '').replace(/^\/+|\/+$/g, '');
2453
+ if (!rootFloor) return clean;
2454
+ if (!clean || !(clean === rootFloor || clean.startsWith(rootFloor + '/'))) return rootFloor;
2455
+ return clean;
2456
+ }
2457
+
2458
+ // Pano (clipboard) aktif panele göre: kes/kopyala pane seçiminden beslenir,
2459
+ // yapıştır pane klasörüne iner. State ana panelinkiyle ORTAK — panolar arası
2460
+ // kes-yapıştır bedavaya çalışır.
2461
+ function paneCut() {
2462
+ const nodes = splitPaneRef.value?.selectedNodes() ?? [];
2463
+ if (nodes.length === 0) return;
2464
+ clipboard.value = { mode: 'cut', items: nodes, sourcePath: splitPaneRef.value?.getPath() ?? '' };
2465
+ flashToast('Kesildi');
2466
+ }
2467
+ function paneCopy() {
2468
+ const nodes = splitPaneRef.value?.selectedNodes() ?? [];
2469
+ if (nodes.length === 0) return;
2470
+ clipboard.value = { mode: 'copy', items: nodes, sourcePath: splitPaneRef.value?.getPath() ?? '' };
2471
+ flashToast('Kopyalandı');
2472
+ }
2473
+ async function panePaste() {
2474
+ const cb = clipboard.value;
2475
+ const pane = splitPaneRef.value;
2476
+ if (!cb.mode || cb.items.length === 0 || !pane) return;
2477
+ const targetWire = qualify(pane.getPath() ?? '');
2478
+ const originWire = qualify(cb.sourcePath || '') || undefined;
2479
+ if (cb.mode === 'cut' && originWire === targetWire) {
2480
+ flashToast('Aynı klasöre kesilemez');
2481
+ return;
2482
+ }
2483
+ await transferItems(cb.items.map((n) => n.path), targetWire, originWire, cb.mode === 'copy');
2484
+ clipboard.value = { mode: null, items: [], sourcePath: null };
2485
+ }
2486
+
2487
+ // ---- paneller arası aktarım ----------------------------------------
2488
+
2489
+ function wireAdapterOf(p: string): string {
2490
+ const i = p.indexOf('://');
2491
+ return i === -1 ? '' : p.slice(0, i);
2492
+ }
2493
+ function dndOrigin(ev: DragEvent): string | undefined {
2494
+ const v = ev.dataTransfer?.getData(FE_DND_SRC_MIME);
2495
+ return v || undefined;
2496
+ }
2497
+
2498
+ /**
2499
+ * transferItems — panel-arası / pano aktarımının tek kapısı.
2500
+ * Aynı depo → TAŞI (mevcut moveSourcesAsync yolu: kuyruk + geri al).
2501
+ * Farklı depo → KOPYALA dene; backend cross-storage desteklemiyorsa
2502
+ * i18n'li hata toast'ı. Bitince ikincil panel de tazelenir (ana panel
2503
+ * moveSourcesAsync / pendingOps onSettled üzerinden zaten tazelenir).
2504
+ */
2505
+ async function transferItems(
2506
+ sources: string[],
2507
+ targetWire: string,
2508
+ originWire?: string,
2509
+ forceCopy = false,
2510
+ ): Promise<void> {
2511
+ const list = sources.filter((p) => p && p !== targetWire && !targetWire.startsWith(p + '/'));
2512
+ if (list.length === 0 || !targetWire) return;
2513
+ const targetAdapter = wireAdapterOf(targetWire);
2514
+ const cross = list.some((p) => wireAdapterOf(p) !== targetAdapter);
2515
+ if (cross || forceCopy) {
2516
+ try {
2517
+ const { op } = await api.copy(list, targetWire);
2518
+ pendingOps.register(op);
2519
+ flashToast(cross ? t('split.cross_copy') : t('split.copy_queued'));
2520
+ } catch (err) {
2521
+ emit('error', { message: (err as Error).message, context: { op: 'transfer', targetWire } });
2522
+ flashToast(cross ? t('split.cross_failed') : (err as Error).message);
2523
+ return;
2524
+ }
2525
+ } else {
2526
+ await moveSourcesAsync(list, targetWire, 'move-transfer', originWire);
2527
+ }
2528
+ void splitPaneRef.value?.reload();
2529
+ }
2530
+
2531
+ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?: string }) {
2532
+ void transferItems(p.sources, p.targetWire, p.originWire);
2533
+ }
2534
+ /* === /wiring:d1 === */
2248
2535
  </script>
2249
2536
 
2250
2537
  <template>
@@ -2265,6 +2552,21 @@ onBeforeUnmount(() => {
2265
2552
  @drop="onDropUpload"
2266
2553
  @contextmenu="onContextCanvas"
2267
2554
  >
2555
+ <!-- wiring:d1 — sekme şeridi: TEK sekmede hiç render edilmez (embed pixel-aynı) -->
2556
+ <TabBar
2557
+ v-if="tabsVisible"
2558
+ :tabs="tabItems"
2559
+ :active-id="tabsActiveId"
2560
+ :locale="locale"
2561
+ :split-enabled="!isNarrow"
2562
+ :split-active="!!activeSplit"
2563
+ @select="activateTab"
2564
+ @close="closeTabById"
2565
+ @new="newTabHere"
2566
+ @reorder="(from: number, to: number) => tabsApi.move(from, to)"
2567
+ @toggle-split="toggleSplit"
2568
+ />
2569
+ <!-- /wiring:d1 -->
2268
2570
  <Toolbar
2269
2571
  ref="toolbarRef"
2270
2572
  :view-mode="viewMode"
@@ -2328,14 +2630,19 @@ onBeforeUnmount(() => {
2328
2630
  <!-- koru:k1 — fe__main lays the listing body and the inspector panel out
2329
2631
  as flex siblings (row). Without the inspector open it is visually
2330
2632
  identical to the previous direct-child fe__body. -->
2331
- <div class="fe__main">
2332
- <div class="fe__body" @click.self="selection.clear()">
2633
+ <div class="fe__main" :class="{ 'fe__main--split': splitVisible } /* wiring:d1 */">
2634
+ <div
2635
+ class="fe__body"
2636
+ :class="{ 'fe-pane--focus': mainPaneFocus } /* wiring:d1 — aktif panel vurgusu */"
2637
+ @pointerdown.capture="setPaneMain() /* wiring:d1 */"
2638
+ @click.self="selection.clear()"
2639
+ >
2333
2640
  <!-- Initial load: skeleton ghosts (view-mode aware) instead of an
2334
2641
  empty/"no files" flash. Only when there's nothing yet — navigation
2335
2642
  keeps the current list, exactly as before. -->
2336
2643
  <div v-if="loading && files.length === 0" class="fe__skeleton" role="status">
2337
2644
  <span class="fe-sr-only">{{ t('loading') }}</span>
2338
- <div v-if="viewMode === 'grid'" class="fe-skel-grid" aria-hidden="true">
2645
+ <div v-if="viewMode !== 'list' /* wiring:d2 — galeri de grid iskeletini kullanır */" class="fe-skel-grid" aria-hidden="true">
2339
2646
  <div v-for="i in 8" :key="i" class="fe-skel-card">
2340
2647
  <div class="fe-skel fe-skel--thumb"></div>
2341
2648
  <div class="fe-skel fe-skel--label"></div>
@@ -2504,6 +2811,22 @@ onBeforeUnmount(() => {
2504
2811
  @star-change="onStarChange"
2505
2812
  />
2506
2813
  <GridView
2814
+ v-else-if="viewMode === 'grid' /* wiring:d2 — v-else → v-else-if (3. mod eklendi) */"
2815
+ :files="files"
2816
+ :selected="selection.selected.value"
2817
+ :clipped="clippedPaths"
2818
+ :show-parent-path="!!searchQuery"
2819
+ :locale="locale"
2820
+ :loading="loading"
2821
+ :thumb-src="thumbs.src"
2822
+ @click-card="(n, m) => selection.click(n.path, m)"
2823
+ @dbl-card="openNode"
2824
+ @context-card="onContextTarget"
2825
+ @item-drag-start="onItemDragStart"
2826
+ @item-drop-into="onItemDropInto"
2827
+ />
2828
+ <!-- wiring:d2 — galeri görünümü (GridView ile aynı event sözleşmesi) -->
2829
+ <GalleryView
2507
2830
  v-else
2508
2831
  :files="files"
2509
2832
  :selected="selection.selected.value"
@@ -2518,8 +2841,35 @@ onBeforeUnmount(() => {
2518
2841
  @item-drag-start="onItemDragStart"
2519
2842
  @item-drop-into="onItemDropInto"
2520
2843
  />
2844
+ <!-- /wiring:d2 -->
2521
2845
  </div>
2522
2846
 
2847
+ <!-- wiring:d1 — tab başına split: sağ ikincil panel (dar modda kapalı).
2848
+ :key tab kimliğine bağlı — tab geçişinde pane kendi konumuyla temiz
2849
+ remount olur. -->
2850
+ <SecondaryPane
2851
+ v-if="splitVisible && activeSplit"
2852
+ ref="splitPaneRef"
2853
+ :key="'split-' + tabsActiveId"
2854
+ :api="api"
2855
+ :initial-path="activeSplit.path"
2856
+ :locale="locale"
2857
+ :qualify="qualify"
2858
+ :to-user="paneToUser"
2859
+ :clamp="paneClamp"
2860
+ :root-label="multiStorageRoot ? '/' : adapter || t('breadcrumb.root')"
2861
+ :floor="rootFloor"
2862
+ :multi-root="multiStorageRoot"
2863
+ :virtual-rows="virtualStorageRows"
2864
+ :active="paneIsActive"
2865
+ @navigate="onPaneNavigate"
2866
+ @activate="activePane = 'split'"
2867
+ @close="closeSplit"
2868
+ @open-tab="(p: string) => tabsApi.openTab(p, { viewMode: viewMode, background: true })"
2869
+ @transfer="onPaneTransfer"
2870
+ />
2871
+ <!-- /wiring:d1 -->
2872
+
2523
2873
  <!-- koru:k1 — inspector (details) panel; v-if keeps the closed state
2524
2874
  free of any DOM. Narrow mode renders it as a full-size overlay. -->
2525
2875
  <InspectorPanel
@@ -2746,13 +3096,16 @@ onBeforeUnmount(() => {
2746
3096
  @navigate="(p: string) => load(p)"
2747
3097
  @new-folder="showNewFolder = true"
2748
3098
  @upload="triggerUpload"
2749
- @toggle-view="viewMode = viewMode === 'list' ? 'grid' : 'list'"
3099
+ @toggle-view="viewMode = viewMode === 'list' ? 'grid' : viewMode === 'grid' ? 'gallery' : 'list' /* wiring:d2 — 3 mod döngüsü */"
2750
3100
  @open-trash="loadTrash"
2751
3101
  @refresh="() => load()"
2752
3102
  @go-up="goUp"
2753
3103
  @open-theme="showThemeGallery = true /* wiring:int */"
2754
3104
  @open-shortcut-settings="showShortcutSettings = true /* wiring:int */"
2755
3105
  @start-tour="startTour() /* wiring:int */"
3106
+ :split-enabled="!isNarrow /* wiring:d1 */"
3107
+ @tab-new="newTabHere() /* wiring:d1 */"
3108
+ @split-toggle="toggleSplit() /* wiring:d1 */"
2756
3109
  />
2757
3110
  <ShortcutsHelp
2758
3111
  :open="showShortcutsHelp"
@@ -39,6 +39,8 @@ const props = defineProps<{
39
39
  canGoUp?: boolean;
40
40
  /* bul:s3 — global search callback; absent = older host, group hidden. */
41
41
  globalSearch?: (q: string) => Promise<GlobalSearchHit[]>;
42
+ /* wiring:d1 — false hides the split command (narrow embeds). */
43
+ splitEnabled?: boolean;
42
44
  }>();
43
45
 
44
46
  const emit = defineEmits<{
@@ -57,6 +59,9 @@ const emit = defineEmits<{
57
59
  (e: 'open-theme'): void;
58
60
  (e: 'open-shortcut-settings'): void;
59
61
  (e: 'start-tour'): void;
62
+ /* wiring:d1 — tab / split commands */
63
+ (e: 'tab-new'): void;
64
+ (e: 'split-toggle'): void;
60
65
  }>();
61
66
 
62
67
  const { t, nodeDisplayName } = useLocale(() => props.locale);
@@ -126,7 +131,7 @@ const commandItems = computed<PaletteItem[]>(() => {
126
131
  const defs: Array<{ command: string; label: string; icon: string; enabled: boolean }> = [
127
132
  { command: 'new-folder', label: t('toolbar.new_folder'), icon: '📁', enabled: props.canWrite !== false },
128
133
  { command: 'upload', label: t('toolbar.upload'), icon: '⬆', enabled: props.canWrite !== false },
129
- { command: 'toggle-view', label: t('cmd.view_toggle'), icon: props.viewMode === 'list' ? '▦' : '☰', enabled: true },
134
+ { command: 'toggle-view', label: t('cmd.view_toggle'), icon: props.viewMode === 'list' ? '▦' : props.viewMode === 'grid' ? '▣' : '☰', enabled: true /* wiring:d2 — sıradaki mod ikonu (list→grid→galeri→list) */ },
130
135
  { command: 'open-trash', label: t('cmd.trash'), icon: '🗑', enabled: true },
131
136
  { command: 'refresh', label: t('toolbar.refresh'), icon: '⟳', enabled: true },
132
137
  { command: 'go-up', label: t('toolbar.go_up'), icon: '↑', enabled: props.canGoUp !== false },
@@ -134,6 +139,9 @@ const commandItems = computed<PaletteItem[]>(() => {
134
139
  { command: 'open-theme', label: t('theme.menu'), icon: '🎨', enabled: true },
135
140
  { command: 'open-shortcut-settings', label: t('shortcuts.settings.menu'), icon: '⌨', enabled: true },
136
141
  { command: 'start-tour', label: t('tour.restart'), icon: '🎓', enabled: true },
142
+ /* wiring:d1 — sekme + split */
143
+ { command: 'tab-new', label: t('cmd.tab_new'), icon: '⧉', enabled: true },
144
+ { command: 'split-toggle', label: t('cmd.split_toggle'), icon: '◫', enabled: props.splitEnabled !== false },
137
145
  ];
138
146
  return defs
139
147
  .filter((d) => d.enabled && matchScore(d.label, q) >= 0)
@@ -322,6 +330,9 @@ function choose(item?: PaletteItem) {
322
330
  case 'open-theme': emit('open-theme'); break;
323
331
  case 'open-shortcut-settings': emit('open-shortcut-settings'); break;
324
332
  case 'start-tour': emit('start-tour'); break;
333
+ /* wiring:d1 */
334
+ case 'tab-new': emit('tab-new'); break;
335
+ case 'split-toggle': emit('split-toggle'); break;
325
336
  }
326
337
  }
327
338
  }