@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
@@ -30,7 +30,8 @@
30
30
  */
31
31
  import { computed, ref } from 'vue';
32
32
  import { useLocale } from '../composables/useLocale';
33
- import type { LocaleCode } from '../types/ExplorerConfig';
33
+ import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
34
+ import ContextMenu, { type ContextAction } from './ContextMenu.vue';
34
35
 
35
36
  /** The virtual listings the panel can open. '' = an ordinary folder. */
36
37
  export type NavView = '' | 'recent' | 'starred' | 'shared' | 'trash' | 'tag';
@@ -98,6 +99,37 @@ const props = defineProps<{
98
99
  /** RBAC/root state — false hides the write affordances. */
99
100
  canWrite?: boolean;
100
101
  locale: LocaleCode;
102
+ /* === surucu:d1 — the Drive shell ==================================== */
103
+ /**
104
+ * Fold the primary actions into ONE "+ New" menu (`uiProfile: 'drive'`).
105
+ *
106
+ * Absent/false keeps the two-button block every other profile has had since
107
+ * v0.30.1 — Upload as the primary, New folder one step quieter — so this is
108
+ * additive and nothing that mounts the package today moves.
109
+ *
110
+ * ⚠ The menu holds what the explorer can ALREADY do: upload files, make a
111
+ * folder (the modal offers the encrypted variant from inside itself), and
112
+ * ask somebody else for files. There is deliberately no "Upload folder": the
113
+ * upload path takes a flat `File[]` and would have to create the intermediate
114
+ * directories itself, and an entry that quietly flattens someone's folder into
115
+ * one heap is worse than an entry that is not there.
116
+ */
117
+ newMenu?: boolean;
118
+ /** Offer "Request files" in that menu — a folder we may write to and share. */
119
+ canRequestFiles?: boolean;
120
+ /**
121
+ * The signed-in person's storage line, from `GET /api/files/quota/me`. Null
122
+ * (the default) renders nothing at all.
123
+ *
124
+ * ⚠ It is a PER-USER figure, not this storage's — `quota.Snapshot` sums
125
+ * `nodes.size WHERE owner_id = me`, and there is no per-provider quota
126
+ * (internal/quota/service.go). The mockup labels it with the drive's name;
127
+ * that would be a lie about which number this is, so the label says
128
+ * "Storage" and the drive name stays out of it.
129
+ */
130
+ quota?: { used: number; total: number; unlimited: boolean } | null;
131
+ /** Resolved theme — the teleported New menu leaves the `.fe` variable scope. */
132
+ theme?: ThemeMode;
101
133
  }>();
102
134
 
