@brftech/filex-core 0.39.1 → 0.41.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 (125) hide show
  1. package/README.md +32 -25
  2. package/dist/ArchiveViewer-BNqukFg8.js +124 -0
  3. package/dist/ArchiveViewer-BNqukFg8.js.map +1 -0
  4. package/dist/CsvViewer-ii_-MgmG.js +141 -0
  5. package/dist/CsvViewer-ii_-MgmG.js.map +1 -0
  6. package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-B2tuu4rX.js} +48 -42
  7. package/dist/DrawioViewer-B2tuu4rX.js.map +1 -0
  8. package/dist/EpubViewer-p4B8iiWb.js +163 -0
  9. package/dist/EpubViewer-p4B8iiWb.js.map +1 -0
  10. package/dist/IpynbViewer-D_qYRJhu.js +184 -0
  11. package/dist/IpynbViewer-D_qYRJhu.js.map +1 -0
  12. package/dist/MermaidViewer-3ZkfNM8v.js +137 -0
  13. package/dist/MermaidViewer-3ZkfNM8v.js.map +1 -0
  14. package/dist/PsdViewer-CuV-OPSd.js +122 -0
  15. package/dist/PsdViewer-CuV-OPSd.js.map +1 -0
  16. package/dist/TiffViewer-BLeoF4Be.js +142 -0
  17. package/dist/TiffViewer-BLeoF4Be.js.map +1 -0
  18. package/dist/Viewer3D-DHhhU76E.js +75 -0
  19. package/dist/Viewer3D-DHhhU76E.js.map +1 -0
  20. package/dist/filex-core.js +217 -20077
  21. package/dist/filex-core.js.map +1 -1
  22. package/dist/filex-core.umd.cjs +61 -62
  23. package/dist/filex-core.umd.cjs.map +1 -1
  24. package/dist/index-BraG7Cz4.js +26664 -0
  25. package/dist/index-BraG7Cz4.js.map +1 -0
  26. package/dist/index.d.ts +4276 -851
  27. package/dist/style.css +1 -1
  28. package/package.json +1 -1
  29. package/src/FileExplorer.vue +2170 -492
  30. package/src/components/AdvancedSearch.vue +870 -0
  31. package/src/components/Breadcrumb.vue +191 -19
  32. package/src/components/CommandPalette.vue +66 -22
  33. package/src/components/ConnectionsPanel.vue +6 -2
  34. package/src/components/ContextMenu.vue +57 -2
  35. package/src/components/EncryptedFolderModal.vue +31 -25
  36. package/src/components/FilePane.vue +939 -0
  37. package/src/components/FilterBar.vue +568 -72
  38. package/src/components/GalleryView.vue +162 -30
  39. package/src/components/GridView.vue +322 -58
  40. package/src/components/HomeView.vue +331 -0
  41. package/src/components/InspectorPanel.vue +296 -53
  42. package/src/components/ListView.vue +1158 -167
  43. package/src/components/NFSExportsPanel.vue +7 -4
  44. package/src/components/OnboardingTour.vue +156 -33
  45. package/src/components/QuickLook.vue +34 -5
  46. package/src/components/RecentlyOpened.vue +17 -2
  47. package/src/components/S3KeysPanel.vue +7 -4
  48. package/src/components/SSHKeysPanel.vue +7 -4
  49. package/src/components/ShortcutSettings.vue +43 -2
  50. package/src/components/ShortcutsHelp.vue +4 -1
  51. package/src/components/SideNav.vue +206 -140
  52. package/src/components/StorageFields.vue +19 -1
  53. package/src/components/TabBar.vue +87 -1
  54. package/src/components/TagPicker.vue +77 -6
  55. package/src/components/ThemeGallery.vue +13 -198
  56. package/src/components/ThemePalette.vue +244 -0
  57. package/src/components/TimeZoneDialog.vue +127 -0
  58. package/src/components/TimeZonePicker.vue +696 -0
  59. package/src/components/TokensPanel.vue +16 -4
  60. package/src/components/Toolbar.vue +965 -469
  61. package/src/components/ViewSwitcher.vue +9 -4
  62. package/src/composables/useConnections.ts +35 -4
  63. package/src/composables/useExplorerTimeZone.ts +85 -0
  64. package/src/composables/useFileApi.ts +53 -4
  65. package/src/composables/useKeyboardShortcuts.ts +122 -0
  66. package/src/composables/useLocale.ts +365 -25
  67. package/src/index.ts +182 -3
  68. package/src/lib/actionIcons.ts +330 -0
  69. package/src/lib/advSearch.ts +162 -0
  70. package/src/lib/dateGroups.ts +197 -0
  71. package/src/lib/destinationTree.ts +235 -0
  72. package/src/lib/downloadSelection.ts +154 -0
  73. package/src/lib/fileFilters.ts +319 -12
  74. package/src/lib/fileIcons.ts +299 -5
  75. package/src/lib/filePreview.ts +605 -0
  76. package/src/lib/listing.ts +72 -0
  77. package/src/lib/shareTtl.ts +11 -1
  78. package/src/lib/sortOrder.ts +459 -0
  79. package/src/lib/themes.ts +152 -23
  80. package/src/lib/timezone.ts +456 -0
  81. package/src/lib/uiProfile.ts +82 -0
  82. package/src/lib/viewPrefs.ts +1036 -0
  83. package/src/locales/en.ts +432 -25
  84. package/src/locales/tr.ts +425 -25
  85. package/src/modals/ConvertModal.vue +23 -3
  86. package/src/modals/DestinationPickerModal.vue +341 -0
  87. package/src/modals/Modal.vue +18 -4
  88. package/src/modals/NewDocumentModal.vue +525 -0
  89. package/src/modals/NewFolderModal.vue +15 -9
  90. package/src/modals/PermissionsModal.vue +543 -473
  91. package/src/modals/PreviewModal.vue +516 -156
  92. package/src/styles/base.css +5435 -8
  93. package/src/styles/variables.css +140 -60
  94. package/src/types/Connections.ts +9 -0
  95. package/src/types/ExplorerConfig.ts +144 -39
  96. package/src/types/FileNode.ts +28 -0
  97. package/src/viewers/ArchiveViewer.vue +46 -8
  98. package/src/viewers/CsvViewer.vue +19 -2
  99. package/src/viewers/DrawioViewer.vue +12 -1
  100. package/src/viewers/EpubViewer.vue +50 -6
  101. package/src/viewers/IpynbViewer.vue +19 -2
  102. package/src/viewers/MermaidViewer.vue +20 -3
  103. package/src/viewers/PdfViewer.vue +24 -4
  104. package/src/viewers/PsdViewer.vue +28 -4
  105. package/src/viewers/TiffViewer.vue +19 -2
  106. package/src/viewers/Viewer3D.vue +36 -2
  107. package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
  108. package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
  109. package/dist/CsvViewer-CqWeV8VO.js +0 -131
  110. package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
  111. package/dist/DrawioViewer-BNALOB04.js.map +0 -1
  112. package/dist/EpubViewer-BPmlImG7.js +0 -145
  113. package/dist/EpubViewer-BPmlImG7.js.map +0 -1
  114. package/dist/IpynbViewer-CDlQmuCV.js +0 -175
  115. package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
  116. package/dist/MermaidViewer-sfZWdR-d.js +0 -127
  117. package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
  118. package/dist/PsdViewer-DwuySZsb.js +0 -112
  119. package/dist/PsdViewer-DwuySZsb.js.map +0 -1
  120. package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
  121. package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
  122. package/dist/Viewer3D-CJJJgkuv.js +0 -60
  123. package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
  124. package/src/components/SecondaryPane.vue +0 -447
  125. package/src/modals/ShareModal.vue +0 -139
