@brftech/filex-core 0.2.0 → 0.4.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 (37) hide show
  1. package/dist/{ArchiveViewer-DCZ1FZYV.js → ArchiveViewer-Dgvl43gn.js} +2 -2
  2. package/dist/{ArchiveViewer-DCZ1FZYV.js.map → ArchiveViewer-Dgvl43gn.js.map} +1 -1
  3. package/dist/{CsvViewer-DMvnp82x.js → CsvViewer-gWO5pc8F.js} +2 -2
  4. package/dist/{CsvViewer-DMvnp82x.js.map → CsvViewer-gWO5pc8F.js.map} +1 -1
  5. package/dist/{DrawioViewer-B5Io0yTO.js → DrawioViewer-DUAlRIAb.js} +2 -2
  6. package/dist/{DrawioViewer-B5Io0yTO.js.map → DrawioViewer-DUAlRIAb.js.map} +1 -1
  7. package/dist/{EpubViewer-z6RZd3K8.js → EpubViewer-DIXzuQWK.js} +2 -2
  8. package/dist/{EpubViewer-z6RZd3K8.js.map → EpubViewer-DIXzuQWK.js.map} +1 -1
  9. package/dist/{IpynbViewer-DfL4EauH.js → IpynbViewer-CJ-6pU_Z.js} +2 -2
  10. package/dist/{IpynbViewer-DfL4EauH.js.map → IpynbViewer-CJ-6pU_Z.js.map} +1 -1
  11. package/dist/{MermaidViewer-QNl_o7V-.js → MermaidViewer-CyQQDyRb.js} +2 -2
  12. package/dist/{MermaidViewer-QNl_o7V-.js.map → MermaidViewer-CyQQDyRb.js.map} +1 -1
  13. package/dist/{PsdViewer-Btmpr0Fz.js → PsdViewer-ChfaXVgN.js} +2 -2
  14. package/dist/{PsdViewer-Btmpr0Fz.js.map → PsdViewer-ChfaXVgN.js.map} +1 -1
  15. package/dist/{TiffViewer-CMLpwds5.js → TiffViewer-CsfD-NG6.js} +2 -2
  16. package/dist/{TiffViewer-CMLpwds5.js.map → TiffViewer-CsfD-NG6.js.map} +1 -1
  17. package/dist/{Viewer3D-gf3cb2gv.js → Viewer3D-BQ2XYmkN.js} +2 -2
  18. package/dist/{Viewer3D-gf3cb2gv.js.map → Viewer3D-BQ2XYmkN.js.map} +1 -1
  19. package/dist/filex-core.js +1 -1
  20. package/dist/filex-core.umd.cjs +42 -42
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-PnhGunSA.js +7972 -0
  23. package/dist/index-PnhGunSA.js.map +1 -0
  24. package/dist/index.d.ts +69 -2
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +146 -0
  28. package/src/components/ContextMenu.vue +101 -3
  29. package/src/components/InspectorPanel.vue +455 -0
  30. package/src/components/Toolbar.vue +250 -5
  31. package/src/composables/useFileApi.ts +59 -0
  32. package/src/composables/useKeyboardShortcuts.ts +11 -0
  33. package/src/locales/en.ts +45 -0
  34. package/src/locales/tr.ts +45 -0
  35. package/src/styles/base.css +458 -0
  36. package/dist/index-D49iw00E.js +0 -7119
  37. package/dist/index-D49iw00E.js.map +0 -1