103
135
  const emit = defineEmits<{
@@ -110,6 +142,8 @@ const emit = defineEmits<{
110
142
  (e: 'new-folder'): void;
111
143
  (e: 'open-connections'): void;
112
144
  (e: 'open-tokens'): void;
145
+ /* surucu:d1 — "Request files": the access modal on THIS folder, drop tab. */
146
+ (e: 'request-files'): void;
113
147
  /** Drawer scrim / Esc — narrow mode only. */
114
148
  (e: 'close'): void;
115
149
  }>();
@@ -168,6 +202,74 @@ const hiddenTagCount = computed(() => Math.max(0, allTags.value.length - visible
168
202
  * answer is still in flight. */
169
203
  const showTags = computed(() => !!props.tagsLoaded || allTags.value.length > 0);
170
204
 
205
+ /* === surucu:d1 — the "+ New" menu ====================================
206
+ * One primary action instead of two buttons, the shape the reporter drew and
207
+ * the shape Drive/FileRun/Nextcloud share. It is the SAME ContextMenu the
208
+ * right-click menu uses — teleported to <body>, so the panel's own scroll
209
+ * container cannot clip it, and one keyboard/focus behaviour for both. */
210
+ const newBtnEl = ref<HTMLElement | null>(null);
211
+ const newMenuRef = ref<InstanceType<typeof ContextMenu> | null>(null);
212
+
213
+ const newActions = computed<ContextAction[]>(() => {
214
+ const list: ContextAction[] = [
215
+ { key: 'upload', label: t('drive.new.upload'), icon: '⬆', disabled: !writable.value },
216
+ { key: 'new-folder', label: t('drive.new.folder'), icon: '📁', disabled: !writable.value },
217
+ ];
218
+ // Only when there is a real folder to hang a drop link on. Rendered as a
219
+ // disabled row rather than dropped, so the menu does not change height
220
+ // between folders — a menu whose items move is a menu people misclick.
221
+ list.push({ divider: true, key: 'new-sep', label: '' });
222
+ list.push({
223
+ key: 'request-files',
224
+ label: t('drive.new.request'),
225
+ icon: '🔗',
226
+ disabled: !props.canRequestFiles,
227
+ });
228
+ return list;
229
+ });
230
+
231
+ function openNewMenu() {
232
+ const r = newBtnEl.value?.getBoundingClientRect();
233
+ newMenuRef.value?.show(
234
+ { clientX: r ? r.left : 0, clientY: r ? r.bottom + 6 : 0 } as MouseEvent,
235
+ [],
236
+ );
237
+ }
238
+
239
+ function onNewSelect(a: ContextAction) {
240
+ if (a.key === 'upload') emit('upload');
241
+ else if (a.key === 'new-folder') emit('new-folder');
242
+ else if (a.key === 'request-files') emit('request-files');
243
+ }
244
+
245
+ /* === surucu:d1 — the storage line =================================== */
246
+ function formatBytes(n: number): string {
247
+ if (!Number.isFinite(n) || n < 0) return '—';
248
+ const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
249
+ let v = n;
250
+ let i = 0;
251
+ while (v >= 1024 && i < units.length - 1) {
252
+ v /= 1024;
253
+ i += 1;
254
+ }
255
+ const digits = v < 10 && i > 0 ? 1 : 0;
256
+ return `${v.toFixed(digits)} ${units[i]}`;
257
+ }
258
+
259
+ const quotaPercent = computed(() => {
260
+ const q = props.quota;
261
+ if (!q || q.unlimited || q.total <= 0) return 0;
262
+ return Math.max(0, Math.min(100, Math.round((q.used / q.total) * 100)));
263
+ });
264
+
265
+ const quotaText = computed(() => {
266
+ const q = props.quota;
267
+ if (!q) return '';
268
+ return q.unlimited || q.total <= 0
269
+ ? t('drive.storage.used_unlimited', { used: formatBytes(q.used) })
270
+ : t('drive.storage.used', { used: formatBytes(q.used), total: formatBytes(q.total) });
271
+ });
272
+
171
273
  const toggleLabel = computed(() =>
172
274
  props.narrow
173
275
  ? t('sidenav.close')
@@ -238,7 +340,35 @@ const toggleLabel = computed(() =>
238
340
  that appears and disappears makes every row below it jump by 90px each
239
341
  time the user opens a view, which reads as the panel reloading. -->
240
342
  <div class="fe-sidenav__primary">
343
+ <!-- surucu:d1 — one primary action, the shape #14's mockups draw. The
344
+ two-button block below is what every other profile still renders. -->
345
+ <button
346
+ v-if="newMenu"
347
+ ref="newBtnEl"
348
+ type="button"
349
+ class="fe-sidenav__new"
350
+ aria-haspopup="menu"
351
+ :title="t('drive.new')"
352
+ :aria-label="t('drive.new')"
353
+ data-testid="sidenav-new"
354
+ @click="openNewMenu"
355
+ >
356
+ <svg
357
+ class="fe-ficon"
358
+ viewBox="0 0 24 24"
359
+ fill="none"
360
+ stroke="currentColor"
361
+ stroke-width="2"
362
+ stroke-linecap="round"
363
+ aria-hidden="true"
364
+ focusable="false"
365
+ >
366
+ <path d="M12 5v14M5 12h14" />
367
+ </svg>
368
+ <span v-if="showLabels" class="fe-sidenav__text">{{ t('drive.new') }}</span>
369
+ </button>
241
370
  <button
371
+ v-if="!newMenu"
242
372
  type="button"
243
373
  class="fe-sidenav__upload"
244
374
  :disabled="!writable"
@@ -265,6 +395,7 @@ const toggleLabel = computed(() =>
265
395
  <span v-if="showLabels" class="fe-sidenav__text">{{ t('toolbar.upload') }}</span>
266
396
  </button>
267
397
  <button
398
+ v-if="!newMenu"
268
399
  type="button"
269
400
  class="fe-sidenav__secondary"
270
401
  :disabled="!writable"
@@ -568,5 +699,34 @@ const toggleLabel = computed(() =>
568
699
  </ul>
569
700
  </div>
570
701
  </div>
702
+
703
+ <!-- surucu:d1 — the storage line. Under the navigation, above nothing:
704
+ it is the last thing in the panel because it is a status, not a
705
+ destination. Hidden entirely when the server did not answer (an app
706
+ token has no person to have a quota) rather than drawn empty. -->
707
+ <div v-if="quota" class="fe-sidenav__quota" data-testid="sidenav-quota">
708
+ <p v-if="showLabels" class="fe-sidenav__quota-label">{{ t('drive.storage.label') }}</p>
709
+ <div
710
+ class="fe-sidenav__quota-bar"
711
+ role="progressbar"
712
+ :aria-valuenow="quotaPercent"
713
+ aria-valuemin="0"
714
+ aria-valuemax="100"
715
+ :aria-label="quotaText"
716
+ :title="quotaText"
717
+ >
718
+ <span class="fe-sidenav__quota-fill" :style="{ width: quotaPercent + '%' }"></span>
719
+ </div>
720
+ <p v-if="showLabels" class="fe-sidenav__quota-text">{{ quotaText }}</p>
721
+ </div>
722
+
723
+ <ContextMenu
724
+ v-if="newMenu"
725
+ ref="newMenuRef"
726
+ :locale="locale"
727
+ :theme="theme"
728
+ :actions="newActions"
729
+ @select="onNewSelect"
730
+ />
571
731
  </nav>
572
732
  </template>
@@ -10,6 +10,7 @@ import { ref, watch, computed } from 'vue';
10
10
  import { setNodeStarred } from '../lib/star';
11
11
  import { useLocale } from '../composables/useLocale';
12
12
  import type { LocaleCode } from '../types/ExplorerConfig';
13
+ import { resolveLocale } from '../locales/resolve';
13
14
 
14
15
  const props = defineProps<{
15
16
  starred: boolean;
@@ -45,7 +46,7 @@ const emit = defineEmits<{
45
46
  const local = ref(props.starred);
46
47
  watch(() => props.starred, (v) => { local.value = v; });
47
48
 
48
- const { t } = useLocale(() => props.locale ?? 'tr');
49
+ const { t } = useLocale(() => resolveLocale(props.locale));
49
50
  const label = computed(() => t(local.value ? 'ctx.unstar' : 'ctx.star'));
50
51
 
51
52
  async function toggle() {
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * TabBar — wiring:d1 sekme şeridi.
3
+ * TabBar — wiring:d1 the tab strip.
4
4
  *
5
5
  * Thin strip that sits ABOVE the toolbar. The host renders it only when
6
6
  * 2+ tabs exist (embed pixel-parity: a single tab shows nothing at all).
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Interactions:
10
10
  * click tab → select
11
- * × / middle-tık → close
11
+ * ×/middle-click → close
12
12
  * + → new tab (clone of the current location)
13
13
  * drag → reorder (HTML5 DnD; live-swap while dragging)
14
14
  * ⫿ (split) → toggle the active tab's secondary pane
@@ -6,7 +6,7 @@
6
6
  * toolbar chips + sample rows) painted with that theme's tokens for the
7
7
  * CURRENTLY resolved light/dark mode — so the card shows exactly what
8
8
  * you'd get. Click applies instantly (live preview behind the modal) and
9
- * persists; the selected card carries a ✓. "Varsayılana dön" resets.
9
+ * persists; the selected card carries a ✓. "Reset to default" resets.
10
10
  *
11
11
  * Presentational only — selection state + persistence live in
12
12
  * lib/themes.ts (shared across instances); the parent forwards `select`.
@@ -30,6 +30,7 @@ import type { ExplorerConfig, LocaleCode } from '../types/ExplorerConfig';
30
30
  import type { ApiToken } from '../types/Tokens';
31
31
  import { useLocale } from '../composables/useLocale';
32
32
  import { useTokens } from '../composables/useTokens';
33
+ import { resolveLocale } from '../locales/resolve';
33
34
 
34
35
  const props = defineProps<{
35
36
  config: ExplorerConfig;
@@ -50,7 +51,7 @@ const emit = defineEmits<{
50
51
  (e: 'active', v: { hasToken: boolean }): void;
51
52
  }>();
52
53
 
53
- const locale = computed<LocaleCode>(() => props.config.locale ?? 'tr');
54
+ const locale = computed<LocaleCode>(() => resolveLocale(props.config.locale));
54
55
  const { t } = useLocale(locale);
55
56
 
56
57
  const { tokens, loading, error, canMint, revealed, load, create, remove, dismiss } = useTokens(