@brftech/filex-core 0.31.0 → 0.33.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 (62) hide show
  1. package/README.md +26 -2
  2. package/dist/{CsvViewer-DxujFEM3.js → CsvViewer-CqWeV8VO.js} +22 -22
  3. package/dist/CsvViewer-CqWeV8VO.js.map +1 -0
  4. package/dist/{DrawioViewer-DCvWnX2t.js → DrawioViewer-BNALOB04.js} +16 -16
  5. package/dist/DrawioViewer-BNALOB04.js.map +1 -0
  6. package/dist/filex-core.js +9346 -8069
  7. package/dist/filex-core.js.map +1 -1
  8. package/dist/filex-core.umd.cjs +49 -49
  9. package/dist/filex-core.umd.cjs.map +1 -1
  10. package/dist/index.d.ts +235 -16
  11. package/dist/style.css +1 -1
  12. package/package.json +1 -1
  13. package/src/FileExplorer.vue +776 -246
  14. package/src/components/CommandPalette.vue +21 -5
  15. package/src/components/ConnectionsPanel.vue +2 -1
  16. package/src/components/E2eRecoveryUnlockModal.vue +29 -4
  17. package/src/components/FilterBar.vue +244 -0
  18. package/src/components/GalleryView.vue +12 -1
  19. package/src/components/GridView.vue +48 -5
  20. package/src/components/InspectorPanel.vue +231 -8
  21. package/src/components/ListView.vue +12 -3
  22. package/src/components/NFSExportsPanel.vue +2 -1
  23. package/src/components/PendingOpsTray.vue +1 -1
  24. package/src/components/PresenceBar.vue +3 -2
  25. package/src/components/S3KeysPanel.vue +2 -1
  26. package/src/components/SSHKeysPanel.vue +2 -1
  27. package/src/components/SecondaryPane.vue +28 -13
  28. package/src/components/SideNav.vue +161 -1
  29. package/src/components/StarButton.vue +2 -1
  30. package/src/components/TabBar.vue +2 -2
  31. package/src/components/ThemeGallery.vue +1 -1
  32. package/src/components/TokensPanel.vue +2 -1
  33. package/src/components/Toolbar.vue +244 -58
  34. package/src/components/ViewSwitcher.vue +81 -0
  35. package/src/composables/useFileApi.ts +49 -2
  36. package/src/composables/useLocale.ts +1 -1
  37. package/src/composables/useOperations.ts +3 -3
  38. package/src/composables/usePendingOps.ts +1 -1
  39. package/src/composables/useTabs.ts +1 -1
  40. package/src/composables/useUploadChunked.ts +39 -7
  41. package/src/index.ts +8 -2
  42. package/src/lib/e2ecrypto.ts +191 -3
  43. package/src/lib/fileFilters.ts +143 -0
  44. package/src/lib/listing.ts +47 -0
  45. package/src/lib/themes.ts +7 -7
  46. package/src/lib/transfer.ts +6 -6
  47. package/src/locales/en.ts +115 -0
  48. package/src/locales/index.ts +1 -0
  49. package/src/locales/resolve.ts +66 -0
  50. package/src/locales/tr.ts +116 -0
  51. package/src/modals/ConvertModal.vue +14 -12
  52. package/src/modals/NewFolderModal.vue +1 -1
  53. package/src/modals/PermissionsModal.vue +30 -13
  54. package/src/modals/PreviewModal.vue +9 -9
  55. package/src/styles/base.css +520 -26
  56. package/src/styles/variables.css +1 -1
  57. package/src/types/ExplorerConfig.ts +18 -2
  58. package/src/types/FileNode.ts +1 -1
  59. package/src/viewers/CsvViewer.vue +4 -4
  60. package/src/viewers/DrawioViewer.vue +2 -2
  61. package/dist/CsvViewer-DxujFEM3.js.map +0 -1
  62. package/dist/DrawioViewer-DCvWnX2t.js.map +0 -1
@@ -3,7 +3,7 @@
3
3
  * Toolbar — selection-aware action row.
4
4
  *
5
5
  * Layout:
6
- * - No selection → [📁 Yeni Klasör] [search | ⬆ ↻] [☰ ▦]
6
+ * - No selection → [📁 New Folder] [search | ⬆ ↻] [☰ ▦]
7
7
  * - 1 file selected → [👁 ⬇ 🔗 ✎ 🗑] [search | ⬆ ↻] [☰ ▦]
8
8
  * - 1 folder selected → [↗ ✎ 🗑] [search | ⬆ ↻] [☰ ▦]
9
9
  * - Multi selection → [✂ ❐ 🗑] [search | ⬆ ↻] [☰ ▦]
@@ -15,6 +15,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
15
15
  import type { ViewMode } from '../types/FileNode';