@@ -0,0 +1,331 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * HomeView — the overview the explorer lands on, INSIDE the shell.
4
+ *
5
+ * Three blocks, each answering a question somebody actually has on arrival:
6
+ *
7
+ * Storages which drives are mine to open? (one card per storage)
8
+ * Recent what was I just working on?
9
+ * Starred what did I mark to come back to?
10
+ *
11
+ * ⚠⚠ Why it is in this package and not in the web app. It used to be a
12
+ * separate PAGE there (`web/src/views/Home.vue`) with a header of its own — a
13
+ * logo, a refresh, an "All files" button, an apps grid, sign-out and a
14
+ * language switch — and no navigation panel at all, so moving between Home and
15
+ * the files replaced the whole screen and half of that header repeated a
16
+ * control the explorer already drew. Home is a VIEW, the way Recent and
17
+ * Starred are: same panel, same top bar, only the content area changes. A view
18
+ * of the explorer belongs to the explorer, and putting it here is also what
19
+ * makes the panel's "Home" row mean something inside `<filex-explorer>`
20
+ * instead of being a destination that only exists on one surface.
21
+ *
22
+ * ⚠ Recent and Starred render through GridView — the SAME card, with the same
23
+ * thumbnails, the same type tiles and the same right-click menu as the listing
24
+ * behind it. A second card implementation for the same object is how the two
25
+ * start disagreeing about what a .docx looks like (it already happened once:
26
+ * the web page deep-imported `fileIcons` precisely to avoid it).
27
+ *
28
+ * ⚠ No `<style>` block, scoped or otherwise — the package's CSS lives in
29
+ * `styles/base.css`. A scoped block compiles to `.cls[data-v-HASH]` and the
30
+ * hash does not match in the web-component build, so the rules would silently
31
+ * stop applying in every embed.
32
+ */
33
+ import { computed, ref } from 'vue';
34
+ import type { FileNode } from '../types/FileNode';
35
+ import type { LocaleCode } from '../types/ExplorerConfig';
36
+ import { useLocale } from '../composables/useLocale';
37
+ import { fileIconTile } from '../lib/fileIcons';
38
+ import { nameMatches } from '../lib/fileFilters';
39
+ import GridView from './GridView.vue';
40
+
41
+ export interface HomeStorage {
42
+ name: string;
43
+ label?: string;
44
+ driver?: string;
45
+ readOnly?: boolean;
46
+ /** Bytes this storage holds, when the server reported one. */
47
+ usedBytes?: number;
48
+ }
49
+
50
+ const props = defineProps<{
51
+ storages: HomeStorage[];
52
+ recent: FileNode[];
53
+ starred: FileNode[];
54
+ /** True while the two per-user lists are still in flight. */
55
+ loading?: boolean;
56
+ locale: LocaleCode;
57
+ /**
58
+ * surucu:d1-scope — what the filter row above this view has in its name box.
59
+ *
60
+ * ⚠ It narrows ALL THREE blocks, not the two made of files. Home's first
61
+ * block is the drive list, and "orada adam isterse storage ismi aratabilir"
62
+ * is precisely the case the owner asked for; a box that skipped the block
63
+ * with the fewest, longest-lived names in it would be the box that misses
64
+ * the one thing people look for here.
65
+ *
66
+ * ⚠ Through `lib/fileFilters`'s own `nameMatches`, which is the predicate the
67
+ * listing's box uses — so an accent folds here exactly as it folds there.
68
+ * Optional: an embedder that draws no filter row passes nothing and sees
69
+ * everything, which is what this view has always done.
70
+ */
71
+ nameFilter?: string;
72
+ /** Authenticated thumb resolver (useThumbs.src) — the grid needs it in an
73
+ * embed, where a bare root-relative `thumb_url` is not authenticated. */
74
+ thumbSrc?: (n: FileNode) => string | null;
75
+ }>();
76
+
77
+ const emit = defineEmits<{
78
+ (e: 'open-storage', name: string): void;
79
+ (e: 'open-node', node: FileNode): void;
80
+ (e: 'context-node', node: FileNode, ev: MouseEvent): void;
81
+ }>();
82
+
83
+ const { t, formatSize } = useLocale(() => props.locale);
84
+
85
+ /* surucu:d1-scope — the three narrowed blocks.
86
+ *
87
+ * ⚠ A storage is matched on the word ON THE CARD (`label || name`), which is
88
+ * also the only word a person can see to type. A card labelled "Arşiv" over a
89
+ * storage named `s3-archive` must answer to "arş"; matching the wire name
90
+ * instead would make the filter miss the thing it is pointing at.
91
+ */
92
+ const needle = computed(() => (props.nameFilter ?? '').trim());
93
+ const filtering = computed(() => needle.value !== '');
94
+ const shownStorages = computed(() =>
95
+ props.storages.filter((s) => nameMatches(s.label || s.name, needle.value)),
96
+ );
97
+ const shownRecent = computed(() =>
98
+ props.recent.filter((n) => nameMatches(n.basename || '', needle.value)),
99
+ );
100
+ const shownStarred = computed(() =>
101
+ props.starred.filter((n) => nameMatches(n.basename || '', needle.value)),
102
+ );
103
+ /** True when a block has rows but the filter hid all of them — a different
104
+ * fact from "you have starred nothing", and it must not borrow that sentence. */
105
+ function filteredEmpty(total: number, shown: number): boolean {
106
+ return filtering.value && total > 0 && shown === 0;
107
+ }
108
+
109
+ /**
110
+ * ⚠ One frozen empty set, not `new Set()` in the template. GridView takes the
111
+ * selection as a prop and a fresh Set on every render is a new identity every
112
+ * time, which re-runs everything downstream of it for a value that never
113
+ * changes. Nothing on Home is selectable: there is no selection bar, no
114
+ * checkbox column and no verb to apply — so the honest answer is "nothing",
115
+ * once.
116
+ */
117
+ const NO_SELECTION: Set<string> = new Set();
118
+
119
+ /**
120
+ * The storage card's glyph.
121
+ *
122
+ * ⚠ `mime_type: 'inode/storage'` is the whole point of this line. A storage row
123
+ * IS `type: 'dir'`, so asking for `{ type: 'dir' }` alone falls through to the
124
+ * plain folder branch and every drive on the first screen a person sees — the
125
+ * screen with the most drives on it — came out as a yellow folder. `storage` is
126
+ * a real icon family now (lib/fileIcons answers it BEFORE the folder branch),
127
+ * and it is the two-bay drive the navigation panel already draws for the same
128
+ * storages: one shape for a drive across the panel, the list, the grid and here.
129
+ *
130
+ * One constant rather than a per-card call: every storage resolves to the same
131
+ * family, so this is one string, not one per row.
132
+ */
133
+ const storageTile = fileIconTile({ type: 'dir', mime_type: 'inode/storage' });
134
+
135
+ /**
136
+ * What a storage card says under its name.
137
+ *
138
+ * ⚠ A size only when the server reported one, and it is the same quantity for
139
+ * everybody who gets it (`/api/admin/storages` for an operator, the
140
+ * RBAC-filtered `/api/files/quota/storages` for everybody else). The per-USER
141
+ * quota is a sum across every storage, so printing that here would be a number
142
+ * about the person under a label about the drive. With no figure the caption
143
+ * names the kind of thing instead: a card with no size line says less; a card
144
+ * with the wrong size line says something false.
145
+ */
146
+ function storageCaption(s: HomeStorage): string {
147
+ return typeof s.usedBytes === 'number' && s.usedBytes >= 0
148
+ ? t('drive.storage.used_unlimited', { used: formatSize(s.usedBytes) })
149
+ : t('conn.guide.storage');
150
+ }
151
+
152
+ /**
153
+ * A card opens what it names.
154
+ *
155
+ * ⚠ SINGLE click, and that is not an inconsistency with the listing behind it.
156
+ * A card in the listing has two jobs — click selects, double click opens — and
157
+ * on Home the first of those does not exist (see NO_SELECTION), so a click
158
+ * that only selected would be a click that did nothing at all.
159
+ *
160
+ * ⚠ Which is why the double click has to be swallowed: a real double click
161
+ * fires `click-card` twice, and two opens of a file are two viewer tabs. The
162
+ * guard is on the NODE, not on a timer alone — opening the same thing twice in
163
+ * a moment is the accident; opening two different things quickly is somebody
164
+ * working fast.
165
+ */
166
+ const lastOpen = ref<{ path: string; at: number }>({ path: '', at: 0 });
167
+ function openNode(n: FileNode) {
168
+ const now = Date.now();
169
+ if (n.path === lastOpen.value.path && now - lastOpen.value.at < 700) return;
170
+ lastOpen.value = { path: n.path, at: now };
171
+ emit('open-node', n);
172
+ }
173
+ </script>
174
+
175
+ <template>
176
+ <!-- ⚠ No title of its own, and as of 2026-09-13 nobody else draws one either.
177
+ The explorer used to render "Home" into the breadcrumb row above this;
178
+ the owner had it removed ("ver sayfa içinde salak bir Home yazısı var,
179
+ onu kaldıralım") because the panel row, the tab and the address bar
180
+ already say it three times over. The section headings below stay — they
181
+ name the blocks, not the page. -->
182
+ <div class="fe-home" data-testid="home-view">
183
+ <!-- ── Storages ──────────────────────────────────────────────────── -->
184
+ <section class="fe-home__section" data-testid="home-storages">
185
+ <h2 class="fe-home__heading">{{ t('sidenav.storages') }}</h2>
186
+
187
+ <div v-if="shownStorages.length" class="fe-home__storages">
188
+ <button
189
+ v-for="s in shownStorages"
190
+ :key="s.name"
191
+ type="button"
192
+ class="fe-home__storage"
193
+ data-testid="home-storage-card"
194
+ :data-storage="s.name"
195
+ :title="s.label || s.name"
196
+ @click="emit('open-storage', s.name)"
197
+ >
198
+ <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
199
+ <span class="fe-home__storage-tile" aria-hidden="true" v-html="storageTile"></span>
200
+ <span class="fe-home__storage-main">
201
+ <span class="fe-home__storage-label">{{ s.label || s.name }}</span>
202
+ <span class="fe-home__storage-meta">{{ storageCaption(s) }}</span>
203
+ </span>
204
+ </button>
205
+ </div>
206
+
207
+ <!-- ⚠ The filter hid them — NOT "no storages yet". Two different facts,
208
+ and borrowing the second sentence for the first tells an operator
209
+ their drives are gone. One row per block, `data-testid` included, so
210
+ a test can tell the two apart too. -->
211
+ <p
212
+ v-else-if="filteredEmpty(storages.length, shownStorages.length)"
213
+ class="fe-home__nomatch"
214
+ data-testid="home-storages-nomatch"
215
+ >
216
+ {{ t('filter.empty.title') }}
217
+ </p>
218
+
219
+ <div v-else class="fe-state fe-home__state" data-testid="home-storages-empty">
220
+ <svg
221
+ class="fe-state__art"
222
+ viewBox="0 0 120 100"
223
+ width="96"
224
+ height="80"
225
+ fill="none"
226
+ stroke="currentColor"
227
+ stroke-width="2"
228
+ stroke-linecap="round"
229
+ stroke-linejoin="round"
230
+ aria-hidden="true"
231
+ >
232
+ <rect x="26" y="30" width="68" height="18" rx="5" />
233
+ <rect x="26" y="54" width="68" height="18" rx="5" />
234
+ <path d="M38 39h.01M38 63h.01" />
235
+ </svg>
236
+ <p class="fe-state__title">{{ t('home.storages.empty') }}</p>
237
+ <p class="fe-state__hint">{{ t('home.storages.hint') }}</p>
238
+ </div>
239
+ </section>
240
+
241
+ <!-- ── Recent ────────────────────────────────────────────────────── -->
242
+ <section class="fe-home__section" data-testid="home-recent">
243
+ <h2 class="fe-home__heading">{{ t('sidenav.recent') }}</h2>
244
+
245
+ <GridView
246
+ v-if="shownRecent.length"
247
+ :files="shownRecent"
248
+ :selected="NO_SELECTION"
249
+ :locale="locale"
250
+ :thumb-src="thumbSrc"
251
+ @click-card="openNode"
252
+ @dbl-card="openNode"
253
+ @context-card="(n: FileNode, ev: MouseEvent) => emit('context-node', n, ev)"
254
+ />
255
+ <!-- ⚠ A block still loading says so, and must NOT fall through to the
256
+ empty state: every visit would otherwise flash "Nothing starred yet"
257
+ at somebody who has starred things. -->
258
+ <p v-else-if="loading" class="fe-home__loading">{{ t('loading') }}</p>
259
+ <p
260
+ v-else-if="filteredEmpty(recent.length, shownRecent.length)"
261
+ class="fe-home__nomatch"
262
+ data-testid="home-recent-nomatch"
263
+ >
264
+ {{ t('filter.empty.title') }}
265
+ </p>
266
+ <div v-else class="fe-state fe-home__state" data-testid="home-recent-empty">
267
+ <svg
268
+ class="fe-state__art"
269
+ viewBox="0 0 120 100"
270
+ width="96"
271
+ height="80"
272
+ fill="none"
273
+ stroke="currentColor"
274
+ stroke-width="2"
275
+ stroke-linecap="round"
276
+ stroke-linejoin="round"
277
+ aria-hidden="true"
278
+ >
279
+ <circle cx="60" cy="50" r="28" />
280
+ <path d="M60 32v18l12 8" />
281
+ </svg>
282
+ <p class="fe-state__title">{{ t('empty.recent.title') }}</p>
283
+ <p class="fe-state__hint">{{ t('empty.recent.hint') }}</p>
284
+ </div>
285
+ </section>
286
+
287
+ <!-- ── Starred ───────────────────────────────────────────────────── -->
288
+ <section class="fe-home__section" data-testid="home-starred">
289
+ <h2 class="fe-home__heading">{{ t('sidenav.starred') }}</h2>
290
+
291
+ <GridView
292
+ v-if="shownStarred.length"
293
+ :files="shownStarred"
294
+ :selected="NO_SELECTION"
295
+ :locale="locale"
296
+ :thumb-src="thumbSrc"
297
+ @click-card="openNode"
298
+ @dbl-card="openNode"
299
+ @context-card="(n: FileNode, ev: MouseEvent) => emit('context-node', n, ev)"
300
+ />
301
+ <p v-else-if="loading" class="fe-home__loading">{{ t('loading') }}</p>
302
+ <p
303
+ v-else-if="filteredEmpty(starred.length, shownStarred.length)"
304
+ class="fe-home__nomatch"
305
+ data-testid="home-starred-nomatch"
306
+ >
307
+ {{ t('filter.empty.title') }}
308
+ </p>
309
+ <div v-else class="fe-state fe-home__state" data-testid="home-starred-empty">
310
+ <svg
311
+ class="fe-state__art"
312
+ viewBox="0 0 120 100"
313
+ width="96"
314
+ height="80"
315
+ fill="none"
316
+ stroke="currentColor"
317
+ stroke-width="2"
318
+ stroke-linecap="round"
319
+ stroke-linejoin="round"
320
+ aria-hidden="true"
321
+ >
322
+ <path
323
+ d="M60 26l9 18.6 20.4 3-14.8 14.4 3.5 20.4L60 72.8 41.9 82.4l3.5-20.4L30.6 47.6l20.4-3z"
324
+ />
325
+ </svg>
326
+ <p class="fe-state__title">{{ t('empty.starred.title') }}</p>
327
+ <p class="fe-state__hint">{{ t('empty.starred.hint') }}</p>
328
+ </div>
329
+ </section>
330
+ </div>
331
+ </template>