@brftech/filex-core 0.4.2 → 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 (58) hide show
  1. package/dist/{ArchiveViewer-B_0IQExT.js → ArchiveViewer-EGgNqLPE.js} +2 -2
  2. package/dist/{ArchiveViewer-B_0IQExT.js.map → ArchiveViewer-EGgNqLPE.js.map} +1 -1
  3. package/dist/{CsvViewer-maMfUy5i.js → CsvViewer-BvriJ42M.js} +2 -2
  4. package/dist/{CsvViewer-maMfUy5i.js.map → CsvViewer-BvriJ42M.js.map} +1 -1
  5. package/dist/{DrawioViewer-DP_pXI00.js → DrawioViewer-B_bSPsWX.js} +2 -2
  6. package/dist/{DrawioViewer-DP_pXI00.js.map → DrawioViewer-B_bSPsWX.js.map} +1 -1
  7. package/dist/{EpubViewer-DSaqGnVx.js → EpubViewer-hl68h4gG.js} +2 -2
  8. package/dist/{EpubViewer-DSaqGnVx.js.map → EpubViewer-hl68h4gG.js.map} +1 -1
  9. package/dist/{IpynbViewer-BmdBanF1.js → IpynbViewer-lhwOjXK7.js} +2 -2
  10. package/dist/{IpynbViewer-BmdBanF1.js.map → IpynbViewer-lhwOjXK7.js.map} +1 -1
  11. package/dist/{MermaidViewer-Drlkl0Z5.js → MermaidViewer-H2Z6AYXg.js} +2 -2
  12. package/dist/{MermaidViewer-Drlkl0Z5.js.map → MermaidViewer-H2Z6AYXg.js.map} +1 -1
  13. package/dist/{PsdViewer-gftUJrsm.js → PsdViewer-Bsk-11yH.js} +2 -2
  14. package/dist/{PsdViewer-gftUJrsm.js.map → PsdViewer-Bsk-11yH.js.map} +1 -1
  15. package/dist/{TiffViewer-0Uv0-Lxh.js → TiffViewer-D-j9vycG.js} +2 -2
  16. package/dist/{TiffViewer-0Uv0-Lxh.js.map → TiffViewer-D-j9vycG.js.map} +1 -1
  17. package/dist/{Viewer3D-Gu3kUpxV.js → Viewer3D-Bcr70rTe.js} +2 -2
  18. package/dist/{Viewer3D-Gu3kUpxV.js.map → Viewer3D-Bcr70rTe.js.map} +1 -1
  19. package/dist/filex-core.js +50 -25
  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 +597 -4
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +636 -15
  28. package/src/components/CommandPalette.vue +24 -1
  29. package/src/components/ContextMenu.vue +114 -8
  30. package/src/components/GalleryView.vue +214 -0
  31. package/src/components/GridView.vue +34 -1
  32. package/src/components/InspectorPanel.vue +170 -0
  33. package/src/components/ListView.vue +40 -6
  34. package/src/components/OnboardingTour.vue +332 -0
  35. package/src/components/OperationsCenter.vue +284 -0
  36. package/src/components/PendingOpsTray.vue +50 -66
  37. package/src/components/QuickLook.vue +142 -0
  38. package/src/components/SecondaryPane.vue +411 -0
  39. package/src/components/ShortcutSettings.vue +270 -0
  40. package/src/components/ShortcutsHelp.vue +30 -9
  41. package/src/components/TabBar.vue +174 -0
  42. package/src/components/ThemeGallery.vue +253 -0
  43. package/src/components/Toolbar.vue +75 -11
  44. package/src/components/UploadProgress.vue +49 -57
  45. package/src/composables/useFileApi.ts +53 -0
  46. package/src/composables/useKeyboardShortcuts.ts +340 -137
  47. package/src/composables/useOperations.ts +291 -0
  48. package/src/composables/useTabs.ts +222 -0
  49. package/src/index.ts +51 -0
  50. package/src/lib/dragGhost.ts +41 -0
  51. package/src/lib/themes.ts +505 -0
  52. package/src/locales/en.ts +125 -0
  53. package/src/locales/tr.ts +125 -0
  54. package/src/modals/Modal.vue +41 -5
  55. package/src/styles/base.css +1210 -0
  56. package/src/types/FileNode.ts +1 -1
  57. package/dist/index-BLocdbmc.js +0 -7993
  58. package/dist/index-BLocdbmc.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';