16
16
  import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
17
17
  import ContextMenu, { type ContextAction } from './ContextMenu.vue';
18
+ import ViewSwitcher from './ViewSwitcher.vue';
18
19
  import { useLocale } from '../composables/useLocale';
19
20
 
20
21
  export type SelectionMode = 'none' | 'single-file' | 'single-dir' | 'multi';
@@ -90,6 +91,26 @@ const props = defineProps<{
90
91
  * "power user tool", and gallery is the one nobody outside photos asks for.
91
92
  */
92
93
  viewModes?: ViewMode[];
94
+ /* === surucu:d1 — the Drive shell ==================================== */
95
+ /**
96
+ * Which header this is.
97
+ *
98
+ * 'classic' (default) — the row this toolbar has always been.
99
+ * 'drive' — `uiProfile: 'drive'`: one search field across the
100
+ * width with its ⌘K/Ctrl+K hint, and the controls
101
+ * that belong to a *listing* rather than to the app
102
+ * (view switcher, details toggle) moved down onto the
103
+ * breadcrumb row, where the mockups put them.
104
+ *
105
+ * ⚠ Nothing is DELETED by 'drive'. Density, theme, the shortcut editor, the
106
+ * tour and both other view modes are all in the "⋯" menu this variant
107
+ * renders — the same list the narrow layout has used since bag:b4. A control
108
+ * that is on screen in one profile and unreachable in another is a capability
109
+ * split, not a preset.
110
+ */
111
+ shell?: 'classic' | 'drive';
112
+ /** Folder name for the drive field's placeholder ("Search in Photos"). */
113
+ scopeLabel?: string;
93
114
  }>();
94
115
 
