@brftech/filex-core 0.5.0 → 0.6.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 (45) hide show
  1. package/dist/{ArchiveViewer-DKW-HyXT.js → ArchiveViewer-EGgNqLPE.js} +2 -2
  2. package/dist/{ArchiveViewer-DKW-HyXT.js.map → ArchiveViewer-EGgNqLPE.js.map} +1 -1
  3. package/dist/{CsvViewer-D2HmP2yr.js → CsvViewer-BvriJ42M.js} +2 -2
  4. package/dist/{CsvViewer-D2HmP2yr.js.map → CsvViewer-BvriJ42M.js.map} +1 -1
  5. package/dist/{DrawioViewer-DPNvOFCs.js → DrawioViewer-B_bSPsWX.js} +2 -2
  6. package/dist/{DrawioViewer-DPNvOFCs.js.map → DrawioViewer-B_bSPsWX.js.map} +1 -1
  7. package/dist/{EpubViewer-ByqjNuNL.js → EpubViewer-hl68h4gG.js} +2 -2
  8. package/dist/{EpubViewer-ByqjNuNL.js.map → EpubViewer-hl68h4gG.js.map} +1 -1
  9. package/dist/{IpynbViewer-2iONhbuJ.js → IpynbViewer-lhwOjXK7.js} +2 -2
  10. package/dist/{IpynbViewer-2iONhbuJ.js.map → IpynbViewer-lhwOjXK7.js.map} +1 -1
  11. package/dist/{MermaidViewer-CmiU7hIT.js → MermaidViewer-H2Z6AYXg.js} +2 -2
  12. package/dist/{MermaidViewer-CmiU7hIT.js.map → MermaidViewer-H2Z6AYXg.js.map} +1 -1
  13. package/dist/{PsdViewer-Inhl2yN9.js → PsdViewer-Bsk-11yH.js} +2 -2
  14. package/dist/{PsdViewer-Inhl2yN9.js.map → PsdViewer-Bsk-11yH.js.map} +1 -1
  15. package/dist/{TiffViewer-C2AFFEze.js → TiffViewer-D-j9vycG.js} +2 -2
  16. package/dist/{TiffViewer-C2AFFEze.js.map → TiffViewer-D-j9vycG.js.map} +1 -1
  17. package/dist/{Viewer3D-YdW14q_n.js → Viewer3D-Bcr70rTe.js} +2 -2
  18. package/dist/{Viewer3D-YdW14q_n.js.map → Viewer3D-Bcr70rTe.js.map} +1 -1
  19. package/dist/filex-core.js +42 -39
  20. package/dist/filex-core.umd.cjs +59 -58
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-C_ZSnk_I.js +11109 -0
  23. package/dist/index-C_ZSnk_I.js.map +1 -0
  24. package/dist/index.d.ts +234 -1
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +367 -14
  28. package/src/components/CommandPalette.vue +12 -1
  29. package/src/components/GalleryView.vue +214 -0
  30. package/src/components/GridView.vue +1 -0
  31. package/src/components/InspectorPanel.vue +170 -0
  32. package/src/components/ListView.vue +1 -0
  33. package/src/components/SecondaryPane.vue +411 -0
  34. package/src/components/TabBar.vue +174 -0
  35. package/src/components/Toolbar.vue +23 -5
  36. package/src/composables/useFileApi.ts +53 -0
  37. package/src/composables/useKeyboardShortcuts.ts +19 -0
  38. package/src/composables/useTabs.ts +222 -0
  39. package/src/index.ts +6 -0
  40. package/src/locales/en.ts +33 -0
  41. package/src/locales/tr.ts +33 -0
  42. package/src/styles/base.css +538 -0
  43. package/src/types/FileNode.ts +1 -1
  44. package/dist/index-eARevpz0.js +0 -9952
  45. package/dist/index-eARevpz0.js.map +0 -1
@@ -106,6 +106,23 @@ export interface UserSearchResponse {
106
106
  users: UserSuggestion[];
107
107
  }
108
108
 