@@ -50,6 +51,26 @@ import InspectorPanel from './components/InspectorPanel.vue'; /* koru:k1 */
50
51
  import CommandPalette from './components/CommandPalette.vue';
51
52
  import ShortcutsHelp from './components/ShortcutsHelp.vue';
52
53
  /* /cila:c wiring */
54
+ /* wiring:c1 — tema galerisi */
55
+ import ThemeGallery from './components/ThemeGallery.vue';
56
+ import { useThemeState, applyThemeToEl, syncThemeStyle } from './lib/themes';
57
+ /* /wiring:c1 */
58
+ /* wiring:c2 — shortcut settings modal + Space quick-look overlay */
59
+ import ShortcutSettings from './components/ShortcutSettings.vue';
60
+ import QuickLook from './components/QuickLook.vue';
61
+ /* /wiring:c2 */
62
+ /* wiring:c3 — unified operations center */
63
+ import OperationsCenter from './components/OperationsCenter.vue';
64
+ import { useOperations } from './composables/useOperations';
65
+ /* /wiring:c3 */
66
+ /* wiring:c4 */
67
+ import OnboardingTour from './components/OnboardingTour.vue';
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 */
53
74
 
54
75
  import NewFolderModal from './modals/NewFolderModal.vue';
55
76
  import RenameModal from './modals/RenameModal.vue';
@@ -152,6 +173,7 @@ const pendingOps = usePendingOps(props.config, api, {
152
173
  flashToast(`${verb} (${op.progress_total})`);
153
174
  }
154
175
  void load();
176
+ void splitPaneRef.value?.reload(); /* wiring:d1 — ikincil panel de tazelenir */
155
177
  },
156
178
  });
157
179
 
