@brftech/filex-core 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{ArchiveViewer-w6GDJJt9.js → ArchiveViewer-Qi4L2jj9.js} +2 -2
  2. package/dist/{ArchiveViewer-w6GDJJt9.js.map → ArchiveViewer-Qi4L2jj9.js.map} +1 -1
  3. package/dist/{CsvViewer-Ci5-mke8.js → CsvViewer-IOIrJDsJ.js} +2 -2
  4. package/dist/{CsvViewer-Ci5-mke8.js.map → CsvViewer-IOIrJDsJ.js.map} +1 -1
  5. package/dist/{DrawioViewer-D2iMuQfD.js → DrawioViewer-D0KtUQBO.js} +2 -2
  6. package/dist/{DrawioViewer-D2iMuQfD.js.map → DrawioViewer-D0KtUQBO.js.map} +1 -1
  7. package/dist/{EpubViewer-D4ydsa9X.js → EpubViewer-DJOXiOi4.js} +2 -2
  8. package/dist/{EpubViewer-D4ydsa9X.js.map → EpubViewer-DJOXiOi4.js.map} +1 -1
  9. package/dist/{IpynbViewer-CWBwiFKX.js → IpynbViewer-D16jXVJc.js} +2 -2
  10. package/dist/{IpynbViewer-CWBwiFKX.js.map → IpynbViewer-D16jXVJc.js.map} +1 -1
  11. package/dist/{MermaidViewer-CDhuqQli.js → MermaidViewer-BnSaVg1L.js} +2 -2
  12. package/dist/{MermaidViewer-CDhuqQli.js.map → MermaidViewer-BnSaVg1L.js.map} +1 -1
  13. package/dist/{PsdViewer-C_TkFb2H.js → PsdViewer-BUerXaQA.js} +2 -2
  14. package/dist/{PsdViewer-C_TkFb2H.js.map → PsdViewer-BUerXaQA.js.map} +1 -1
  15. package/dist/{TiffViewer-BmWCXR7e.js → TiffViewer-D0Qn3IV-.js} +2 -2
  16. package/dist/{TiffViewer-BmWCXR7e.js.map → TiffViewer-D0Qn3IV-.js.map} +1 -1
  17. package/dist/{Viewer3D-5kPT1QsY.js → Viewer3D-BtRK-k1B.js} +2 -2
  18. package/dist/{Viewer3D-5kPT1QsY.js.map → Viewer3D-BtRK-k1B.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-CGNE1bSv.js +11802 -0
  23. package/dist/index-CGNE1bSv.js.map +1 -0
  24. package/dist/index.d.ts +54 -0
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +27 -6
  28. package/src/components/SecondaryPane.vue +72 -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 +33 -1
  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
@@ -1581,7 +1581,10 @@ async function dispatchItemAction(key: string, targets: FileNode[]) {
1581
1581
  flashToast('Kopyala → Yapıştır hazır');
1582
1582
  break;
1583
1583
  case 'paste':
1584
- await paste();
1584
+ /* ui-fix — sağ-klik menüsünden yapıştırma da aktif panele gider
1585
+ * (klavye kısayolu zaten pane-route'luydu; menü değildi). */
1586
+ if (paneIsActive.value) await panePaste();
1587
+ else await paste();
1585
1588
  break;
1586
1589
  case 'delete':
1587
1590
  showDelete.value = true;
@@ -2518,14 +2521,30 @@ function toggleSplit() {
2518
2521
  return;
2519
2522
  }
2520
2523
  if (isNarrow.value) return;
2521
- tabsApi.setSplit({ path: currentPath.value ?? '' });
2524
+ tabsApi.setSplit({ path: currentPath.value ?? '', viewMode: viewMode.value });
2522
2525
  }
2523
2526
  function closeSplit() {
2524
2527
  tabsApi.setSplit(null);
2525
2528
  activePane.value = 'main';
2526
2529
  }
2527
2530
  function onPaneNavigate(p: string) {
2528
- tabsApi.setSplit({ path: p });
2531
+ tabsApi.setSplit({ ...(activeSplit.value ?? {}), path: p });
2532
+ }
2533
+ /* ui-fix — pane'in KENDİ görünüm modu: split açılırken ana panelinkini
2534
+ * devralır, sonrasında bağımsız. Toolbar'ın görünüm değiştiricisi ve palet
2535
+ * toggle'ı AKTİF panele yazar (Burak: "B tıklıyken ikon değiştir dersem
2536
+ * B'nin değişmesi lazım"). */
2537
+ const paneViewMode = computed<ViewMode>(() => activeSplit.value?.viewMode ?? viewMode.value);
2538
+ function setPaneViewMode(v: ViewMode) {
2539
+ if (!activeSplit.value) return;
2540
+ tabsApi.setSplit({ ...activeSplit.value, viewMode: v });
2541
+ }
2542
+ const displayedViewMode = computed<ViewMode>(() =>
2543
+ paneIsActive.value ? paneViewMode.value : viewMode.value,
2544
+ );
2545
+ function setDisplayedViewMode(v: ViewMode) {
2546
+ if (paneIsActive.value) setPaneViewMode(v);
2547
+ else viewMode.value = v;
2529
2548
  }
2530
2549
 
2531
2550
  // Aktif panel: kısayollar aktif panele gider; panel tıklamayla aktifleşir.
@@ -2883,7 +2902,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
2883
2902
  <!-- /wiring:d1 -->
2884
2903
  <Toolbar
2885
2904
  ref="toolbarRef"
2886
- :view-mode="viewMode"
2905
+ :view-mode="displayedViewMode /* ui-fix — aktif panelin modu */"
2887
2906
  :search-query="searchQuery"
2888
2907
  :trash-active="trashActive"
2889
2908
  :actions="toolbarActions"
@@ -2899,7 +2918,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
2899
2918
  :inspector-open="showInspector /* koru:k1 */"
2900
2919
  @toggle-inspector="toggleInspector /* koru:k1 */"
2901
2920
  @open-theme="showThemeGallery = true /* wiring:c1 */"
2902
- @update:view-mode="viewMode = $event"
2921
+ @update:view-mode="setDisplayedViewMode($event) /* ui-fix — aktif panele */"
2903
2922
  @update:search-query="searchQuery = $event"
2904
2923
  @update:density="density = $event"
2905
2924
  @open-shortcut-settings="showShortcutSettings = true /* wiring:c2 */"
@@ -3230,6 +3249,8 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
3230
3249
  :multi-root="multiStorageRoot"
3231
3250
  :virtual-rows="virtualStorageRows"
3232
3251
  :active="paneIsActive"
3252
+ :view-mode="paneViewMode /* ui-fix */"
3253
+ :thumb-src="thumbs.src /* ui-fix */"
3233
3254
  @navigate="onPaneNavigate"
3234
3255
  @activate="activePane = 'split'"
3235
3256
  @close="closeSplit"
@@ -3476,7 +3497,7 @@ async function submitEncryptedFolder(payload: { name: string; password: string }
3476
3497
  @navigate="(p: string) => load(p)"
3477
3498
  @new-folder="showNewFolder = true"
3478
3499
  @upload="triggerUpload"
3479
- @toggle-view="viewMode = viewMode === 'list' ? 'grid' : viewMode === 'grid' ? 'gallery' : 'list' /* wiring:d2 — 3 mod döngüsü */"
3500
+ @toggle-view="setDisplayedViewMode(displayedViewMode === 'list' ? 'grid' : displayedViewMode === 'grid' ? 'gallery' : 'list') /* wiring:d2 + ui-fix — 3 mod döngüsü, aktif panele */"
3480
3501
  @open-trash="loadTrash"
3481
3502
  @refresh="() => load()"
3482
3503
  @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<{
@@ -60,7 +67,7 @@ const emit = defineEmits<{
60
67
  (e: 'transfer', p: { sources: string[]; targetWire: string; originWire?: string }): void;
61
68
  }>();
62
69
 
63
- const { t, formatSize, nodeDisplayName } = useLocale(() => props.locale);
70
+ const { t } = useLocale(() => props.locale);
64
71
 
65
72
  const path = ref<string>('');
66
73
  const files = ref<FileNode[]>([]);
@@ -149,35 +156,40 @@ function crumbGo(target: string) {
149
156
  // Selection + navigation
150
157
  // ------------------------------------------------------------------
151
158
 
152
- function onRowClick(n: FileNode, ev: MouseEvent) {
159
+ /* ui-fix adapter for the shared view components' click contract
160
+ * ({ctrl, shift} mod object instead of a MouseEvent). Shift behaves as
161
+ * ctrl here: the pane keeps a simple Set, no range anchor. */
162
+ function onViewClick(n: FileNode, mod: { ctrl: boolean; shift: boolean }) {
153
163
  emit('activate');
154
- const multi = ev.ctrlKey || ev.metaKey;
164
+ const multi = mod.ctrl || mod.shift;
155
165
  const next = new Set<string>(multi ? selected.value : []);
156
166
  if (multi && next.has(n.path)) next.delete(n.path);
157
167
  else next.add(n.path);
158
168
  selected.value = next;
159
169
  }
160
170
 
161
- function onRowDbl(n: FileNode) {
162
- if (n.type !== 'dir') return;
163
- void loadPane(isStorageRow(n) ? n.path : props.toUser(n.path));
171
+ /* ui-fix — right-click on a pane item: activate + select it, no menu
172
+ * (the pane has no item menu; letting the main panel's menu open here
173
+ * would act on the WRONG panel's selection). */
174
+ function onViewContext(n: FileNode, ev: MouseEvent) {
175
+ ev.preventDefault();
176
+ ev.stopPropagation();
177
+ emit('activate');
178
+ if (!selected.value.has(n.path)) selected.value = new Set([n.path]);
164
179
  }
165
180
 
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();
181
+ /* ui-fix drop-into from the shared views: same payload path as the
182
+ * old flat rows (dir check + wire-qualified row paths). */
183
+ function onViewDropInto(target: FileNode, ev: DragEvent) {
184
+ dropBg.value = false;
185
+ if (target.type !== 'dir' || isStorageRow(target)) return;
186
+ if (!acceptDrag(ev)) return;
187
+ handleDropPayload(ev, target.path);
170
188
  }
171
189
 
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();
190
+ function onRowDbl(n: FileNode) {
179
191
  if (n.type !== 'dir') return;
180
- emit('open-tab', isStorageRow(n) ? n.path : props.toUser(n.path));
192
+ void loadPane(isStorageRow(n) ? n.path : props.toUser(n.path));
181
193
  }
182
194
 
183
195
  function clearSelection() {
@@ -212,7 +224,6 @@ function onRowDragStart(n: FileNode, ev: DragEvent) {
212
224
  // Drop target (dir rows + pane background)
213
225
  // ------------------------------------------------------------------
214
226
 
215
- const dropPath = ref<string | null>(null);
216
227
  const dropBg = ref(false);
217
228
 
218
229
  function acceptDrag(ev: DragEvent): boolean {
@@ -238,28 +249,6 @@ function handleDropPayload(ev: DragEvent, targetWire: string) {
238
249
  emit('transfer', { sources, targetWire, originWire: origin });
239
250
  }
240
251
 
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
252
  function onBgDragOver(ev: DragEvent) {
264
253
  if (!acceptDrag(ev) || atVirtualRoot.value) return;
265
254
  ev.preventDefault();
@@ -274,7 +263,6 @@ function onBgDragLeave() {
274
263
 
275
264
  function onBgDrop(ev: DragEvent) {
276
265
  dropBg.value = false;
277
- dropPath.value = null;
278
266
  if (!acceptDrag(ev) || atVirtualRoot.value) return;
279
267
  handleDropPayload(ev, props.qualify(path.value));
280
268
  }
@@ -313,10 +301,6 @@ function getPath(): string {
313
301
  }
314
302
 
315
303
  defineExpose({ reload, goUp, selectAll, openSelected, selectedNodes, getPath });
316
-
317
- function specialEmojiFor(n: FileNode): string | null {
318
- return isStorageRow(n) ? '💾' : null;
319
- }
320
304
  </script>
321
305
 
322
306
  <template>
@@ -376,36 +360,46 @@ function specialEmojiFor(n: FileNode): string | null {
376
360
  <div v-else-if="files.length === 0" class="fe-split__state">
377
361
  {{ t('empty.folder') }}
378
362
  </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>
363
+ <!-- ui-fix aynı görünüm bileşenleri (list/grid/gallery), pane'in
364
+ kendi viewMode'uyla; eski düz fe-split__list kalktı. -->
365
+ <ListView
366
+ v-else-if="(viewMode ?? 'list') === 'list'"
367
+ :files="files"
368
+ :selected="selected"
369
+ :locale="locale"
370
+ :loading="loading"
371
+ @click-row="onViewClick"
372
+ @dbl-row="onRowDbl"
373
+ @context-row="onViewContext"
374
+ @item-drag-start="onRowDragStart"
375
+ @item-drop-into="onViewDropInto"
376
+ />
377
+ <GridView
378
+ v-else-if="viewMode === 'grid'"
379
+ :files="files"
380
+ :selected="selected"
381
+ :locale="locale"
382
+ :loading="loading"
383
+ :thumb-src="thumbSrc"
384
+ @click-card="onViewClick"
385
+ @dbl-card="onRowDbl"
386
+ @context-card="onViewContext"
387
+ @item-drag-start="onRowDragStart"
388
+ @item-drop-into="onViewDropInto"
389
+ />
390
+ <GalleryView
391
+ v-else
392
+ :files="files"
393
+ :selected="selected"
394
+ :locale="locale"
395
+ :loading="loading"
396
+ :thumb-src="thumbSrc"
397
+ @click-card="onViewClick"
398
+ @dbl-card="onRowDbl"
399
+ @context-card="onViewContext"
400
+ @item-drag-start="onRowDragStart"
401
+ @item-drop-into="onViewDropInto"
402
+ />
409
403
  </div>
410
404
  </section>
411
405
  </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(),
@@ -4,6 +4,7 @@
4
4
  position: relative;
5
5
  display: flex;
6
6
  flex-direction: column;
7
+ box-sizing: border-box;
7
8
  min-height: 420px;
8
9
  height: 100%;
9
10
  background: var(--fe-bg);
@@ -3099,7 +3100,12 @@ filex-explorer {
3099
3100
  flex: 1 1 auto;
3100
3101
  min-width: 0;
3101
3102
  overflow-x: auto;
3102
- scrollbar-width: thin;
3103
+ /* Tab overflow scrolls (wheel / drag / keyboard) but never shows a
3104
+ * scrollbar strip under the tabs — the 30px bar cannot fit one. */
3105
+ scrollbar-width: none;
3106
+ }
3107
+ .fe-tabs__scroll::-webkit-scrollbar {
3108
+ display: none;
3103
3109
  }
3104
3110
  .fe-tabs__tab {
3105
3111
  display: flex;
@@ -3708,3 +3714,29 @@ filex-explorer {
3708
3714
  text-decoration: underline;
3709
3715
  }
3710
3716
  /* === /wiring:e2 === */
3717
+
3718
+ /* === ui-fix — toolbar tek satır + katlama + ölçüm şeridi ============= */
3719
+ .fe:not(.fe--narrow) .fe-toolbar {
3720
+ flex-wrap: nowrap;
3721
+ }
3722
+ .fe:not(.fe--narrow) .fe-toolbar__primary {
3723
+ flex: 1 1 auto;
3724
+ min-width: 0;
3725
+ flex-wrap: nowrap;
3726
+ overflow: hidden;
3727
+ position: relative;
3728
+ }
3729
+ .fe-toolbar__measure {
3730
+ position: absolute;
3731
+ visibility: hidden;
3732
+ pointer-events: none;
3733
+ height: 0;
3734
+ overflow: hidden;
3735
+ display: flex;
3736
+ gap: 8px;
3737
+ white-space: nowrap;
3738
+ }
3739
+ .fe-toolbar__measure .fe-btn {
3740
+ flex: none;
3741
+ }
3742
+ /* === /ui-fix ========================================================= */
@@ -13,7 +13,7 @@
13
13
  --fe-text: #1a1e27;
14
14
  --fe-text-muted: #5a6475;
15
15
  --fe-text-on-primary: #ffffff;
16
- --fe-primary: #3b82f6;
16
+ --fe-primary: #2f6fe0; /* ui-fix: beyaz metinle WCAG AA (eski #3b82f6 = 3.68) */
17
17
  --fe-primary-hover: #2563eb;
18
18
  --fe-danger: #dc2626;
19
19
  --fe-danger-hover: #b91c1c;
@@ -102,7 +102,7 @@
102
102
  --fe-icon-archive: #78716c;
103
103
  --fe-icon-code: #4f46e5;
104
104
  --fe-icon-text: #64748b;
105
- --fe-icon-unknown: #94a3b8;
105
+ --fe-icon-unknown: #64748b; /* ui-fix: acik temada 3:1 UI kontrasti */
106
106
  }
107
107
 
108
108
  /* Dark values — same selector strategy as the palette blocks above. */