109
+ /* === calisma:d3 — node comments (inspector panel) === */
110
+
111
+ /** Mirrors backend `model.NodeComment` (GET /api/files/comments?node_id=…). */
112
+ export interface NodeComment {
113
+ id: number;
114
+ node_id: number;
115
+ user_id: number;
116
+ body: string;
117
+ created_at: string;
118
+ updated_at?: string;
119
+ /** Joined author display name (email fallback), filled by the backend. */
120
+ author_name?: string;
121
+ /** Whether the CURRENT caller may delete this row (author or admin). */
122
+ can_delete?: boolean;
123
+ }
124
+ /* === /calisma:d3 === */
125
+
109
126
  export interface InviteResponse {
110
127
  mode: 'granted' | 'user_created' | 'shared';
111
128
  user_id?: number;
@@ -741,6 +758,38 @@ export function useFileApi(config: ExplorerConfig) {
741
758
  }
742
759
  /* === /koru:k1 === */
743
760
 
761
+ /* === calisma:d3 — node comments (inspector panel) ===
762
+ * Same manager-URL derivation trick as versions/permissions so embedded
763
+ * proxies forwarding the whole /api/files/* subtree keep working.
764
+ * GET /api/files/comments?node_id=N → {comments, node_id}
765
+ * POST /api/files/comments → {node_id, body}
766
+ * DELETE /api/files/comments/{id} → {ok}
767
+ */
768
+ function commentsUrl(sub = ''): string {
769
+ const base = endpoints.manager.replace(/\/manager(\?.*)?$/, '/comments');
770
+ return base + sub;
771
+ }
772
+ async function listComments(nodeId: number): Promise<NodeComment[]> {
773
+ const data = await jsonFetch<{ comments?: NodeComment[] | null }>(
774
+ commentsUrl() + '?node_id=' + encodeURIComponent(String(nodeId)),
775
+ );
776
+ return Array.isArray(data?.comments) ? data.comments : [];
777
+ }
778
+ async function addComment(nodeId: number, body: string): Promise<NodeComment> {
779
+ const data = await jsonFetch<{ comment: NodeComment }>(commentsUrl(), {
780
+ method: 'POST',
781
+ headers: { 'Content-Type': 'application/json' },
782
+ body: JSON.stringify({ node_id: nodeId, body }),
783
+ });
784
+ return data.comment;
785
+ }
786
+ async function deleteComment(id: number): Promise<void> {
787
+ await jsonFetch(commentsUrl('/' + encodeURIComponent(String(id))), {
788
+ method: 'DELETE',
789
+ });
790
+ }
791
+ /* === /calisma:d3 === */
792
+
744
793
  // Mint a short-lived WebSocket auth ticket for the realtime layer. Derived
745
794
  // from the manager URL (so it flows through the same host proxy) and uses the
746
795
  // same auth/creds as every other call. Returns null on any failure (a backend
@@ -790,6 +839,10 @@ export function useFileApi(config: ExplorerConfig) {
790
839
  listVersions,
791
840
  restoreVersion,
792
841
  snapshotVersion,
842
+ // Node comments (calisma:d3 inspector)
843
+ listComments,
844
+ addComment,
845
+ deleteComment,
793
846
  // Permissions (RBAC panel)
794
847
  listPermissions,
795
848
  resolveEmail,
@@ -38,6 +38,12 @@ export interface ShortcutHandlers {
38
38
  onShowHelp?: () => void; // ? (Shift+/ on most layouts)
39
39
  onToggleInspector?: () => void; // i (koru:k1 details panel)
40
40
  onQuickLook?: () => void; // Space (wiring:c2 quick-look overlay)
41
+ /* wiring:d1 — tab strip actions */
42
+ onTabNew?: () => void; // Ctrl+T
43
+ onTabClose?: () => void; // Ctrl+W
44
+ onTabNext?: () => void; // Ctrl+Tab
45
+ onTabPrev?: () => void; // Ctrl+Shift+Tab
46
+ /* /wiring:d1 */
41
47
  hasSelection?: () => boolean; // disambiguates Backspace
42
48
  }
43
49
 
@@ -87,6 +93,14 @@ export const SHORTCUT_ACTIONS: ShortcutActionDef[] = [
87
93
  { id: 'cut', defaultCombo: 'Ctrl+X', labelKey: 'shortcuts.cut', groupKey: 'shortcuts.group.file' },
88
94
  { id: 'copy', defaultCombo: 'Ctrl+C', labelKey: 'shortcuts.copy', groupKey: 'shortcuts.group.file' },
89
95
  { id: 'paste', defaultCombo: 'Ctrl+V', labelKey: 'shortcuts.paste', groupKey: 'shortcuts.group.file' },
96
+ /* wiring:d1 — tabs. Note: browsers reserve Ctrl+T/W/Tab in normal pages
97
+ * (preventDefault can't stop them there); they work in webcomponent/PWA/
98
+ * kiosk contexts and stay remappable through the settings modal. */
99
+ { id: 'tab-new', defaultCombo: 'Ctrl+T', labelKey: 'shortcuts.tab_new', groupKey: 'shortcuts.group.tabs' },
100
+ { id: 'tab-close', defaultCombo: 'Ctrl+W', labelKey: 'shortcuts.tab_close', groupKey: 'shortcuts.group.tabs' },
101
+ { id: 'tab-next', defaultCombo: 'Ctrl+Tab', labelKey: 'shortcuts.tab_next', groupKey: 'shortcuts.group.tabs' },
102
+ { id: 'tab-prev', defaultCombo: 'Ctrl+Shift+Tab', labelKey: 'shortcuts.tab_prev', groupKey: 'shortcuts.group.tabs' },
103
+ /* /wiring:d1 */
90
104
  ];
91
105
 
92
106
  /** action id → ShortcutHandlers callback name. */
@@ -105,6 +119,11 @@ const HANDLER_KEY: Record<string, keyof ShortcutHandlers> = {
105
119
  cut: 'onCut',
106
120
  copy: 'onCopy',
107
121
  paste: 'onPaste',
122
+ /* wiring:d1 */
123
+ 'tab-new': 'onTabNew',
124
+ 'tab-close': 'onTabClose',
125
+ 'tab-next': 'onTabNext',
126
+ 'tab-prev': 'onTabPrev',
108
127
  };
109
128
 
110
129
  // --------------------------------------------------------------------
@@ -0,0 +1,222 @@
1
+ /**
2
+ * useTabs — wiring:d1 sekmeler (çalışma alanı tab şeridi).
3
+ *
4
+ * A LAYER ABOVE FileExplorer's location state, never a replacement for
5
+ * it: each tab is a location snapshot `{ id, path, viewMode, split }`.
6
+ * The composable performs no fetching and owns no navigation — the host
7
+ * activates a tab by running its EXISTING `load(path)` pipeline and
8
+ * reports navigations back through `syncActive()`, which keeps the
9
+ * active snapshot glued to wherever the user actually is.
10
+ *
11
+ * `path` is stored in the SAME form as FileExplorer's `currentPath`
12
+ * (virtual `<storage>/<rel>` in multi-storage mode, bare relative path
13
+ * in single-storage mode) — i.e. the exact string `load(path)` accepts.
14
+ * Storing the wire form (`adapter://rel`) instead would break the
15
+ * rootPath-floor clamp inside load() (the floor is compared in user-path
16
+ * form), so the user-path form is deliberate.
17
+ *
18
+ * Persistence: localStorage under a host-supplied key (the host derives
19
+ * it from the pathPersist scope: disabled when `pathPersist: 'none'`,
20
+ * suffixed with the rootPath confine so embedded instances with
21
+ * different confines never clobber each other). Schema:
22
+ *
23
+ * { "v": 1, "active": "<id>",
24
+ * "tabs": [ { "id", "path", "viewMode", "split": {"path"}|null } ] }
25
+ */
26
+
27
+ import { computed, ref, watch, type Ref } from 'vue';
28
+ import type { ViewMode } from '../types/FileNode';
29
+
30
+ /** Per-tab split state — the secondary pane's own location. */
31
+ export interface TabSplit {
32
+ path: string;
33
+ }
34
+
35
+ export interface TabState {
36
+ id: string;
37
+ /** Location snapshot in `currentPath` form (see module docs). */
38
+ path: string;
39
+ viewMode: ViewMode;
40
+ split: TabSplit | null;
41
+ }
42
+
43
+ export interface UseTabsOptions {
44
+ /** localStorage key; null disables persistence entirely. */
45
+ storageKey: string | null;
46
+ }
47
+
48
+ function makeId(): string {
49
+ try {
50
+ return crypto.randomUUID();
51
+ } catch {
52
+ return `t${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
53
+ }
54
+ }
55
+
56
+ export function useTabs(opts: UseTabsOptions) {
57
+ const tabs: Ref<TabState[]> = ref([]);
58
+ const activeId = ref<string>('');
59
+
60
+ const activeTab = computed<TabState | null>(
61
+ () => tabs.value.find((t) => t.id === activeId.value) ?? null,
62
+ );
63
+ const activeIndex = computed(() => tabs.value.findIndex((t) => t.id === activeId.value));
64
+ const hasMultiple = computed(() => tabs.value.length > 1);
65
+
66
+ // ------------------------------------------------------------------
67
+ // Persistence
68
+ // ------------------------------------------------------------------
69
+
70
+ /** Restore from storage. Returns true when at least one tab loaded. */
71
+ function restore(): boolean {
72
+ if (!opts.storageKey) return false;
73
+ try {
74
+ const raw = localStorage.getItem(opts.storageKey);
75
+ if (!raw) return false;
76
+ const parsed = JSON.parse(raw) as { v?: unknown; active?: unknown; tabs?: unknown };
77
+ if (!parsed || !Array.isArray(parsed.tabs) || parsed.tabs.length === 0) return false;
78
+ const clean: TabState[] = [];
79
+ for (const t of parsed.tabs as Array<Record<string, unknown>>) {
80
+ if (!t || typeof t.path !== 'string') continue;
81
+ // ViewMode is validated loosely on purpose: future modes (e.g. the
82
+ // gallery wave) must survive a round-trip through an older schema.
83
+ const vm =
84
+ typeof t.viewMode === 'string' && t.viewMode ? (t.viewMode as ViewMode) : 'list';
85
+ const rawSplit = t.split as { path?: unknown } | null | undefined;
86
+ const split =
87
+ rawSplit && typeof rawSplit === 'object' && typeof rawSplit.path === 'string'
88
+ ? { path: rawSplit.path }
89
+ : null;
90
+ clean.push({
91
+ id: typeof t.id === 'string' && t.id ? t.id : makeId(),
92
+ path: t.path,
93
+ viewMode: vm,
94
+ split,
95
+ });
96
+ }
97
+ if (clean.length === 0) return false;
98
+ tabs.value = clean;
99
+ activeId.value = clean.some((t) => t.id === parsed.active)
100
+ ? String(parsed.active)
101
+ : clean[0].id;
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ function persist(): void {
109
+ if (!opts.storageKey) return;
110
+ try {
111
+ localStorage.setItem(
112
+ opts.storageKey,
113
+ JSON.stringify({ v: 1, active: activeId.value, tabs: tabs.value }),
114
+ );
115
+ } catch {
116
+ /* private mode / quota */
117
+ }
118
+ }
119
+
120
+ watch([tabs, activeId], persist, { deep: true });
121
+
122
+ // ------------------------------------------------------------------
123
+ // Mutations
124
+ // ------------------------------------------------------------------
125
+
126
+ /** Create the first tab (no-op once any tab exists). */
127
+ function seed(path: string, viewMode: ViewMode): void {
128
+ if (tabs.value.length > 0) return;
129
+ const t: TabState = { id: makeId(), path, viewMode, split: null };
130
+ tabs.value = [t];
131
+ activeId.value = t.id;
132
+ }
133
+
134
+ /** Update the ACTIVE tab's snapshot (navigation / view-mode change). */
135
+ function syncActive(patch: Partial<Pick<TabState, 'path' | 'viewMode'>>): void {
136
+ const t = activeTab.value;
137
+ if (!t) return;
138
+ if (patch.path !== undefined) t.path = patch.path;
139
+ if (patch.viewMode !== undefined) t.viewMode = patch.viewMode;
140
+ }
141
+
142
+ /** Open a new tab right after the active one (browser convention). */
143
+ function openTab(
144
+ path: string,
145
+ o: { viewMode: ViewMode; background?: boolean; split?: TabSplit | null },
146
+ ): TabState {
147
+ const t: TabState = { id: makeId(), path, viewMode: o.viewMode, split: o.split ?? null };
148
+ const idx = activeIndex.value;
149
+ tabs.value.splice(idx === -1 ? tabs.value.length : idx + 1, 0, t);
150
+ if (!o.background) activeId.value = t.id;
151
+ return t;
152
+ }
153
+
154
+ /**
155
+ * Close a tab. The last remaining tab never closes. Returns the tab
156
+ * the host must ACTIVATE (right neighbour, else left) when the active
157
+ * one was closed; null when the visible location is unchanged.
158
+ */
159
+ function closeTab(id: string): TabState | null {
160
+ if (tabs.value.length <= 1) return null;
161
+ const idx = tabs.value.findIndex((t) => t.id === id);
162
+ if (idx === -1) return null;
163
+ const wasActive = tabs.value[idx].id === activeId.value;
164
+ tabs.value.splice(idx, 1);
165
+ if (!wasActive) return null;
166
+ const next = tabs.value[Math.min(idx, tabs.value.length - 1)];
167
+ activeId.value = next.id;
168
+ return next;
169
+ }
170
+
171
+ /** Make a tab active. Returns it when the active tab actually changed. */
172
+ function activate(id: string): TabState | null {
173
+ const t = tabs.value.find((x) => x.id === id);
174
+ if (!t || t.id === activeId.value) return null;
175
+ activeId.value = t.id;
176
+ return t;
177
+ }
178
+
179
+ /** Cycle: +1 = next, -1 = previous (wraps). */
180
+ function step(delta: number): TabState | null {
181
+ if (tabs.value.length < 2) return null;
182
+ const idx = activeIndex.value === -1 ? 0 : activeIndex.value;
183
+ const next = tabs.value[(idx + delta + tabs.value.length) % tabs.value.length];
184
+ return activate(next.id);
185
+ }
186
+
187
+ /** Drag-sort support: move the tab at `from` to position `to`. */
188
+ function move(from: number, to: number): void {
189
+ if (from === to) return;
190
+ if (from < 0 || to < 0 || from >= tabs.value.length || to >= tabs.value.length) return;
191
+ const list = [...tabs.value];
192
+ const [t] = list.splice(from, 1);
193
+ list.splice(to, 0, t);
194
+ tabs.value = list;
195
+ }
196
+
197
+ /** Set (or clear with null) the ACTIVE tab's split state. */
198
+ function setSplit(split: TabSplit | null): void {
199
+ const t = activeTab.value;
200
+ if (!t) return;
201
+ t.split = split;
202
+ }
203
+
204
+ return {
205
+ tabs,
206
+ activeId,
207
+ activeTab,
208
+ activeIndex,
209
+ hasMultiple,
210
+ restore,
211
+ seed,
212
+ syncActive,
213
+ openTab,
214
+ closeTab,
215
+ activate,
216
+ step,
217
+ move,
218
+ setSplit,
219
+ };
220
+ }
221
+
222
+ export type TabsApi = ReturnType<typeof useTabs>;
package/src/index.ts CHANGED
@@ -127,3 +127,9 @@ export type {
127
127
  OperationsStore,
128
128
  } from './composables/useOperations';
129
129
  export { default as OperationsCenter } from './components/OperationsCenter.vue';
130
+ /* wiring:d1 — sekmeler + split panel */
131
+ export { useTabs } from './composables/useTabs';
132
+ export type { TabState, TabSplit, TabsApi } from './composables/useTabs';
133
+ export { default as TabBar } from './components/TabBar.vue';
134
+ export { default as SecondaryPane } from './components/SecondaryPane.vue';
135
+ /* /wiring:d1 */
package/src/locales/en.ts CHANGED
@@ -332,4 +332,37 @@ export const en: Record<string, string> = {
332
332
  'toolbar.view_label': 'View',
333
333
  'list.aria': 'File list',
334
334
  'grid.aria': 'File grid',
335
+
336
+ /* === wiring:d1 — tabs + split pane === */
337
+ 'tabs.strip': 'Tabs',
338
+ 'tabs.new': 'New tab',
339
+ 'tabs.close': 'Close tab',
340
+ 'tabs.split': 'Split view',
341
+ 'tabs.split_off': 'Close split',
342
+ 'ctx.open_new_tab': 'Open in new tab',
343
+ 'shortcuts.group.tabs': 'Tabs',
344
+ 'shortcuts.tab_new': 'New tab',
345
+ 'shortcuts.tab_close': 'Close tab',
346
+ 'shortcuts.tab_next': 'Next tab',
347
+ 'shortcuts.tab_prev': 'Previous tab',
348
+ 'cmd.tab_new': 'Open a new tab',
349
+ 'cmd.split_toggle': 'Toggle split view',
350
+ 'split.pane': 'Secondary pane',
351
+ 'split.close': 'Close the side pane',
352
+ 'split.error': 'Could not load the listing',
353
+ 'split.retry': 'Retry',
354
+ 'split.copy_queued': 'Copy queued',
355
+ 'split.cross_copy': 'Different storages — copy queued instead',
356
+ 'split.cross_failed': 'Cross-storage copy is not supported by this server',
357
+ /* === /wiring:d1 === */
358
+ /* wiring:d2 — gallery view */
359
+ 'toolbar.view.gallery': 'Gallery',
360
+ 'gallery.aria': 'File gallery',
361
+ /* /wiring:d2 */
362
+ /* wiring:d3 — inspector node comments */
363
+ 'inspector.section.comments': 'Comments',
364
+ 'inspector.comments.empty': 'No comments yet.',
365
+ 'inspector.comments.placeholder': 'Write a comment…',
366
+ 'inspector.comments.send': 'Send',
367
+ 'inspector.comments.delete': 'Delete comment',
335
368
  };
package/src/locales/tr.ts CHANGED
@@ -332,4 +332,37 @@ export const tr: Record<string, string> = {
332
332
  'toolbar.view_label': 'Görünüm',
333
333
  'list.aria': 'Dosya listesi',
334
334
  'grid.aria': 'Dosya ızgarası',
335
+
336
+ /* === wiring:d1 — sekmeler + split panel === */
337
+ 'tabs.strip': 'Sekmeler',
338
+ 'tabs.new': 'Yeni sekme',
339
+ 'tabs.close': 'Sekmeyi kapat',
340
+ 'tabs.split': 'Görünümü böl',
341
+ 'tabs.split_off': 'Bölmeyi kapat',
342
+ 'ctx.open_new_tab': 'Yeni sekmede aç',
343
+ 'shortcuts.group.tabs': 'Sekmeler',
344
+ 'shortcuts.tab_new': 'Yeni sekme',
345
+ 'shortcuts.tab_close': 'Sekmeyi kapat',
346
+ 'shortcuts.tab_next': 'Sonraki sekme',
347
+ 'shortcuts.tab_prev': 'Önceki sekme',
348
+ 'cmd.tab_new': 'Yeni sekme aç',
349
+ 'cmd.split_toggle': 'Görünümü böl / birleştir',
350
+ 'split.pane': 'İkincil panel',
351
+ 'split.close': 'Yan paneli kapat',
352
+ 'split.error': 'Liste yüklenemedi',
353
+ 'split.retry': 'Yeniden dene',
354
+ 'split.copy_queued': 'Kopyalama kuyruğa alındı',
355
+ 'split.cross_copy': 'Depolar farklı — kopyalama kuyruğa alındı',
356
+ 'split.cross_failed': 'Depolar arası kopyalama bu sunucuda desteklenmiyor',
357
+ /* === /wiring:d1 === */
358
+ /* wiring:d2 — galeri görünümü */
359
+ 'toolbar.view.gallery': 'Galeri',
360
+ 'gallery.aria': 'Dosya galerisi',
361
+ /* /wiring:d2 */
362
+ /* wiring:d3 — Yorumlar (inspector node comments) */
363
+ 'inspector.section.comments': 'Yorumlar',
364
+ 'inspector.comments.empty': 'Henüz yorum yok.',
365
+ 'inspector.comments.placeholder': 'Yorum yaz…',
366
+ 'inspector.comments.send': 'Gönder',
367
+ 'inspector.comments.delete': 'Yorumu sil',
335
368
  };