@@ -0,0 +1,455 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * InspectorPanel — koru:k1 details ("Ayrıntılar") side panel.
4
+ *
5
+ * Rendered as a flex sibling of `.fe__body` (right side, ~300px). In
6
+ * `fe--narrow` embeds it becomes a full-size overlay with its own close
7
+ * button (Esc handled by FileExplorer's shortcut chain).
8
+ *
9
+ * Sections:
10
+ * - Genel : icon/thumb, name, path (copy), size, modified, mime, etag.
11
+ * Multi-select → "N items, total X" summary; no selection →
12
+ * current-folder summary.
13
+ * - Sürümler : version history via GET /api/files/versions?node_id=…
14
+ * + restore (with optional snapshot-current) + snapshot-now.
15
+ * The section hides silently when the backend gates the
16
+ * endpoint (401/403) or doesn't ship it (404).
17
+ * - İzinler : effective RBAC level badge + "manage" (opens the existing
18
+ * PermissionsModal through the host). Only when ACL is
19
+ * enforced on the storage (perm is a non-empty string).
20
+ * - Paylaşımlar: existing share links (GET /api/files/share?path=…) with
21
+ * copy buttons. Hidden silently when the list call fails
22
+ * (viewer-level users get 403 by design).
23
+ *
24
+ * The panel is mounted with v-if by the host — closed state leaves zero DOM.
25
+ */
26
+ import { computed, ref, watch } from 'vue';
27
+ import type { FileApi, NodeVersion } from '../composables/useFileApi';
28
+ import type { FileNode, ShareInfo } from '../types/FileNode';
29
+ import type { LocaleCode } from '../types/ExplorerConfig';
30
+ import { useLocale } from '../composables/useLocale';
31
+ import { fileIconSvg } from '../lib/fileIcons';
32
+
33
+ const props = defineProps<{
34
+ api: FileApi;
35
+ /** Current selection (empty array → current-folder summary). */
36
+ nodes: FileNode[];
37
+ /** Display label of the folder being viewed (for the no-selection state). */
38
+ dirLabel: string;
39
+ /** Number of entries in the folder being viewed. */
40
+ dirCount: number;
41
+ /** RBAC effective level of the current dir ('' = ACL not enforced). */
42
+ dirPerm?: string;
43
+ locale: LocaleCode;
44
+ /** Narrow/embed mode → full-size overlay presentation. */
45
+ narrow?: boolean;
46
+ /** Authenticated thumbnail resolver (useThumbs.src). Optional. */
47
+ thumbSrc?: (n: FileNode) => string | null;
48
+ }>();
49
+
50
+ const emit = defineEmits<{
51
+ (e: 'close'): void;
52
+ (e: 'manage-permissions', node: FileNode): void;
53
+ (e: 'toast', message: string): void;
54
+ /** Fired after a successful restore/snapshot so the host can reload. */
55
+ (e: 'changed'): void;
56
+ }>();
57
+
58
+ const { t, formatSize, nodeDisplayName } = useLocale(() => props.locale);
59
+
60
+ // ── selection shape ──────────────────────────────────────────────────
61
+ const single = computed<FileNode | null>(() =>
62
+ props.nodes.length === 1 ? props.nodes[0] : null,
63
+ );
64
+ const isMulti = computed(() => props.nodes.length > 1);
65
+ const isFile = computed(() => single.value?.type === 'file');
66
+ const nodeId = computed<number | null>(() =>
67
+ typeof single.value?.id === 'number' ? (single.value.id as number) : null,
68
+ );
69
+ const multiTotal = computed(() =>
70
+ props.nodes.reduce((acc, n) => acc + (typeof n.size === 'number' ? n.size : 0), 0),
71
+ );
72
+ const etag = computed<string | null>(() => {
73
+ const v = single.value?.etag;
74
+ return typeof v === 'string' && v !== '' ? v : null;
75
+ });
76
+ const thumb = computed<string | null>(() =>
77
+ single.value && isFile.value && props.thumbSrc ? props.thumbSrc(single.value) : null,
78
+ );
79
+
80
+ function shortHash(h: string): string {
81
+ return h.length > 12 ? `${h.slice(0, 12)}…` : h;
82
+ }
83
+
84
+ function formatDate(ms: number | undefined): string {
85
+ if (!ms) return '—';
86
+ try {
87
+ return new Date(ms).toLocaleString(props.locale === 'en' ? 'en-GB' : 'tr-TR', {
88
+ dateStyle: 'medium',
89
+ timeStyle: 'short',
90
+ });
91
+ } catch {
92
+ return new Date(ms).toISOString();
93
+ }
94
+ }
95
+
96
+ function formatDateStr(s: string | undefined | null): string {
97
+ if (!s) return '—';
98
+ const ms = Date.parse(s);
99
+ return Number.isNaN(ms) ? s : formatDate(ms);
100
+ }
101
+
102
+ async function copyText(text: string): Promise<void> {
103
+ try {
104
+ await navigator.clipboard.writeText(text);
105
+ emit('toast', t('inspector.copied'));
106
+ } catch {
107
+ emit('toast', text);
108
+ }
109
+ }
110
+
111
+ // ── RBAC (İzinler) ───────────────────────────────────────────────────
112
+ // Effective level for the selected item: its own perm, else the dir's.
113
+ // A non-empty string means ACL is enforced → section shows.
114
+ const effectivePerm = computed<string>(() => {
115
+ if (!single.value) return '';
116
+ // Backends may send '' when ACL is off — widen past the declared union.
117
+ const own = single.value.perm as string | undefined;
118
+ if (typeof own === 'string' && own !== '') return own;
119
+ return props.dirPerm || '';
120
+ });
121
+ const canManagePerms = computed(
122
+ () => effectivePerm.value === 'editor' || effectivePerm.value === 'owner',
123
+ );
124
+ function permLabel(level: string): string {
125
+ return t(`inspector.perm.${level}`) === `inspector.perm.${level}`
126
+ ? level
127
+ : t(`inspector.perm.${level}`);
128
+ }
129
+
130
+ // ── versions state ───────────────────────────────────────────────────
131
+ type SectionState = 'idle' | 'loading' | 'ok' | 'hidden' | 'error';
132
+ const versionsState = ref<SectionState>('idle');
133
+ const versions = ref<NodeVersion[]>([]);
134
+ const confirmVersionId = ref<number | null>(null);
135
+ const snapshotFirst = ref(true);
136
+ const versionBusy = ref(false);
137
+
138
+ // ── shares state ─────────────────────────────────────────────────────
139
+ const sharesState = ref<SectionState>('idle');
140
+ const shares = ref<ShareInfo[]>([]);
141
+
142
+ // Race guard: only the latest refresh may write state.
143
+ let refreshSeq = 0;
144
+
145
+ async function refresh(): Promise<void> {
146
+ const seq = ++refreshSeq;
147
+ confirmVersionId.value = null;
148
+ versions.value = [];
149
+ shares.value = [];
150
+
151
+ // Versions — single file with a backend node id only.
152
+ if (single.value && isFile.value && nodeId.value != null) {
153
+ versionsState.value = 'loading';
154
+ void loadVersions(seq);
155
+ } else {
156
+ versionsState.value = 'hidden';
157
+ }
158
+
159
+ // Shares — any single selection (files and folders both shareable).
160
+ if (single.value) {
161
+ sharesState.value = 'loading';
162
+ const path = single.value.path;
163
+ try {
164
+ const { shares: list } = await props.api.listShares(path);
165
+ if (seq !== refreshSeq) return;
166
+ shares.value = Array.isArray(list) ? list : [];
167
+ sharesState.value = 'ok';
168
+ } catch {
169
+ if (seq !== refreshSeq) return;
170
+ // 403 = viewer-level user (by design), anything else — auxiliary
171
+ // info, hide silently rather than alarm.
172
+ sharesState.value = 'hidden';
173
+ }
174
+ } else {
175
+ sharesState.value = 'hidden';
176
+ }
177
+ }
178
+
179
+ async function loadVersions(seq: number): Promise<void> {
180
+ const id = nodeId.value;
181
+ if (id == null) return;
182
+ try {
183
+ const list = await props.api.listVersions(id);
184
+ if (seq !== refreshSeq) return;
185
+ versions.value = list;
186
+ versionsState.value = 'ok';
187
+ } catch (err) {
188
+ if (seq !== refreshSeq) return;
189
+ const status = (err as { status?: number }).status;
190
+ // Gated (401/403) or absent (404) endpoint → section silently hidden.
191
+ if (status === 401 || status === 403 || status === 404) {
192
+ versionsState.value = 'hidden';
193
+ } else {
194
+ versionsState.value = 'error';
195
+ }
196
+ }
197
+ }
198
+
199
+ function askRestore(v: NodeVersion): void {
200
+ confirmVersionId.value = v.id;
201
+ snapshotFirst.value = true;
202
+ }
203
+
204
+ async function confirmRestore(v: NodeVersion): Promise<void> {
205
+ const id = nodeId.value;
206
+ if (id == null || versionBusy.value) return;
207
+ versionBusy.value = true;
208
+ try {
209
+ await props.api.restoreVersion(id, v.id, snapshotFirst.value);
210
+ emit('toast', t('inspector.versions.restored'));
211
+ confirmVersionId.value = null;
212
+ emit('changed');
213
+ await loadVersions(++refreshSeq);
214
+ } catch (err) {
215
+ emit('toast', (err as Error).message);
216
+ } finally {
217
+ versionBusy.value = false;
218
+ }
219
+ }
220
+
221
+ async function takeSnapshot(): Promise<void> {
222
+ const id = nodeId.value;
223
+ if (id == null || versionBusy.value) return;
224
+ versionBusy.value = true;
225
+ try {
226
+ await props.api.snapshotVersion(id);
227
+ emit('toast', t('inspector.versions.snapshotted'));
228
+ await loadVersions(++refreshSeq);
229
+ } catch (err) {
230
+ const status = (err as { status?: number }).status;
231
+ // Older backends don't ship POST /versions/snapshot yet.
232
+ if (status === 404 || status === 405 || status === 501) {
233
+ emit('toast', t('inspector.versions.unsupported'));
234
+ } else {
235
+ emit('toast', (err as Error).message);
236
+ }
237
+ } finally {
238
+ versionBusy.value = false;
239
+ }
240
+ }
241
+
242
+ watch(
243
+ () => props.nodes.map((n) => n.path).join(''),
244
+ () => void refresh(),
245
+ { immediate: true },
246
+ );
247
+ </script>
248
+
249
+ <template>
250
+ <aside
251
+ class="fe-inspector"
252
+ :class="{ 'fe-inspector--overlay': narrow }"
253
+ role="complementary"
254
+ :aria-label="t('inspector.title')"
255
+ >
256
+ <header class="fe-inspector__head">
257
+ <h2 class="fe-inspector__title">{{ t('inspector.title') }}</h2>
258
+ <button
259
+ type="button"
260
+ class="fe-inspector__close"
261
+ :title="t('inspector.close')"
262
+ :aria-label="t('inspector.close')"
263
+ @click="emit('close')"
264
+ >×</button>
265
+ </header>
266
+
267
+ <div class="fe-inspector__scroll">
268
+ <!-- ══ Genel ══ -->
269
+ <section class="fe-inspector__section">
270
+ <h3 class="fe-inspector__heading">{{ t('inspector.section.general') }}</h3>
271
+
272
+ <!-- Multi selection → summary -->
273
+ <div v-if="isMulti" class="fe-inspector__hero">
274
+ <span class="fe-inspector__bigicon" v-html="fileIconSvg({ type: 'dir' })"></span>
275
+ <p class="fe-inspector__name">
276
+ {{ t('inspector.items_summary', { n: nodes.length, size: formatSize(multiTotal) }) }}
277
+ </p>
278
+ </div>
279
+
280
+ <!-- Single selection → full meta -->
281
+ <template v-else-if="single">
282
+ <div class="fe-inspector__hero">
283
+ <img
284
+ v-if="thumb"
285
+ class="fe-inspector__thumb"
286
+ :src="thumb"
287
+ alt=""
288
+ aria-hidden="true"
289
+ />
290
+ <span
291
+ v-else
292
+ class="fe-inspector__bigicon"
293
+ v-html="fileIconSvg(single)"
294
+ ></span>
295
+ <p class="fe-inspector__name" :title="single.basename">
296
+ {{ nodeDisplayName(single) }}
297
+ </p>
298
+ </div>
299
+
300
+ <dl class="fe-inspector__meta">
301
+ <div class="fe-inspector__row">
302
+ <dt>{{ t('inspector.path') }}</dt>
303
+ <dd class="fe-inspector__pathcell">
304
+ <span class="fe-inspector__path" :title="single.path">{{ single.path }}</span>
305
+ <button
306
+ type="button"
307
+ class="fe-inspector__copy"
308
+ :title="t('inspector.copy')"
309
+ :aria-label="t('inspector.copy')"
310
+ @click="copyText(single.path)"
311
+ >⧉</button>
312
+ </dd>
313
+ </div>
314
+ <div v-if="isFile" class="fe-inspector__row">
315
+ <dt>{{ t('inspector.size') }}</dt>
316
+ <dd>{{ formatSize(typeof single.size === 'number' ? single.size : null) }}</dd>
317
+ </div>
318
+ <div class="fe-inspector__row">
319
+ <dt>{{ t('inspector.modified') }}</dt>
320
+ <dd>{{ formatDate(single.last_modified) }}</dd>
321
+ </div>
322
+ <div v-if="isFile && single.mime_type" class="fe-inspector__row">
323
+ <dt>{{ t('inspector.mime') }}</dt>
324
+ <dd class="fe-inspector__mime">{{ single.mime_type }}</dd>
325
+ </div>
326
+ <div v-if="etag" class="fe-inspector__row">
327
+ <dt>{{ t('inspector.etag') }}</dt>
328
+ <dd class="fe-inspector__pathcell">
329
+ <span class="fe-inspector__path" :title="etag">{{ shortHash(etag) }}</span>
330
+ <button
331
+ type="button"
332
+ class="fe-inspector__copy"
333
+ :title="t('inspector.copy')"
334
+ :aria-label="t('inspector.copy')"
335
+ @click="copyText(etag)"
336
+ >⧉</button>
337
+ </dd>
338
+ </div>
339
+ </dl>
340
+ </template>
341
+
342
+ <!-- No selection → current folder summary -->
343
+ <div v-else class="fe-inspector__hero">
344
+ <span class="fe-inspector__bigicon" v-html="fileIconSvg({ type: 'dir' })"></span>
345
+ <p class="fe-inspector__name" :title="dirLabel">{{ dirLabel }}</p>
346
+ <p class="fe-inspector__sub">{{ t('inspector.folder_items', { n: dirCount }) }}</p>
347
+ </div>
348
+ </section>
349
+
350
+ <!-- ══ Sürümler ══ -->
351
+ <section
352
+ v-if="versionsState === 'ok' || versionsState === 'error'"
353
+ class="fe-inspector__section"
354
+ >
355
+ <h3 class="fe-inspector__heading">{{ t('inspector.section.versions') }}</h3>
356
+
357
+ <p v-if="versionsState === 'error'" class="fe-inspector__empty">
358
+ {{ t('inspector.error') }}
359
+ </p>
360
+ <template v-else>
361
+ <p v-if="versions.length === 0" class="fe-inspector__empty">
362
+ {{ t('inspector.versions.empty') }}
363
+ </p>
364
+ <ul v-else class="fe-inspector__versions">
365
+ <li v-for="v in versions" :key="v.id" class="fe-inspector__version">
366
+ <div class="fe-inspector__version-main">
367
+ <span class="fe-inspector__version-n">
368
+ {{ t('inspector.versions.v', { n: v.version_n }) }}
369
+ </span>
370
+ <span class="fe-inspector__version-meta">
371
+ {{ formatDateStr(v.created_at) }} · {{ formatSize(v.size) }}
372
+ </span>
373
+ </div>
374
+ <div v-if="confirmVersionId === v.id" class="fe-inspector__confirm">
375
+ <p class="fe-inspector__confirm-q">{{ t('inspector.versions.restore_confirm') }}</p>
376
+ <label class="fe-inspector__check">
377
+ <input v-model="snapshotFirst" type="checkbox" />
378
+ {{ t('inspector.versions.snapshot_current') }}
379
+ </label>
380
+ <div class="fe-inspector__confirm-actions">
381
+ <button
382
+ type="button"
383
+ class="fe-btn fe-btn--primary fe-btn--sm"
384
+ :disabled="versionBusy"
385
+ @click="confirmRestore(v)"
386
+ >{{ t('inspector.versions.confirm') }}</button>
387
+ <button
388
+ type="button"
389
+ class="fe-btn fe-btn--sm"
390
+ :disabled="versionBusy"
391
+ @click="confirmVersionId = null"
392
+ >{{ t('inspector.versions.cancel') }}</button>
393
+ </div>
394
+ </div>
395
+ <button
396
+ v-else
397
+ type="button"
398
+ class="fe-btn fe-btn--sm"
399
+ :disabled="versionBusy"
400
+ @click="askRestore(v)"
401
+ >{{ t('inspector.versions.restore') }}</button>
402
+ </li>
403
+ </ul>
404
+ <button
405
+ type="button"
406
+ class="fe-btn fe-btn--sm fe-inspector__snapshot"
407
+ :disabled="versionBusy"
408
+ @click="takeSnapshot"
409
+ >{{ t('inspector.versions.take_snapshot') }}</button>
410
+ </template>
411
+ </section>
412
+
413
+ <!-- ══ İzinler ══ -->
414
+ <section v-if="single && effectivePerm" class="fe-inspector__section">
415
+ <h3 class="fe-inspector__heading">{{ t('inspector.section.permissions') }}</h3>
416
+ <div class="fe-inspector__permrow">
417
+ <span
418
+ class="fe-inspector__badge"
419
+ :class="`fe-inspector__badge--${effectivePerm}`"
420
+ >{{ permLabel(effectivePerm) }}</span>
421
+ <button
422
+ v-if="canManagePerms"
423
+ type="button"
424
+ class="fe-btn fe-btn--sm"
425
+ @click="emit('manage-permissions', single)"
426
+ >{{ t('inspector.perm.manage') }}</button>
427
+ </div>
428
+ </section>
429
+
430
+ <!-- ══ Paylaşımlar ══ -->
431
+ <section v-if="sharesState === 'ok'" class="fe-inspector__section">
432
+ <h3 class="fe-inspector__heading">{{ t('inspector.section.shares') }}</h3>
433
+ <p v-if="shares.length === 0" class="fe-inspector__empty">
434
+ {{ t('inspector.shares.empty') }}
435
+ </p>
436
+ <ul v-else class="fe-inspector__shares">
437
+ <li v-for="s in shares" :key="s.uuid" class="fe-inspector__share">
438
+ <span class="fe-inspector__share-url" :title="s.url">{{ s.url }}</span>
439
+ <span v-if="s.password_pin" class="fe-inspector__badge fe-inspector__badge--pin">PIN</span>
440
+ <span v-if="s.expires_at" class="fe-inspector__share-exp">
441
+ {{ formatDateStr(s.expires_at) }}
442
+ </span>
443
+ <button
444
+ type="button"
445
+ class="fe-inspector__copy"
446
+ :title="t('inspector.shares.copy')"
447
+ :aria-label="t('inspector.shares.copy')"
448
+ @click="copyText(s.url)"
449
+ >⧉</button>
450
+ </li>
451
+ </ul>
452
+ </section>
453
+ </div>
454
+ </aside>
455
+ </template>