@@ -191,7 +213,7 @@ const viewMode = customRef<ViewMode>((track, trigger) => {
191
213
  let value: ViewMode = (() => {
192
214
  try {
193
215
  const stored = localStorage.getItem(VIEW_MODE_KEY);
194
- if (stored === 'list' || stored === 'grid') return stored;
216
+ if (stored === 'list' || stored === 'grid' || stored === 'gallery') return stored; /* wiring:d2 */
195
217
  } catch {
196
218
  /* private mode */
197
219
  }
@@ -1069,8 +1091,9 @@ useKeyboardShortcuts(rootEl, {
1069
1091
  showRename.value = true;
1070
1092
  }
1071
1093
  },
1072
- onSelectAll: () => selection.selectAll(),
1094
+ onSelectAll: () => (paneIsActive.value ? splitPaneRef.value?.selectAll() : selection.selectAll()) /* wiring:d1 pane-route */,
1073
1095
  onOpen: () => {
1096
+ if (paneIsActive.value) return splitPaneRef.value?.openSelected(); /* wiring:d1 pane-route */
1074
1097
  const n = selection.nodes.value[0];
1075
1098
  if (n) openNode(n);
1076
1099
  },
@@ -1087,10 +1110,10 @@ useKeyboardShortcuts(rootEl, {
1087
1110
  if (isNarrow.value) closeInspector();
1088
1111
  },
1089
1112
  onFocusSearch: () => toolbarRef.value?.focusSearch(),
1090
- onCut: () => cut(),
1091
- onCopy: () => copyToClipboard(),
1092
- onPaste: () => paste(),
1093
- 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 */,
1094
1117
  /* cila:c wiring */
1095
1118
  onPathJump: () => {
1096
1119
  showPalette.value = true;
@@ -1099,7 +1122,14 @@ useKeyboardShortcuts(rootEl, {
1099
1122
  showShortcutsHelp.value = true;
1100
1123
  },
1101
1124
  /* /cila:c wiring */
1125
+ onQuickLook: () => quickLookToggle() /* wiring:c2 */,
1102
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 */
1103
1133
  hasSelection: () => !selection.isEmpty.value,
1104
1134
  });
1105
1135
 
@@ -1354,6 +1384,7 @@ const contextActions = computed<ContextAction[]>(() => {
1354
1384
  if (!single) return [];
1355
1385
  return [
1356
1386
  { key: 'open', label: t('ctx.open'), icon: '↗' },
1387
+ { key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉' } /* wiring:d1 */,
1357
1388
  ];
1358
1389
  }
1359
1390
 
@@ -1394,6 +1425,7 @@ function selectionActionList(sel: FileNode[]): ContextAction[] {
1394
1425
  const accessLabel = locale.value === 'en' ? 'Share / Permissions' : 'Paylaş / İzinler';
1395
1426
  return [
1396
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ç */,
1397
1429
  { key: 'preview', label: t('ctx.preview'), icon: '👁', hidden: !single, disabled: !isFile },
1398
1430
  { key: 'download', label: t('ctx.download'), icon: '⬇', hidden: !single, disabled: !isFile },
1399
1431
  { key: 'convert', label: t('ctx.convert'), icon: '🔄', hidden: !single || !effectiveConvertUrl.value || !w, disabled: !isFile },
@@ -1519,6 +1551,11 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
1519
1551
  case 'duplicate':
1520
1552
  if (targets[0]) await duplicate(targets[0]);
1521
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 */
1522
1559
  }
1523
1560
  }
1524
1561
 
@@ -1865,11 +1902,38 @@ function onDragLeave() {
1865
1902
  if (dragCounter.value === 0) dragOver.value = false;
1866
1903
  }
1867
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 */
1868
1913
  if (isExternalFileDrag(ev)) {
1869
1914
  ev.preventDefault();
1870
1915
  }
1871
1916
  }
1872
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 */
1873
1937
  // Internal row drag — nothing to do here, the row drop handler
1874
1938
  // in GridView/ListView already resolved the move.
1875
1939
  if (ev.dataTransfer?.types.includes(FE_DND_MIME)) {
@@ -1943,13 +2007,14 @@ function onItemDragStart(node: FileNode, ev: DragEvent) {
1943
2007
  .filter((n) => n.basename !== '.trash')
1944
2008
  .map((n) => ({ path: n.path, basename: n.basename, type: n.type })); // qualified
1945
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ı */
1946
2011
  ev.dataTransfer.setData('text/plain', items.map((i) => i.path).join('\n'));
1947
2012
  ev.dataTransfer.effectAllowed = 'move';
1948
2013
  }
1949
2014
 
1950
- 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> {
1951
2016
  try {
1952
- 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 */
1953
2018
  if (api.endpoints.moveAsync) {
1954
2019
  const { op } = await api.moveAsync(sources, targetDir, originWire);
1955
2020
  registerMoveUndo(op.id, sources, targetDir, originWire);
@@ -1990,7 +2055,7 @@ async function onItemDropInto(target: FileNode, ev: DragEvent) {
1990
2055
  flashToast('Aynı klasöre taşınamaz');
1991
2056
  return;
1992
2057
  }
1993
- await moveSourcesAsync(sources, targetDir, 'move-dnd');
2058
+ await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
1994
2059
  }
1995
2060
 
1996
2061
  async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
@@ -2009,7 +2074,7 @@ async function onCrumbDropInto(adapterPath: string, ev: DragEvent) {
2009
2074
  .map((i) => i.path)
2010
2075
  .filter((p) => p && p !== targetDir && !targetDir.startsWith(p + '/'));
2011
2076
  if (sources.length === 0) return;
2012
- await moveSourcesAsync(sources, targetDir, 'move-dnd-crumb');
2077
+ await transferItems(sources, targetDir, dndOrigin(ev)); /* wiring:d1 — depo-farkında aktarım */
2013
2078
  }
2014
2079
 
2015
2080
  function onCancelUpload(job: UploadJob) {
@@ -2052,6 +2117,421 @@ async function onCopyPath(adapterPath: string) {
2052
2117
  function buildAuthHeaders(extra: Record<string, string> = {}) {
2053
2118
  return api.authHeadersSync({ ...extra });
2054
2119
  }
2120
+
2121
+ /* === wiring:c1 — tema galerisi ===
2122
+ * Selected theme (shared module state, localStorage `filex.theme`) is applied
2123
+ * as inline `--fe-*` variables on the explorer root, resolved to the active
2124
+ * light/dark variant — plus a mirrored injected stylesheet for the surfaces
2125
+ * that re-declare tokens (teleported context menu, modal backdrops). Theme
2126
+ * selection is independent from the light/dark mode: the mode only picks
2127
+ * WHICH variant of the theme paints. */
2128
+ const showThemeGallery = ref(false);
2129
+ const { themeId: activeThemeId, setTheme: setActiveTheme } = useThemeState();
2130
+ // Resolved mode: explicit config wins, otherwise the OS preference — the same
2131
+ // logic variables.css encodes in CSS. The inline root variables beat every
2132
+ // stylesheet rule, so they must track this resolution at runtime.
2133
+ const themeMq =
2134
+ typeof window !== 'undefined' && window.matchMedia
2135
+ ? window.matchMedia('(prefers-color-scheme: dark)')
2136
+ : undefined;
2137
+ const themeOsDark = ref(!!themeMq?.matches);
2138
+ function onThemeMqChange(e: MediaQueryListEvent) {
2139
+ themeOsDark.value = e.matches;
2140
+ }
2141
+ onMounted(() => themeMq?.addEventListener?.('change', onThemeMqChange));
2142
+ onBeforeUnmount(() => themeMq?.removeEventListener?.('change', onThemeMqChange));
2143
+ const themeResolvedDark = computed(
2144
+ () => props.config.theme === 'dark' || (props.config.theme !== 'light' && themeOsDark.value),
2145
+ );
2146
+ watch(
2147
+ [activeThemeId, themeResolvedDark, rootEl],
2148
+ () => {
2149
+ syncThemeStyle(activeThemeId.value);
2150
+ if (rootEl.value) applyThemeToEl(rootEl.value, activeThemeId.value, themeResolvedDark.value);
2151
+ },
2152
+ { immediate: true },
2153
+ );
2154
+ /* === /wiring:c1 === */
2155
+ /* === wiring:c2 — shortcut settings modal + Space quick-look ===
2156
+ * quickLookTarget follows the selection while the peek is open: arrow
2157
+ * keys emitted by QuickLook move the selection here, and the watcher
2158
+ * below syncs the previewed file. Space toggles (registry action
2159
+ * `quicklook`); Enter promotes the peek into the normal open flow. */
2160
+ const showShortcutSettings = ref(false);
2161
+ const quickLookOpen = ref(false);
2162
+ const quickLookTarget = ref<FileNode | null>(null);
2163
+
2164
+ function quickLookToggle() {
2165
+ if (quickLookOpen.value) {
2166
+ quickLookOpen.value = false;
2167
+ return;
2168
+ }
2169
+ const sel = selection.nodes.value;
2170
+ const n = sel.length === 1 ? sel[0] : null;
2171
+ if (!n || n.type !== 'file' || n.basename === '.trash') return;
2172
+ quickLookTarget.value = n;
2173
+ quickLookOpen.value = true;
2174
+ void markRecent(n);
2175
+ }
2176
+
2177
+ function quickLookNav(delta: number) {
2178
+ const onlyFiles = files.value.filter((f) => f.type === 'file');
2179
+ if (onlyFiles.length === 0) return;
2180
+ const cur = quickLookTarget.value;
2181
+ let idx = cur ? onlyFiles.findIndex((f) => f.path === cur.path) : -1;
2182
+ idx = idx === -1 ? (delta > 0 ? 0 : onlyFiles.length - 1) : (idx + delta + onlyFiles.length) % onlyFiles.length;
2183
+ const next = onlyFiles[idx];
2184
+ if (!next || next.path === cur?.path) return;
2185
+ selection.click(next.path);
2186
+ }
2187
+
2188
+ function quickLookOpenFull() {
2189
+ const n = quickLookTarget.value;
2190
+ quickLookOpen.value = false;
2191
+ if (n) openNode(n);
2192
+ }
2193
+
2194
+ watch(
2195
+ () => selection.nodes.value,
2196
+ (nodes) => {
2197
+ if (!quickLookOpen.value) return;
2198
+ const n = nodes.length === 1 && nodes[0].type === 'file' ? nodes[0] : null;
2199
+ if (n && n.path !== quickLookTarget.value?.path) {
2200
+ quickLookTarget.value = n;
2201
+ void markRecent(n);
2202
+ }
2203
+ },
2204
+ );
2205
+ /* === /wiring:c2 === */
2206
+ /* wiring:c3 — operations center store + failed-upload retry */
2207
+ const opsCenter = useOperations();
2208
+
2209
+ /**
2210
+ * Retry a failed upload from the operations center. The failed row is
2211
+ * already retired by the store; re-run the upload against the job's ORIGINAL
2212
+ * target folder (the user may have navigated away since) via the legacy
2213
+ * single-POST path — works for any storage / size, no chunked precondition.
2214
+ */
2215
+ function retryUploadJob(job: UploadJob) {
2216
+ uploadJobs.value = uploadJobs.value.filter((j) => j.id !== job.id);
2217
+ const file = job.file;
2218
+ const target = job.path || qualify(currentPath.value);
2219
+ const id = crypto.randomUUID();
2220
+ uploadJobs.value = [
2221
+ ...uploadJobs.value,
2222
+ { id, file, path: target, totalBytes: file.size, uploadedBytes: 0, percent: 0, status: 'uploading', cancel() {} },
2223
+ ];
2224
+ const patchRetry = (p: Partial<UploadJob>) => {
2225
+ const idx = uploadJobs.value.findIndex((j) => j.id === id);
2226
+ if (idx === -1) return;
2227
+ const next = [...uploadJobs.value];
2228
+ next[idx] = { ...next[idx], ...p };
2229
+ uploadJobs.value = next;
2230
+ };
2231
+ api
2232
+ .uploadMultipart(target, [file], (percent) => {
2233
+ patchRetry({ percent, uploadedBytes: Math.round((percent / 100) * file.size) });
2234
+ emit('upload-progress', { uploadId: id, percent, done: percent >= 100 });
2235
+ })
2236
+ .then(() => {
2237
+ patchRetry({ percent: 100, uploadedBytes: file.size, status: 'done' });
2238
+ emit('upload-progress', { uploadId: id, percent: 100, done: true });
2239
+ void load();
2240
+ })
2241
+ .catch((err: Error) => {
2242
+ patchRetry({ status: 'error', error: err.message });
2243
+ emit('error', { message: err.message, context: { op: 'upload-retry', file: file.name } });
2244
+ });
2245
+ }
2246
+ /* /wiring:c3 */
2247
+ /* === wiring:c4 — onboarding coach-mark tour ===
2248
+ * First mount with no `filex.tourDone` flag auto-starts the tour (short
2249
+ * delay so the listing/toolbar are laid out). Closing it — finished OR
2250
+ * skipped — stamps the flag; "Turu tekrar başlat" re-opens it any time.
2251
+ * Restart arrives as a bubbled `fe:tour-restart` CustomEvent from the
2252
+ * Toolbar overflow menu, so no extra prop/emit threading through the
2253
+ * shared component tags is needed. */
2254
+ const TOUR_LS_KEY = 'filex.tourDone';
2255
+ const showTour = ref(false);
2256
+ let tourTimer: ReturnType<typeof setTimeout> | undefined;
2257
+
2258
+ function tourAlreadyDone(): boolean {
2259
+ try {
2260
+ return localStorage.getItem(TOUR_LS_KEY) === '1';
2261
+ } catch {
2262
+ return true; // no storage → never auto-nag
2263
+ }
2264
+ }
2265
+
2266
+ function startTour() {
2267
+ showTour.value = true;
2268
+ }
2269
+
2270
+ function onTourClose() {
2271
+ showTour.value = false;
2272
+ try {
2273
+ localStorage.setItem(TOUR_LS_KEY, '1');
2274
+ } catch {
2275
+ /* private mode / quota */
2276
+ }
2277
+ rootEl.value?.focus();
2278
+ }
2279
+
2280
+ function onTourRestartEvent() {
2281
+ startTour();
2282
+ }
2283
+
2284
+ onMounted(() => {
2285
+ rootEl.value?.addEventListener('fe:tour-restart', onTourRestartEvent);
2286
+ if (!tourAlreadyDone()) {
2287
+ tourTimer = setTimeout(() => {
2288
+ if (!showTour.value) startTour();
2289
+ }, 900);
2290
+ }
2291
+ });
2292
+ onBeforeUnmount(() => {
2293
+ if (tourTimer) clearTimeout(tourTimer);
2294
+ rootEl.value?.removeEventListener('fe:tour-restart', onTourRestartEvent);
2295
+ });
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 === */
2055
2535
  </script>
2056
2536
 
2057
2537
  <template>
@@ -2072,6 +2552,21 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
2072
2552
  @drop="onDropUpload"
2073
2553
  @contextmenu="onContextCanvas"
2074
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 -->
2075
2570
  <Toolbar
2076
2571
  ref="toolbarRef"
2077
2572
  :view-mode="viewMode"
@@ -2089,9 +2584,11 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
2089
2584
  :theme="config.theme || 'auto' /* bag:b4 */"
2090
2585
  :inspector-open="showInspector /* koru:k1 */"
2091
2586
  @toggle-inspector="toggleInspector /* koru:k1 */"
2587
+ @open-theme="showThemeGallery = true /* wiring:c1 */"
2092
2588
  @update:view-mode="viewMode = $event"
2093
2589
  @update:search-query="searchQuery = $event"
2094
2590
  @update:density="density = $event"
2591
+ @open-shortcut-settings="showShortcutSettings = true /* wiring:c2 */"
2095
2592
  @new-folder="showNewFolder = true"
2096
2593
  @upload="triggerUpload"
2097
2594
  @refresh="() => load()"
@@ -2133,14 +2630,19 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
2133
2630
  <!-- koru:k1 — fe__main lays the listing body and the inspector panel out
2134
2631
  as flex siblings (row). Without the inspector open it is visually
2135
2632
  identical to the previous direct-child fe__body. -->
2136
- <div class="fe__main">
2137
- <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
+ >
2138
2640
  <!-- Initial load: skeleton ghosts (view-mode aware) instead of an
2139
2641
  empty/"no files" flash. Only when there's nothing yet — navigation
2140
2642
  keeps the current list, exactly as before. -->
2141
2643
  <div v-if="loading && files.length === 0" class="fe__skeleton" role="status">
2142
2644
  <span class="fe-sr-only">{{ t('loading') }}</span>
2143
- <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">
2144
2646
  <div v-for="i in 8" :key="i" class="fe-skel-card">
2145
2647
  <div class="fe-skel fe-skel--thumb"></div>
2146
2648
  <div class="fe-skel fe-skel--label"></div>
@@ -2204,12 +2706,19 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
2204
2706
  <path d="M24 88h72" stroke-dasharray="3 5" />
2205
2707
  </svg>
2206
2708
  <p class="fe-state__title">{{ t('error.title') }}</p>
2207
- <p class="fe-state__hint">{{ loadError }}</p>
2709
+ <!-- wiring:c4 friendly hint + collapsible technical detail; the raw
2710
+ error message used to sit in the hint slot and read like UI copy. -->
2711
+ <p class="fe-state__hint">{{ t('error.hint') }}</p>
2208
2712
  <div class="fe-state__actions">
2209
2713
  <button type="button" class="fe-btn fe-btn--primary" @click="retryLoad">
2210
2714
  {{ t('error.retry') }}
2211
2715
  </button>
2212
2716
  </div>
2717
+ <details class="fe-state__details">
2718
+ <summary class="fe-state__details-summary">{{ t('error.details') }}</summary>
2719
+ <pre class="fe-state__details-pre">{{ loadError }}</pre>
2720
+ </details>
2721
+ <!-- /wiring:c4 -->
2213
2722
  </div>
2214
2723
  <!-- Search with zero hits — its own message, not "folder is empty". -->
2215
2724
  <div v-else-if="!loading && files.length === 0 && searchQuery" class="fe-state">
@@ -2302,6 +2811,22 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
2302
2811
  @star-change="onStarChange"
2303
2812
  />
2304
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
2305
2830
  v-else
2306
2831
  :files="files"
2307
2832
  :selected="selection.selected.value"
@@ -2316,8 +2841,35 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
2316
2841
  @item-drag-start="onItemDragStart"
2317
2842
  @item-drop-into="onItemDropInto"
2318
2843
  />
2844
+ <!-- /wiring:d2 -->
2319
2845
  </div>
2320
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
+
2321
2873
  <!-- koru:k1 — inspector (details) panel; v-if keeps the closed state
2322
2874
  free of any DOM. Narrow mode renders it as a full-size overlay. -->
2323
2875
  <InspectorPanel
@@ -2345,19 +2897,33 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
2345
2897
  </div>
2346
2898
  </div>
2347
2899
 
2900
+ <!-- wiring:c3 — unified operations center. UploadProgress + PendingOpsTray
2901
+ no longer draw their own corner UIs: they are renderless publishers
2902
+ feeding the opsCenter store; the single visible surface is the
2903
+ OperationsCenter badge + panel below. -->
2348
2904
  <UploadProgress
2349
2905
  :jobs="uploadJobs"
2350
2906
  :locale="locale"
2907
+ :center="opsCenter"
2351
2908
  @cancel="onCancelUpload"
2352
2909
  @dismiss="onDismissUpload"
2910
+ @retry="retryUploadJob"
2353
2911
  />
2354
2912
 
2355
2913
  <PendingOpsTray
2356
2914
  :ops="pendingOps.ops.value"
2357
2915
  :locale="locale"
2916
+ :center="opsCenter"
2358
2917
  @dismiss="(id) => pendingOps.dismiss(id)"
2359
2918
  />
2360
2919
 
2920
+ <OperationsCenter
2921
+ :center="opsCenter"
2922
+ :locale="locale"
2923
+ :narrow="isNarrow"
2924
+ />
2925
+ <!-- /wiring:c3 -->
2926
+
2361
2927
  <!-- bag:b4 — narrow-mode upload FAB (hidden in trash / read-only /
2362
2928
  virtual root; PendingOpsTray+UploadProgress shift up via CSS). -->
2363
2929
  <button
@@ -2530,18 +3096,37 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
2530
3096
  @navigate="(p: string) => load(p)"
2531
3097
  @new-folder="showNewFolder = true"
2532
3098
  @upload="triggerUpload"
2533
- @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ü */"
2534
3100
  @open-trash="loadTrash"
2535
3101
  @refresh="() => load()"
2536
3102
  @go-up="goUp"
3103
+ @open-theme="showThemeGallery = true /* wiring:int */"
3104
+ @open-shortcut-settings="showShortcutSettings = true /* wiring:int */"
3105
+ @start-tour="startTour() /* wiring:int */"
3106
+ :split-enabled="!isNarrow /* wiring:d1 */"
3107
+ @tab-new="newTabHere() /* wiring:d1 */"
3108
+ @split-toggle="toggleSplit() /* wiring:d1 */"
2537
3109
  />
2538
3110
  <ShortcutsHelp
2539
3111
  :open="showShortcutsHelp"
2540
3112
  :locale="locale"
2541
3113
  @close="showShortcutsHelp = false"
3114
+ @customize="showShortcutsHelp = false; showShortcutSettings = true /* wiring:c2 */"
2542
3115
  />
2543
3116
  <!-- /cila:c wiring -->
2544
3117
 
3118
+ <!-- wiring:c1 — tema galerisi -->
3119
+ <ThemeGallery
3120
+ :open="showThemeGallery"
3121
+ :locale="locale"
3122
+ :theme="config.theme || 'auto'"
3123
+ :dark="themeResolvedDark"
3124
+ :current="activeThemeId"
3125
+ @close="showThemeGallery = false"
3126
+ @select="setActiveTheme"
3127
+ />
3128
+ <!-- /wiring:c1 -->
3129
+
2545
3130
  <input
2546
3131
  ref="fileInputEl"
2547
3132
  type="file"
@@ -2567,6 +3152,42 @@ function buildAuthHeaders(extra: Record<string, string> = {}) {
2567
3152
  >{{ toast.actionLabel }}</button>
2568
3153
  </div>
2569
3154
  </transition>
3155
+
3156
+ <!-- wiring:c2 — shortcut settings modal + Space quick-look overlay -->
3157
+ <ShortcutSettings
3158
+ :open="showShortcutSettings"
3159
+ :locale="locale"
3160
+ :theme="config.theme || 'auto'"
3161
+ @close="showShortcutSettings = false"
3162
+ />
3163
+ <QuickLook
3164
+ :open="quickLookOpen"
3165
+ :locale="locale"
3166
+ :file="quickLookTarget"
3167
+ :theme="config.theme || 'auto'"
3168
+ :preview-url="(p: string) => api.previewUrl(p)"
3169
+ :download-url="(p: string) => api.downloadUrl(p)"
3170
+ :only-office-base="effectiveOnlyOfficeBase"
3171
+ :only-office-config-endpoint="effectiveOnlyOfficeConfigEndpoint"
3172
+ :auth-headers="() => buildAuthHeaders({ 'Content-Type': 'application/json' })"
3173
+ :auth-credentials="api.credentialsMode()"
3174
+ :drawio-url="effectiveDrawioUrl"
3175
+ :pdf-worker-url="props.config.pdfWorkerUrl || null"
3176
+ :viewer-base-url="props.config.viewerBaseUrl || null"
3177
+ @close="quickLookOpen = false"
3178
+ @nav="quickLookNav"
3179
+ @open-full="quickLookOpenFull"
3180
+ />
3181
+ <!-- /wiring:c2 -->
3182
+ <!-- wiring:c4 — onboarding coach-mark tour (teleports itself to body) -->
3183
+ <OnboardingTour
3184
+ :open="showTour"
3185
+ :locale="locale"
3186
+ :root="rootEl"
3187
+ :theme="config.theme || 'auto'"
3188
+ @close="onTourClose"
3189
+ />
3190
+ <!-- /wiring:c4 -->
2570
3191
  </div>
2571
3192
  </template>
2572
3193