95
116
  const emit = defineEmits<{
@@ -106,6 +127,8 @@ const emit = defineEmits<{
106
127
  (e: 'toggle-nav'): void /* gezinti:g1 */;
107
128
  (e: 'open-theme'): void /* wiring:c1 — tema galerisi */;
108
129
  (e: 'open-shortcut-settings'): void /* wiring:c2 */;
130
+ /* surucu:d1 — escalate this query into the command palette (⌘K/Ctrl+K). */
131
+ (e: 'open-palette', query: string): void;
109
132
  }>();
110
133
 
111
134
  // Density toggle — the toolbar owns the persisted preference; the parent
@@ -294,16 +317,14 @@ function offersView(v: ViewMode): boolean {
294
317
  return !props.viewModes || props.viewModes.includes(v);
295
318
  }
296
319
 
297
- const moreActions = computed<ContextAction[]>(() => {
320
+ /* surucu:d1 the tail of the "⋯" menu on its own: everything that is a
321
+ * SETTING of the explorer rather than an action on a file. The narrow layout
322
+ * shows it after the write/selection actions (unchanged); the drive header
323
+ * shows only this half, because in that layout the selection actions are
324
+ * already on screen as buttons and a menu that repeats them is a second door
325
+ * to the same room. */
326
+ const utilityActions = computed<ContextAction[]>(() => {
298
327
  const list: ContextAction[] = [];
299
- const writable =
300
- !props.trashActive && !props.atVirtualRoot && props.canWrite !== false;
301
- if (mode.value === 'none' && writable) {
302
- list.push({ key: 'new-folder', label: t('toolbar.new_folder'), icon: '📁' });
303
- if (props.pasteEnabled) list.push({ key: 'paste', label: t('ctx.paste'), icon: '📋' });
304
- }
305
- list.push(...toolbarItems.value);
306
- if (list.length) list.push({ divider: true, key: 'bag-sep', label: '' });
307
328
  list.push({ key: 'refresh', label: t('toolbar.refresh'), icon: '↻' });
308
329
  list.push({
309
330
  key: 'density',
@@ -313,8 +334,8 @@ const moreActions = computed<ContextAction[]>(() => {
313
334
  : t('toolbar.density.compact'),
314
335
  icon: '⇅',
315
336
  });
316
- /* wiring:d2 — dar mod menüsü: aktif olmayan İKİ görünüm de listelenir
317
- (list/grid/gallery); eski tekli toggle üç modda eksik kalıyordu. */
337
+ /* wiring:d2 — narrow-modemenu: BOTH inactive views are listed
338
+ (list/grid/gallery); the old single toggle fell short with three modes. */
318
339
  if (props.viewMode !== 'list' && offersView('list')) list.push({ key: 'view-list', label: t('toolbar.view.list'), icon: '☰' });
319
340
  if (props.viewMode !== 'grid' && offersView('grid')) list.push({ key: 'view-grid', label: t('toolbar.view.grid'), icon: '▦' });
320
341
  if (props.viewMode !== 'gallery' && offersView('gallery')) list.push({ key: 'view-gallery', label: t('toolbar.view.gallery'), icon: '▣' });
@@ -326,13 +347,69 @@ const moreActions = computed<ContextAction[]>(() => {
326
347
  }
327
348
  /* koru:k1 — inspector toggle also reachable from the narrow overflow menu */
328
349
  list.push({ key: 'inspector', label: t('toolbar.inspector'), icon: 'ℹ' });
329
- /* wiring:c1 — tema galerisi de dar moddamenüsünden açılır */
350
+ /* wiring:c1 — the theme gallery is opened from the menu in narrow mode too */
330
351
  list.push({ key: 'theme', label: t('theme.menu'), icon: '🎨' });
331
352
  list.push({ key: 'shortcut-settings', label: t('shortcuts.settings.menu'), icon: '⌨' }) /* wiring:c2 */;
332
353
  list.push({ key: 'tour', label: t('tour.restart'), icon: '🎓' }); /* wiring:c4 */
333
354
  return list;
334
355
  });
335
356
 
357
+ const moreActions = computed<ContextAction[]>(() => {
358
+ const list: ContextAction[] = [];
359
+ const writable =
360
+ !props.trashActive && !props.atVirtualRoot && props.canWrite !== false;
361
+ if (mode.value === 'none' && writable) {
362
+ list.push({ key: 'new-folder', label: t('toolbar.new_folder'), icon: '📁' });
363
+ if (props.pasteEnabled) list.push({ key: 'paste', label: t('ctx.paste'), icon: '📋' });
364
+ }
365
+ list.push(...toolbarItems.value);
366
+ if (list.length) list.push({ divider: true, key: 'bag-sep', label: '' });
367
+ list.push(...utilityActions.value);
368
+ return list;
369
+ });
370
+
371
+ /* === surucu:d1 — the drive header's search field ======================
372
+ * ONE field, and it is the one the toolbar already had: typing here sets
373
+ * `searchQuery`, which the explorer answers with
374
+ * `?action=search&filter=…` — a real search of the folder you are standing in,
375
+ * exactly as before.
376
+ *
377
+ * The ⌘K chip beside it is the ESCALATION, not a second box: it opens the
378
+ * command palette carrying whatever has been typed, and the palette is where
379
+ * "everywhere" lives (the global endpoint, saved searches, and the commands).
380
+ * That is why the hint can sit on this field honestly — Ctrl+K from here does
381
+ * what the hint says, and Ctrl+K from anywhere else still opens the palette
382
+ * as it has since cila:c.
383
+ */
384
+ const macLike =
385
+ typeof navigator !== 'undefined' &&
386
+ /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent || '');
387
+ const paletteCombo = computed(() => (macLike ? '⌘ K' : 'Ctrl K'));
388
+
389
+ const drivePlaceholder = computed(() =>
390
+ props.scopeLabel
391
+ ? t('drive.search.placeholder', { scope: props.scopeLabel })
392
+ : t('drive.search.placeholder_all'),
393
+ );
394
+
395
+ /** ⌘/Ctrl+K typed INSIDE the field. The global shortcut handler ignores
396
+ * keystrokes aimed at an input — which is correct, and it would otherwise
397
+ * make the hint on this very field a lie. */
398
+ function onSearchKeydown(ev: KeyboardEvent) {
399
+ if ((ev.ctrlKey || ev.metaKey) && (ev.key === 'k' || ev.key === 'K')) {
400
+ ev.preventDefault();
401
+ ev.stopPropagation();
402
+ emit('open-palette', localSearch.value);
403
+ }
404
+ }
405
+
406
+ /* surucu:d1 — the drive header renders the selection actions as buttons, so
407
+ * its "⋯" carries only the settings half; the narrow layout renders no action
408
+ * buttons at all and still needs the whole list. */
409
+ const menuActions = computed<ContextAction[]>(() =>
410
+ props.shell === 'drive' && !props.narrow ? utilityActions.value : moreActions.value,
411
+ );
412
+
336
413
  function openMore() {
337
414
  const r = moreBtnEl.value?.getBoundingClientRect();
338
415
  moreRef.value?.show({ clientX: r ? r.right : 0, clientY: r ? r.bottom + 4 : 0 }, []);
@@ -387,6 +464,7 @@ function onMoreSelect(a: ContextAction) {
387
464
  :class="{
388
465
  'fe-toolbar--narrow': narrow /* bag:b4 */,
389
466
  'fe-toolbar--searching': narrow && searchOpen /* bag:b4 */,
467
+ 'fe-toolbar--drive': shell === 'drive' /* surucu:d1 */,
390
468
  }"
391
469
  >
392
470
  <!-- bag:b4 — wide layout, untouched; renders exactly as before when not narrow -->
@@ -433,8 +511,11 @@ function onMoreSelect(a: ContextAction) {
433
511
  <span class="fe-icon">↑</span>
434
512
  </button>
435
513
 
514
+ <!-- surucu:d1 — in the drive shell the panel's "+ New" menu is the one
515
+ primary action; a second New Folder button here would be a second
516
+ front door to the same modal. -->
436
517
  <button
437
- v-if="mode === 'none' && !trashActive && !atVirtualRoot && canWrite !== false"
518
+ v-if="shell !== 'drive' && mode === 'none' && !trashActive && !atVirtualRoot && canWrite !== false"
438
519
  type="button"
439
520
  class="fe-btn fe-btn--primary"
440
521
  :title="t('toolbar.new_folder')"
@@ -458,8 +539,8 @@ function onMoreSelect(a: ContextAction) {
458
539
  <span class="fe-btn__label">{{ a.label }}</span>
459
540
  </button>
460
541
 
461
- <!-- ui-fix — sığmayan aksiyonlar tek satırı korumak içinmenüsüne
462
- katlanır (arama/görünüm kontrolleri artık alt satıra düşmez). -->
542
+ <!-- ui-fix — actions that don't fit fold into the menu so the row stays
543
+ a single line (search/view controls no longer drop to a second row). -->
463
544
  <button
464
545
  v-if="overflowToolbarItems.length > 0"
465
546
  ref="wideMoreBtnEl"
@@ -473,8 +554,8 @@ function onMoreSelect(a: ContextAction) {
473
554
  <span class="fe-icon">⋯</span>
474
555
  </button>
475
556
 
476
- <!-- Görünmez ölçüm şeridi: TÜM aksiyonlar her zaman burada render
477
- edilir; katlama hesabı gerçek genişliklerden yapılır. -->
557
+ <!-- Invisible measuring strip: EVERY action is always rendered here, so
558
+ the fold calculation runs off real measured widths. -->
478
559
  <div ref="measureEl" class="fe-toolbar__measure" aria-hidden="true">
479
560
  <button
480
561
  v-for="a in toolbarItems"
@@ -502,8 +583,45 @@ function onMoreSelect(a: ContextAction) {
502
583
 
503
584
  <div class="fe-toolbar__spacer" />
504
585
 
505
- <div class="fe-toolbar__search-group">
506
- <div class="fe-search">
586
+ <div class="fe-toolbar__search-group" :class="{ 'fe-toolbar__search-group--drive': shell === 'drive' }">
587
+ <!-- surucu:d1 — the drive shell's one search field: wide, in the header,
588
+ with the ⌘K chip that opens the palette carrying this query. -->
589
+ <div v-if="shell === 'drive'" class="fe-drivesearch" data-testid="drive-search">
590
+ <svg
591
+ class="fe-ficon fe-drivesearch__glass"
592
+ viewBox="0 0 24 24"
593
+ fill="none"
594
+ stroke="currentColor"
595
+ stroke-width="1.9"
596
+ stroke-linecap="round"
597
+ aria-hidden="true"
598
+ focusable="false"
599
+ >
600
+ <circle cx="11" cy="11" r="6.5" />
601
+ <path d="M16 16l4.5 4.5" />
602
+ </svg>
603
+ <input
604
+ ref="searchEl"
605
+ type="search"
606
+ class="fe-drivesearch__input"
607
+ :placeholder="drivePlaceholder"
608
+ :value="localSearch"
609
+ :aria-label="t('toolbar.search')"
610
+ @input="onSearchInput"
611
+ @keydown="onSearchKeydown"
612
+ />
613
+ <button
614
+ type="button"
615
+ class="fe-drivesearch__kbd"
616
+ :title="t('drive.search.hint_title', { combo: paletteCombo })"
617
+ :aria-label="t('drive.search.hint_title', { combo: paletteCombo })"
618
+ data-testid="drive-search-palette"
619
+ @click="emit('open-palette', localSearch)"
620
+ >
621
+ {{ paletteCombo }}
622
+ </button>
623
+ </div>
624
+ <div v-else class="fe-search">
507
625
  <input
508
626
  ref="searchEl"
509
627
  type="search"
@@ -515,7 +633,7 @@ function onMoreSelect(a: ContextAction) {
515
633
  />
516
634
  </div>
517
635
  <button
518
- v-if="!atVirtualRoot && canWrite !== false"
636
+ v-if="shell !== 'drive' && !atVirtualRoot && canWrite !== false"
519
637
  type="button"
520
638
  class="fe-btn fe-btn--icon-only"
521
639
  :title="t('toolbar.upload')"
@@ -533,9 +651,26 @@ function onMoreSelect(a: ContextAction) {
533
651
  >
534
652
  <span class="fe-icon" aria-hidden="true">↻</span>
535
653
  </button>
654
+ <!-- surucu:d1 — density, theme, the shortcut editor, the tour and the
655
+ other view modes. Everything the drive header does not draw is one
656
+ click away here; nothing is removed from the build. -->
657
+ <button
658
+ v-if="shell === 'drive'"
659
+ ref="moreBtnEl"
660
+ type="button"
661
+ class="fe-btn fe-btn--icon-only"
662
+ :title="t('toolbar.more')"
663
+ :aria-label="t('toolbar.more')"
664
+ aria-haspopup="menu"
665
+ data-testid="drive-more"
666
+ @click="openMore"
667
+ >
668
+ <span class="fe-icon" aria-hidden="true">⋯</span>
669
+ </button>
536
670
  </div>
537
671
 
538
672
  <button
673
+ v-if="shell !== 'drive'"
539
674
  type="button"
540
675
  class="fe-btn fe-btn--icon-only fe-toolbar__density"
541
676
  :class="{ 'is-active': density === 'compact' }"
@@ -573,8 +708,11 @@ function onMoreSelect(a: ContextAction) {
573
708
  </svg>
574
709
  </button>
575
710
 
576
- <!-- koru:k1 — inspector (details panel) toggle -->
711
+ <!-- koru:k1 — inspector (details panel) toggle.
712
+ surucu:d1 — the drive shell renders it on the breadcrumb row instead,
713
+ beside the view switcher, where both belong to the listing. -->
577
714
  <button
715
+ v-if="shell !== 'drive'"
578
716
  type="button"
579
717
  class="fe-btn fe-btn--icon-only fe-toolbar__inspector"
580
718
  :class="{ 'is-active': inspectorOpen }"
@@ -601,6 +739,7 @@ function onMoreSelect(a: ContextAction) {
601
739
 
602
740
  <!-- wiring:c1 — tema galerisi (palet ikonu) -->
603
741
  <button
742
+ v-if="shell !== 'drive'"
604
743
  type="button"
605
744
  class="fe-btn fe-btn--icon-only fe-toolbar__theme"
606
745
  :title="t('theme.menu')"
@@ -626,49 +765,96 @@ function onMoreSelect(a: ContextAction) {
626
765
  </button>
627
766
  <!-- /wiring:c1 -->
628
767
 
629
- <div class="fe-toolbar__view" role="tablist" :aria-label="t('toolbar.view_label') /* wiring:c4 — was hardcoded EN */">
630
- <button
631
- type="button"
632
- v-if="offersView('list')"
633
- class="fe-btn fe-btn--icon-only"
634
- :class="{ 'is-active': viewMode === 'list' }"
635
- role="tab"
636
- :aria-selected="viewMode === 'list'"
637
- :title="t('toolbar.view.list')"
638
- :aria-label="t('toolbar.view.list') /* wiring:c4 */"
639
- @click="emit('update:viewMode', 'list')"
640
- >
641
- <span class="fe-icon" aria-hidden="true">☰</span>
642
- </button>
768
+ <ViewSwitcher
769
+ v-if="shell !== 'drive'"
770
+ :view-mode="viewMode"
771
+ :locale="locale"
772
+ :modes="viewModes"
773
+ @update:view-mode="emit('update:viewMode', $event)"
774
+ />
775
+ </template>
776
+
777
+ <!-- surucu:d1 narrow DRIVE layout: [☰] [ search ] [⋯].
778
+ The classic narrow row hides search behind a magnifier, which is the
779
+ right trade when the row also carries Up / Upload / actions. This shell
780
+ does not: "+ New" lives in the drawer, upload has the floating button
781
+ at this width already, and the whole point of the profile is that the
782
+ search field is the thing you see. So the field keeps the row, and only
783
+ the ⌘K chip goes (CSS) — a phone cannot press it, and the palette stays
784
+ in the "⋯" menu. -->
785
+ <template v-else-if="shell === 'drive'">
643
786
  <button
787
+ v-if="navEnabled !== false"
644
788
  type="button"
645
- v-if="offersView('grid')"
646
- class="fe-btn fe-btn--icon-only"
647
- :class="{ 'is-active': viewMode === 'grid' }"
648
- role="tab"
649
- :aria-selected="viewMode === 'grid'"
650
- :title="t('toolbar.view.grid')"
651
- :aria-label="t('toolbar.view.grid') /* wiring:c4 */"
652
- @click="emit('update:viewMode', 'grid')"
789
+ class="fe-btn fe-btn--icon-only fe-toolbar__nav"
790
+ :class="{ 'is-active': navOpen }"
791
+ :aria-pressed="!!navOpen"
792
+ :title="t('toolbar.nav')"
793
+ :aria-label="t('toolbar.nav')"
794
+ data-testid="toolbar-nav"
795
+ @click="emit('toggle-nav')"
653
796
  >
654
- <span class="fe-icon" aria-hidden="true">▦</span>
797
+ <svg
798
+ class="fe-ficon"
799
+ viewBox="0 0 24 24"
800
+ fill="none"
801
+ stroke="currentColor"
802
+ stroke-width="1.8"
803
+ stroke-linecap="round"
804
+ aria-hidden="true"
805
+ focusable="false"
806
+ >
807
+ <rect x="3.5" y="4.5" width="17" height="15" rx="2" />
808
+ <path d="M9.5 4.5v15" />
809
+ </svg>
655
810
  </button>
656
- <!-- wiring:d2 — üçüncü görünüm: galeri -->
811
+ <div class="fe-drivesearch" data-testid="drive-search">
812
+ <svg
813
+ class="fe-ficon fe-drivesearch__glass"
814
+ viewBox="0 0 24 24"
815
+ fill="none"
816
+ stroke="currentColor"
817
+ stroke-width="1.9"
818
+ stroke-linecap="round"
819
+ aria-hidden="true"
820
+ focusable="false"
821
+ >
822
+ <circle cx="11" cy="11" r="6.5" />
823
+ <path d="M16 16l4.5 4.5" />
824
+ </svg>
825
+ <input
826
+ ref="searchEl"
827
+ type="search"
828
+ class="fe-drivesearch__input"
829
+ :placeholder="drivePlaceholder"
830
+ :value="localSearch"
831
+ :aria-label="t('toolbar.search')"
832
+ @input="onSearchInput"
833
+ @keydown="onSearchKeydown"
834
+ />
835
+ <button
836
+ type="button"
837
+ class="fe-drivesearch__kbd"
838
+ :title="t('drive.search.hint_title', { combo: paletteCombo })"
839
+ :aria-label="t('drive.search.hint_title', { combo: paletteCombo })"
840
+ data-testid="drive-search-palette"
841
+ @click="emit('open-palette', localSearch)"
842
+ >
843
+ {{ paletteCombo }}
844
+ </button>
845
+ </div>
657
846
  <button
847
+ ref="moreBtnEl"
658
848
  type="button"
659
- v-if="offersView('gallery')"
660
849
  class="fe-btn fe-btn--icon-only"
661
- :class="{ 'is-active': viewMode === 'gallery' }"
662
- role="tab"
663
- :aria-selected="viewMode === 'gallery'"
664
- :title="t('toolbar.view.gallery')"
665
- :aria-label="t('toolbar.view.gallery')"
666
- @click="emit('update:viewMode', 'gallery')"
850
+ :title="t('toolbar.more')"
851
+ :aria-label="t('toolbar.more')"
852
+ aria-haspopup="menu"
853
+ data-testid="drive-more"
854
+ @click="openMore"
667
855
  >
668
- <span class="fe-icon" aria-hidden="true">▣</span>
856
+ <span class="fe-icon">⋯</span>
669
857
  </button>
670
- <!-- /wiring:d2 -->
671
- </div>
672
858
  </template>
673
859
 
674
860
  <!-- bag:b4 — narrow layout: [↑?] ... [🔍] [⬆] [⋯], search expands full-width -->
@@ -778,12 +964,12 @@ function onMoreSelect(a: ContextAction) {
778
964
  :locale="locale"
779
965
  :theme="theme || 'auto'"
780
966
  :sheet="coarse"
781
- :actions="moreActions"
967
+ :actions="menuActions"
782
968
  @select="onMoreSelect"
783
969
  />
784
970
  </template>
785
971
 
786
- <!-- ui-fix — wide modda katlanan aksiyonların menüsü -->
972
+ <!-- ui-fix — menu for the actions folded away in wide mode -->
787
973
  <ContextMenu
788
974
  v-if="!narrow"
789
975
  ref="wideMoreRef"
@@ -0,0 +1,81 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * ViewSwitcher — the list / grid / gallery toggle.
4
+ *
5
+ * Lifted out of Toolbar.vue unchanged (same classes, same glyphs, same
6
+ * `role="tablist"` semantics) because the `drive` profile puts it on the
7
+ * breadcrumb row instead of in the header, and two copies of a switcher is
8
+ * how the two rows end up offering different modes. One component, mounted
9
+ * in whichever row the profile asks for.
10
+ *
11
+ * `modes` absent = all three, so every embedder that predates `uiProfile`
12
+ * keeps the switcher it had.
13
+ *
14
+ * ⚠ No `<style>` block — package CSS lives in `styles/base.css` (a scoped
15
+ * block's `data-v` hash does not match in the web-component build).
16
+ */
17
+ import { useLocale } from '../composables/useLocale';
18
+ import type { LocaleCode } from '../types/ExplorerConfig';
19
+ import type { ViewMode } from '../types/FileNode';
20
+
21
+ const props = defineProps<{
22
+ viewMode: ViewMode;
23
+ locale: LocaleCode;
24
+ modes?: ViewMode[];
25
+ }>();
26
+
27
+ const emit = defineEmits<{ (e: 'update:viewMode', v: ViewMode): void }>();
28
+
29
+ const { t } = useLocale(() => props.locale);
30
+
31
+ function offers(v: ViewMode): boolean {
32
+ return !props.modes || props.modes.includes(v);
33
+ }
34
+ </script>
35
+
36
+ <template>
37
+ <div class="fe-toolbar__view" role="tablist" :aria-label="t('toolbar.view_label')">
38
+ <button
39
+ v-if="offers('list')"
40
+ type="button"
41
+ class="fe-btn fe-btn--icon-only"
42
+ :class="{ 'is-active': viewMode === 'list' }"
43
+ role="tab"
44
+ :aria-selected="viewMode === 'list'"
45
+ :title="t('toolbar.view.list')"
46
+ :aria-label="t('toolbar.view.list')"
47
+ data-testid="view-list"
48
+ @click="emit('update:viewMode', 'list')"
49
+ >
50
+ <span class="fe-icon" aria-hidden="true">☰</span>
51
+ </button>
52
+ <button
53
+ v-if="offers('grid')"
54
+ type="button"
55
+ class="fe-btn fe-btn--icon-only"
56
+ :class="{ 'is-active': viewMode === 'grid' }"
57
+ role="tab"
58
+ :aria-selected="viewMode === 'grid'"
59
+ :title="t('toolbar.view.grid')"
60
+ :aria-label="t('toolbar.view.grid')"
61
+ data-testid="view-grid"
62
+ @click="emit('update:viewMode', 'grid')"
63
+ >
64
+ <span class="fe-icon" aria-hidden="true">▦</span>
65
+ </button>
66
+ <button
67
+ v-if="offers('gallery')"
68
+ type="button"
69
+ class="fe-btn fe-btn--icon-only"
70
+ :class="{ 'is-active': viewMode === 'gallery' }"
71
+ role="tab"
72
+ :aria-selected="viewMode === 'gallery'"
73
+ :title="t('toolbar.view.gallery')"
74
+ :aria-label="t('toolbar.view.gallery')"
75
+ data-testid="view-gallery"
76
+ @click="emit('update:viewMode', 'gallery')"
77
+ >
78
+ <span class="fe-icon" aria-hidden="true">▣</span>
79
+ </button>
80
+ </div>
81
+ </template>
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import type { ExplorerConfig, AuthConfig, EndpointMap } from '../types/ExplorerConfig';
25
+ import { resolveLocale } from '../locales/resolve';
25
26
  import type {
26
27
  FileNode,
27
28
  ShareInfo,
@@ -77,6 +78,14 @@ export interface Grant {
77
78
  inherited?: boolean;
78
79
  }
79
80
 
81
+ /** surucu:d1 — `GET /api/files/quota/me` (quota.Snapshot). */
82
+ export interface QuotaSnapshot {
83
+ used_bytes: number;
84
+ quota_bytes: number;
85
+ percent_used: number;
86
+ unlimited: boolean;
87
+ }
88
+
80
89
  export interface PermissionsResponse {
81
90
  path: string;
82
91
  storage_rbac: boolean;
@@ -328,9 +337,10 @@ export function useFileApi(config: ExplorerConfig) {
328
337
 
329
338
  // Map an HTTP status to a short, human-readable message in the explorer's
330
339
  // locale. The raw JSON body is attached as `.detail` for debugging but never
331
- // shown in the toast (Ada: "404/403 falan verince ham json görüyorum").
340
+ // shown in the toast (Ada, translated from Turkish: "when it gives a 404/403
341
+ // or whatever, I see raw json").
332
342
  function statusMessage(status: number): string {
333
- const tr = (config.locale ?? 'tr') !== 'en';
343
+ const tr = resolveLocale(config.locale) !== 'en';
334
344
  const m: Record<number, [string, string]> = {
335
345
  400: ['Geçersiz istek', 'Bad request'],
336
346
  401: ['Oturum gerekli, tekrar giriş yapın', 'Sign-in required'],
@@ -457,6 +467,31 @@ export function useFileApi(config: ExplorerConfig) {
457
467
  return Array.isArray(data?.results) ? data.results : [];
458
468
  }
459
469
 
470
+ /* === surucu:d1 — the signed-in person's storage line ==================
471
+ * `GET /api/files/quota/me` → `{used_bytes, quota_bytes, percent_used,
472
+ * unlimited}` (internal/quota/service.go `Snapshot`). Derived from the
473
+ * manager endpoint the same way permissions and search are, so a proxy that
474
+ * forwards /api/files/* keeps working.
475
+ *
476
+ * ⚠ PER-USER, never per-storage: usage is `SUM(nodes.size) WHERE owner_id=me`
477
+ * and there is no per-provider quota. Anything labelling this figure with a
478
+ * drive's name is describing a number the server did not send.
479
+ *
480
+ * ⚠ Returns null rather than throwing on ANY failure — a server without the
481
+ * route (404), an app token with no person behind it (403) or an older build
482
+ * must leave the panel exactly as it was, not put an error where a status
483
+ * line goes.
484
+ */
485
+ async function quotaMe(): Promise<QuotaSnapshot | null> {
486
+ const base = endpoints.manager.replace(/\/manager(\?.*)?$/, '/quota/me');
487
+ try {
488
+ const q = await jsonFetch<QuotaSnapshot>(base);
489
+ return q && typeof q.used_bytes === 'number' ? q : null;
490
+ } catch {
491
+ return null;
492
+ }
493
+ }
494
+
460
495
  async function subfolders(path: string): Promise<{ folders: FileNode[] }> {
461
496
  return jsonFetch<{ folders: FileNode[] }>(managerUrl('subfolders', { path }));
462
497
  }
@@ -627,11 +662,22 @@ export function useFileApi(config: ExplorerConfig) {
627
662
  */
628
663
  async function fetchBlob(
629
664
  path: string,
665
+ opts: { fresh?: boolean } = {},
630
666
  ): Promise<{ url: string; blob: Blob; mime: string }> {
631
667
  const headers = await authHeaders();
632
668
  const res = await fetch(previewUrl(path), {
633
669
  headers,
634
670
  credentials: credentialsMode(),
671
+ // ⚠ The preview endpoint answers `Cache-Control: private, max-age=60`,
672
+ // which is right for the thing it was built for — a viewer re-opening
673
+ // an image — and wrong for anything the client treats as CONTROL data.
674
+ // `.filex-e2e.json` is control data: after the client rewrites it
675
+ // (recovery upgrade, escrow slot, escrow refusal) the next read inside
676
+ // that minute came back PRE-write, so the folder looked un-upgraded
677
+ // and the question filex had just been answered was asked again.
678
+ // Measured 2026-09-05 in a real browser: decline the escrow offer,
679
+ // reopen the folder, and the offer was back.
680
+ cache: opts.fresh ? 'no-store' : 'default',
635
681
  });
636
682
  if (!res.ok) {
637
683
  const text = await res.text().catch(() => '');
@@ -885,6 +931,7 @@ export function useFileApi(config: ExplorerConfig) {
885
931
  index,
886
932
  search,
887
933
  globalSearch /* bul:s3 */,
934
+ quotaMe /* surucu:d1 */,
888
935
  subfolders,
889
936
  newFolder,
890
937
  rename,
@@ -12,7 +12,7 @@ import { messages } from '../locales';
12
12
  export function useLocale(localeRef: Ref<LocaleCode> | (() => LocaleCode)) {
13
13
  const lookup = computed(() => {
14
14
  const code = typeof localeRef === 'function' ? localeRef() : localeRef.value;
15
- return messages[code] ?? messages.tr;
15
+ return messages[code] ?? messages.en;
16
16
  });
17
17
 
18
18
  function t(key: string, vars: Record<string, string | number> = {}): string {
@@ -14,9 +14,9 @@
14
14
  * are registered per row so the panel can route user intent back to the
15
15
  * owning surface without the store knowing any transport details.
16
16
  *
17
- * Lifecycle rules (per the İşlem Merkezi contract):
17
+ * Lifecycle rules (per the Operations Center contract):
18
18
  * - running rows live in `active`;
19
- * - done/aborted rows linger in `active` for DONE_LINGER_MS ("bitti"
19
+ * - done/aborted rows linger in `active` for DONE_LINGER_MS ("done"
20
20
  * flash) then move to `history` (session-only, capped);
21
21
  * - error rows are STICKY: they stay in `active` until the user
22
22
  * dismisses or retries them — even after the source list swept them
@@ -39,7 +39,7 @@ export interface OperationInput {
39
39
  percent: number | null;
40
40
  status: OperationStatus;
41
41
  error?: string | null;
42
- /** Queue op accepted but not started yet ("Sırada"). */
42
+ /** Queue op accepted but not started yet ("Queued"). */
43
43
  queued?: boolean;
44
44
  /** Progress counters for queue ops (3/5 items). */
45
45
  doneCount?: number;