@brftech/filex-core 0.4.2 → 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 (58) hide show
  1. package/dist/{ArchiveViewer-B_0IQExT.js → ArchiveViewer-EGgNqLPE.js} +2 -2
  2. package/dist/{ArchiveViewer-B_0IQExT.js.map → ArchiveViewer-EGgNqLPE.js.map} +1 -1
  3. package/dist/{CsvViewer-maMfUy5i.js → CsvViewer-BvriJ42M.js} +2 -2
  4. package/dist/{CsvViewer-maMfUy5i.js.map → CsvViewer-BvriJ42M.js.map} +1 -1
  5. package/dist/{DrawioViewer-DP_pXI00.js → DrawioViewer-B_bSPsWX.js} +2 -2
  6. package/dist/{DrawioViewer-DP_pXI00.js.map → DrawioViewer-B_bSPsWX.js.map} +1 -1
  7. package/dist/{EpubViewer-DSaqGnVx.js → EpubViewer-hl68h4gG.js} +2 -2
  8. package/dist/{EpubViewer-DSaqGnVx.js.map → EpubViewer-hl68h4gG.js.map} +1 -1
  9. package/dist/{IpynbViewer-BmdBanF1.js → IpynbViewer-lhwOjXK7.js} +2 -2
  10. package/dist/{IpynbViewer-BmdBanF1.js.map → IpynbViewer-lhwOjXK7.js.map} +1 -1
  11. package/dist/{MermaidViewer-Drlkl0Z5.js → MermaidViewer-H2Z6AYXg.js} +2 -2
  12. package/dist/{MermaidViewer-Drlkl0Z5.js.map → MermaidViewer-H2Z6AYXg.js.map} +1 -1
  13. package/dist/{PsdViewer-gftUJrsm.js → PsdViewer-Bsk-11yH.js} +2 -2
  14. package/dist/{PsdViewer-gftUJrsm.js.map → PsdViewer-Bsk-11yH.js.map} +1 -1
  15. package/dist/{TiffViewer-0Uv0-Lxh.js → TiffViewer-D-j9vycG.js} +2 -2
  16. package/dist/{TiffViewer-0Uv0-Lxh.js.map → TiffViewer-D-j9vycG.js.map} +1 -1
  17. package/dist/{Viewer3D-Gu3kUpxV.js → Viewer3D-Bcr70rTe.js} +2 -2
  18. package/dist/{Viewer3D-Gu3kUpxV.js.map → Viewer3D-Bcr70rTe.js.map} +1 -1
  19. package/dist/filex-core.js +50 -25
  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 +597 -4
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +636 -15
  28. package/src/components/CommandPalette.vue +24 -1
  29. package/src/components/ContextMenu.vue +114 -8
  30. package/src/components/GalleryView.vue +214 -0
  31. package/src/components/GridView.vue +34 -1
  32. package/src/components/InspectorPanel.vue +170 -0
  33. package/src/components/ListView.vue +40 -6
  34. package/src/components/OnboardingTour.vue +332 -0
  35. package/src/components/OperationsCenter.vue +284 -0
  36. package/src/components/PendingOpsTray.vue +50 -66
  37. package/src/components/QuickLook.vue +142 -0
  38. package/src/components/SecondaryPane.vue +411 -0
  39. package/src/components/ShortcutSettings.vue +270 -0
  40. package/src/components/ShortcutsHelp.vue +30 -9
  41. package/src/components/TabBar.vue +174 -0
  42. package/src/components/ThemeGallery.vue +253 -0
  43. package/src/components/Toolbar.vue +75 -11
  44. package/src/components/UploadProgress.vue +49 -57
  45. package/src/composables/useFileApi.ts +53 -0
  46. package/src/composables/useKeyboardShortcuts.ts +340 -137
  47. package/src/composables/useOperations.ts +291 -0
  48. package/src/composables/useTabs.ts +222 -0
  49. package/src/index.ts +51 -0
  50. package/src/lib/dragGhost.ts +41 -0
  51. package/src/lib/themes.ts +505 -0
  52. package/src/locales/en.ts +125 -0
  53. package/src/locales/tr.ts +125 -0
  54. package/src/modals/Modal.vue +41 -5
  55. package/src/styles/base.css +1210 -0
  56. package/src/types/FileNode.ts +1 -1
  57. package/dist/index-BLocdbmc.js +0 -7993
  58. package/dist/index-BLocdbmc.js.map +0 -1
@@ -39,6 +39,8 @@ const props = defineProps<{
39
39
  canGoUp?: boolean;
40
40
  /* bul:s3 — global search callback; absent = older host, group hidden. */
41
41
  globalSearch?: (q: string) => Promise<GlobalSearchHit[]>;
42
+ /* wiring:d1 — false hides the split command (narrow embeds). */
43
+ splitEnabled?: boolean;
42
44
  }>();
43
45
 
44
46
  const emit = defineEmits<{
@@ -53,6 +55,13 @@ const emit = defineEmits<{
53
55
  (e: 'go-up'): void;
54
56
  /* bul:s3 — an "everywhere" hit was chosen. */
55
57
  (e: 'open-hit', hit: GlobalSearchHit): void;
58
+ /* wiring:int — settings surfaces reachable from anywhere via the palette */
59
+ (e: 'open-theme'): void;
60
+ (e: 'open-shortcut-settings'): void;
61
+ (e: 'start-tour'): void;
62
+ /* wiring:d1 — tab / split commands */
63
+ (e: 'tab-new'): void;
64
+ (e: 'split-toggle'): void;
56
65
  }>();
57
66
 
58
67
  const { t, nodeDisplayName } = useLocale(() => props.locale);
@@ -122,10 +131,17 @@ const commandItems = computed<PaletteItem[]>(() => {
122
131
  const defs: Array<{ command: string; label: string; icon: string; enabled: boolean }> = [
123
132
  { command: 'new-folder', label: t('toolbar.new_folder'), icon: '📁', enabled: props.canWrite !== false },
124
133
  { command: 'upload', label: t('toolbar.upload'), icon: '⬆', enabled: props.canWrite !== false },
125
- { command: 'toggle-view', label: t('cmd.view_toggle'), icon: props.viewMode === 'list' ? '▦' : '☰', enabled: true },
134
+ { command: 'toggle-view', label: t('cmd.view_toggle'), icon: props.viewMode === 'list' ? '▦' : props.viewMode === 'grid' ? '▣' : '☰', enabled: true /* wiring:d2 — sıradaki mod ikonu (list→grid→galeri→list) */ },
126
135
  { command: 'open-trash', label: t('cmd.trash'), icon: '🗑', enabled: true },
127
136
  { command: 'refresh', label: t('toolbar.refresh'), icon: '⟳', enabled: true },
128
137
  { command: 'go-up', label: t('toolbar.go_up'), icon: '↑', enabled: props.canGoUp !== false },
138
+ /* wiring:int */
139
+ { command: 'open-theme', label: t('theme.menu'), icon: '🎨', enabled: true },
140
+ { command: 'open-shortcut-settings', label: t('shortcuts.settings.menu'), icon: '⌨', enabled: true },
141
+ { command: 'start-tour', label: t('tour.restart'), icon: '🎓', enabled: true },
142
+ /* wiring:d1 — sekme + split */
143
+ { command: 'tab-new', label: t('cmd.tab_new'), icon: '⧉', enabled: true },
144
+ { command: 'split-toggle', label: t('cmd.split_toggle'), icon: '◫', enabled: props.splitEnabled !== false },
129
145
  ];
130
146
  return defs
131
147
  .filter((d) => d.enabled && matchScore(d.label, q) >= 0)
@@ -310,6 +326,13 @@ function choose(item?: PaletteItem) {
310
326
  case 'open-trash': emit('open-trash'); break;
311
327
  case 'refresh': emit('refresh'); break;
312
328
  case 'go-up': emit('go-up'); break;
329
+ /* wiring:int */
330
+ case 'open-theme': emit('open-theme'); break;
331
+ case 'open-shortcut-settings': emit('open-shortcut-settings'); break;
332
+ case 'start-tour': emit('start-tour'); break;
333
+ /* wiring:d1 */
334
+ case 'tab-new': emit('tab-new'); break;
335
+ case 'split-toggle': emit('split-toggle'); break;
313
336
  }
314
337
  }
315
338
  }
@@ -54,10 +54,23 @@ const y = ref(0);
54
54
  const targetNodes = ref<FileNode[]>([]);
55
55
  const menuEl = ref<HTMLElement | null>(null);
56
56
 
57
+ /* wiring:c4 — where the pointer opened the menu; kept separate from x/y so
58
+ * the edge-flip can re-anchor ABOVE/LEFT of the cursor instead of merely
59
+ * clamping (a clamp near the bottom edge used to leave the menu covering
60
+ * the clicked row). */
61
+ let anchorX = 0;
62
+ let anchorY = 0;
63
+ /* wiring:c4 — focus restore: whatever had focus before the menu opened gets
64
+ * it back on hide, so keyboard users don't fall back to <body>. */
65
+ let prevFocus: HTMLElement | null = null;
66
+
57
67
  async function show(ev: { clientX: number; clientY: number }, nodes: FileNode[]) {
68
+ prevFocus = (document.activeElement as HTMLElement | null) ?? null; /* wiring:c4 */
58
69
  open.value = true;
59
70
  x.value = ev.clientX;
60
71
  y.value = ev.clientY;
72
+ anchorX = ev.clientX; /* wiring:c4 */
73
+ anchorY = ev.clientY; /* wiring:c4 */
61
74
  targetNodes.value = nodes;
62
75
  // After the menu renders, clamp it inside the viewport so a click near the
63
76
  // bottom/right edge doesn't push the menu off-screen (it opens down-right
@@ -70,6 +83,7 @@ async function show(ev: { clientX: number; clientY: number }, nodes: FileNode[])
70
83
  return;
71
84
  }
72
85
  clampToViewport();
86
+ focusItem(0); /* wiring:c4 — keyboard nav starts on the first item */
73
87
  }
74
88
 
75
89
  function clampToViewport() {
@@ -79,11 +93,16 @@ function clampToViewport() {
79
93
  const margin = 8;
80
94
  const vw = window.innerWidth;
81
95
  const vh = window.innerHeight;
82
- if (y.value + rect.height > vh - margin) {
83
- y.value = Math.max(margin, vh - rect.height - margin);
96
+ /* wiring:c4 edge flip: overflowing the bottom/right edge re-anchors the
97
+ * menu above/left of the cursor when there's room; clamping stays as the
98
+ * fallback for tiny viewports. */
99
+ if (anchorY + rect.height > vh - margin) {
100
+ const flipped = anchorY - rect.height;
101
+ y.value = flipped >= margin ? flipped : Math.max(margin, vh - rect.height - margin);
84
102
  }
85
- if (x.value + rect.width > vw - margin) {
86
- x.value = Math.max(margin, vw - rect.width - margin);
103
+ if (anchorX + rect.width > vw - margin) {
104
+ const flipped = anchorX - rect.width;
105
+ x.value = flipped >= margin ? flipped : Math.max(margin, vw - rect.width - margin);
87
106
  }
88
107
  }
89
108
 
@@ -92,7 +111,42 @@ function hide() {
92
111
  /* bag:b4 — reset any in-flight sheet drag so reopening starts clean. */
93
112
  sheetDragging.value = false;
94
113
  sheetDragY.value = 0;
114
+ /* wiring:c4 — give focus back to the opener (row/card/⋯ button). */
115
+ prevFocus?.focus?.();
116
+ prevFocus = null;
117
+ }
118
+
119
+ /* === wiring:c4 — keyboard navigation (menu + sheet) ===
120
+ * Arrow keys cycle through enabled items, Home/End jump, Enter/Space
121
+ * activate the focused <button> natively, Esc closes (see onKey). Disabled
122
+ * buttons are unfocusable, so the query skips them by construction. */
123
+ function menuItems(): HTMLElement[] {
124
+ const host = props.sheet ? sheetEl.value : menuEl.value;
125
+ if (!host) return [];
126
+ return Array.from(
127
+ host.querySelectorAll<HTMLElement>('.fe-ctx__item:not(:disabled)'),
128
+ );
129
+ }
130
+
131
+ function focusItem(idx: number) {
132
+ const items = menuItems();
133
+ if (items.length === 0) return;
134
+ const i = ((idx % items.length) + items.length) % items.length;
135
+ items[i]?.focus();
136
+ }
137
+
138
+ function moveFocus(delta: number) {
139
+ const items = menuItems();
140
+ if (items.length === 0) return;
141
+ const active = document.activeElement as HTMLElement | null;
142
+ const cur = active ? items.indexOf(active) : -1;
143
+ if (cur === -1) {
144
+ focusItem(delta > 0 ? 0 : items.length - 1);
145
+ return;
146
+ }
147
+ focusItem(cur + delta);
95
148
  }
149
+ /* === /wiring:c4 === */
96
150
 
97
151
  /* bag:b4 — sheet drag-to-dismiss. Bound to the grab handle only, so the
98
152
  * scrollable item list keeps its native touch scrolling. Dragging past the
@@ -139,10 +193,60 @@ function pick(a: ContextAction) {
139
193
  }
140
194
 
141
195
  function onKey(e: KeyboardEvent) {
142
- if (e.key === 'Escape') hide();
196
+ if (e.key === 'Escape') {
197
+ // Scoped close: Esc with the menu open closes ONLY the menu — the
198
+ // explorer's global onClose (which also dismisses modals/toasts)
199
+ // shouldn't fire underneath.
200
+ e.stopPropagation();
201
+ hide();
202
+ return;
203
+ }
204
+ if (e.key === 'Enter' || e.key === ' ') {
205
+ // Native button activation handles the pick; stop the bubble so the
206
+ // explorer's global Enter=open shortcut doesn't double-fire and pop
207
+ // the preview modal on top of the chosen action.
208
+ e.stopPropagation();
209
+ return;
210
+ }
211
+ /* wiring:c4 — arrow-key navigation; stopPropagation keeps the explorer's
212
+ * global shortcut handler (Enter=open, Backspace=up…) out of the loop
213
+ * while the menu is up. */
214
+ if (e.key === 'ArrowDown') {
215
+ e.preventDefault();
216
+ e.stopPropagation();
217
+ moveFocus(1);
218
+ } else if (e.key === 'ArrowUp') {
219
+ e.preventDefault();
220
+ e.stopPropagation();
221
+ moveFocus(-1);
222
+ } else if (e.key === 'Home') {
223
+ e.preventDefault();
224
+ focusItem(0);
225
+ } else if (e.key === 'End') {
226
+ e.preventDefault();
227
+ focusItem(-1);
228
+ } else if (e.key === 'Tab') {
229
+ // A menu is not a tab-stop container — Tab closes it (WAI-ARIA menu
230
+ // pattern) instead of tabbing behind the backdrop.
231
+ e.preventDefault();
232
+ hide();
233
+ }
143
234
  }
144
235
 
145
- const visibleActions = computed(() => props.actions.filter((a) => !a.hidden));
236
+ /* wiring:c4 hidden-entry filtering used to leave orphaned separators
237
+ * (leading/trailing/doubled) when the entries around a divider were hidden
238
+ * by RBAC or selection shape. Collapse them here so every visible divider
239
+ * actually separates two groups. */
240
+ const visibleActions = computed(() => {
241
+ const vis = props.actions.filter((a) => !a.hidden);
242
+ const out: ContextAction[] = [];
243
+ for (const a of vis) {
244
+ if (a.divider && (out.length === 0 || out[out.length - 1].divider)) continue;
245
+ out.push(a);
246
+ }
247
+ while (out.length > 0 && out[out.length - 1].divider) out.pop();
248
+ return out;
249
+ });
146
250
 
147
251
  const prefersDark = ref(false);
148
252
  let mq: MediaQueryList | undefined;
@@ -204,7 +308,7 @@ defineExpose({ show, hide });
204
308
  />
205
309
  <div class="fe-sheet__items">
206
310
  <template v-for="(a, i) in visibleActions" :key="a.key || i">
207
- <div v-if="a.divider" class="fe-ctx__sep" />
311
+ <div v-if="a.divider" class="fe-ctx__sep" role="separator" />
208
312
  <button
209
313
  v-else
210
314
  type="button"
@@ -225,11 +329,13 @@ defineExpose({ show, hide });
225
329
  ref="menuEl"
226
330
  class="fe-ctx"
227
331
  role="menu"
332
+ aria-orientation="vertical"
333
+ :aria-label="t('sheet.menu') /* wiring:c4 */"
228
334
  :style="{ top: y + 'px', left: x + 'px' }"
229
335
  @click.stop
230
336
  >
231
337
  <template v-for="(a, i) in visibleActions" :key="a.key || i">
232
- <div v-if="a.divider" class="fe-ctx__sep" />
338
+ <div v-if="a.divider" class="fe-ctx__sep" role="separator" />
233
339
  <button
234
340
  v-else
235
341
  type="button"
@@ -0,0 +1,214 @@
1
+ <script setup lang="ts">
2
+ // (unscoped styles by design — see base.css wiring:d2 block; SFC scoped
3
+ // styles are banned for webcomponent data-v hash mismatch reasons.)
4
+ /**
5
+ * GalleryView — wiring:d2. Third view mode: large-thumbnail gallery for
6
+ * visual browsing (photos/videos). Derived from GridView (same props/emits,
7
+ * same listbox a11y, same selection/drag/touch contract) but renders big
8
+ * square tiles: thumbnail (or SVG file-type icon fallback) with the name
9
+ * below and size+date revealed on hover/focus. GridView itself is untouched.
10
+ */
11
+ import { ref } from 'vue';
12
+ import type { FileNode } from '../types/FileNode';
13
+ import type { LocaleCode } from '../types/ExplorerConfig';
14
+ import { useLocale } from '../composables/useLocale';
15
+ import { fileIconSvg } from '../lib/fileIcons';
16
+ import { applyDragGhost } from '../lib/dragGhost';
17
+
18
+ const props = defineProps<{
19
+ files: FileNode[];
20
+ selected: Set<string>;
21
+ clipped?: Set<string>;
22
+ showParentPath?: boolean;
23
+ locale: LocaleCode;
24
+ loading?: boolean;
25
+ /** Authenticated thumb resolver (useThumbs.src) — same contract as
26
+ * GridView: raw `thumb_url` is root-relative and unauthenticated, so
27
+ * embedded hosts NEED this. null = icon fallback. */
28
+ thumbSrc?: (n: FileNode) => string | null;
29
+ }>();
30
+
31
+ const emit = defineEmits<{
32
+ (e: 'click-card', node: FileNode, mod: { ctrl: boolean; shift: boolean }): void;
33
+ (e: 'dbl-card', node: FileNode): void;
34
+ (e: 'context-card', node: FileNode, ev: MouseEvent): void;
35
+ (e: 'item-drag-start', node: FileNode, ev: DragEvent): void;
36
+ (e: 'item-drop-into', target: FileNode, ev: DragEvent): void;
37
+ }>();
38
+
39
+ const { t, formatSize, nodeDisplayName } = useLocale(() => props.locale);
40
+
41
+ function thumbOf(n: FileNode): string | null {
42
+ return props.thumbSrc ? props.thumbSrc(n) : (n.thumb_url ?? null);
43
+ }
44
+
45
+ function isSelected(n: FileNode): boolean {
46
+ return props.selected.has(n.path);
47
+ }
48
+
49
+ function onClick(n: FileNode, ev: MouseEvent) {
50
+ emit('click-card', n, { ctrl: ev.ctrlKey || ev.metaKey, shift: ev.shiftKey });
51
+ }
52
+
53
+ function onDbl(n: FileNode) {
54
+ emit('dbl-card', n);
55
+ }
56
+
57
+ function onCtx(n: FileNode, ev: MouseEvent) {
58
+ ev.preventDefault();
59
+ ev.stopPropagation();
60
+ emit('context-card', n, ev);
61
+ }
62
+
63
+ function onItemDragStart(n: FileNode, ev: DragEvent) {
64
+ applyDragGhost(
65
+ ev,
66
+ nodeDisplayName(n),
67
+ props.selected.has(n.path) ? props.selected.size : 1,
68
+ );
69
+ emit('item-drag-start', n, ev);
70
+ }
71
+
72
+ // Drop-target highlight — mirrors GridView (visual layer only).
73
+ const dropTargetPath = ref<string | null>(null);
74
+
75
+ function onItemDragOver(n: FileNode, ev: DragEvent) {
76
+ if (n.type !== 'dir') return;
77
+ if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
78
+ ev.preventDefault();
79
+ ev.stopPropagation();
80
+ if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
81
+ dropTargetPath.value = n.path;
82
+ }
83
+
84
+ function onItemDragLeave(n: FileNode) {
85
+ if (dropTargetPath.value === n.path) dropTargetPath.value = null;
86
+ }
87
+
88
+ function onItemDrop(n: FileNode, ev: DragEvent) {
89
+ dropTargetPath.value = null;
90
+ if (n.type !== 'dir') return;
91
+ if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
92
+ ev.preventDefault();
93
+ ev.stopPropagation();
94
+ emit('item-drop-into', n, ev);
95
+ }
96
+
97
+ // Long-press → context menu, same as GridView (touch parity).
98
+ let pressTimer: ReturnType<typeof setTimeout> | undefined;
99
+ let pressTarget: FileNode | null = null;
100
+ function onTouchStart(n: FileNode, ev: TouchEvent) {
101
+ pressTarget = n;
102
+ if (pressTimer) clearTimeout(pressTimer);
103
+ pressTimer = setTimeout(() => {
104
+ if (pressTarget) {
105
+ const t0 = ev.touches[0];
106
+ emit('context-card', pressTarget, {
107
+ clientX: t0.clientX,
108
+ clientY: t0.clientY,
109
+ preventDefault: () => {},
110
+ } as unknown as MouseEvent);
111
+ }
112
+ }, 500);
113
+ }
114
+ function cancelPress() {
115
+ if (pressTimer) clearTimeout(pressTimer);
116
+ pressTarget = null;
117
+ }
118
+
119
+ function parentDir(path: string): string {
120
+ const stripped = path.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
121
+ const idx = stripped.lastIndexOf('/');
122
+ if (idx === -1) return '';
123
+ return stripped.slice(0, idx);
124
+ }
125
+
126
+ // Special rows keep their emoji (trash/storage are not file-TYPE icons).
127
+ function specialEmojiFor(n: FileNode): string | null {
128
+ if (n.basename === '.trash') return '🗑';
129
+ if (n.mime_type === 'inode/storage') return '💾';
130
+ return null;
131
+ }
132
+
133
+ function displayDate(ms: number | undefined): string {
134
+ if (!ms) return '';
135
+ const d = new Date(ms * (ms < 1e12 ? 1000 : 1));
136
+ return d.toLocaleString();
137
+ }
138
+
139
+ // Hover meta line: size for files, entry count (when known) for dirs.
140
+ function metaFor(n: FileNode): string {
141
+ const date = displayDate(n.last_modified);
142
+ const size = n.type === 'dir' ? '' : formatSize(n.size);
143
+ return [size, date].filter((s) => !!s).join(' · ');
144
+ }
145
+ </script>
146
+
147
+ <template>
148
+ <div
149
+ class="fe-gal"
150
+ :class="{ 'is-loading': loading }"
151
+ role="listbox"
152
+ aria-multiselectable="true"
153
+ :aria-label="t('gallery.aria')"
154
+ :aria-busy="loading ? 'true' : undefined"
155
+ >
156
+ <div
157
+ v-for="n in files"
158
+ :key="n.path"
159
+ class="fe-gal__card"
160
+ :class="{
161
+ 'is-selected': isSelected(n),
162
+ 'is-dir': n.type === 'dir',
163
+ 'is-trash': n.trashed,
164
+ 'is-clipped': clipped?.has(n.path),
165
+ 'is-droptarget': dropTargetPath === n.path,
166
+ }"
167
+ tabindex="0"
168
+ role="option"
169
+ :aria-selected="isSelected(n) ? 'true' : 'false'"
170
+ :aria-label="nodeDisplayName(n)"
171
+ draggable="true"
172
+ @click="onClick(n, $event)"
173
+ @dblclick="onDbl(n)"
174
+ @contextmenu="onCtx(n, $event)"
175
+ @dragstart="onItemDragStart(n, $event)"
176
+ @dragover="onItemDragOver(n, $event)"
177
+ @dragleave="onItemDragLeave(n)"
178
+ @drop="onItemDrop(n, $event)"
179
+ @touchstart.passive="onTouchStart(n, $event)"
180
+ @touchend="cancelPress"
181
+ @touchmove="cancelPress"
182
+ >
183
+ <div class="fe-gal__thumb">
184
+ <!-- draggable="false" for the same reason as GridView: HTML5 image
185
+ drag adds a 'Files' MIME to dataTransfer, which would trip the
186
+ parent's upload handler on internal drags. -->
187
+ <img
188
+ v-if="thumbOf(n)"
189
+ :src="thumbOf(n)!"
190
+ :alt="n.basename"
191
+ loading="lazy"
192
+ draggable="false"
193
+ />
194
+ <span v-else-if="specialEmojiFor(n)" class="fe-gal__icon">{{ specialEmojiFor(n) }}</span>
195
+ <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
196
+ <span v-else class="fe-gal__icon fe-gal__icon--svg" v-html="fileIconSvg(n)"></span>
197
+ <div class="fe-gal__meta" aria-hidden="true">
198
+ <span v-if="metaFor(n)" class="fe-gal__meta-line">{{ metaFor(n) }}</span>
199
+ <span
200
+ v-if="showParentPath && parentDir(n.path)"
201
+ class="fe-gal__meta-line fe-gal__meta-line--path"
202
+ :title="parentDir(n.path)"
203
+ >{{ parentDir(n.path) }}</span>
204
+ </div>
205
+ </div>
206
+ <div class="fe-gal__label" :title="n.basename">
207
+ {{ nodeDisplayName(n) }}
208
+ </div>
209
+ </div>
210
+ <div v-if="!loading && files.length === 0" class="fe-gal__empty">
211
+ {{ t('empty.folder') }}
212
+ </div>
213
+ </div>
214
+ </template>
@@ -2,11 +2,13 @@
2
2
  /**
3
3
  * GridView — card grid. Thumbnails preferred, fall back to icon.
4
4
  */
5
+ import { ref } from 'vue'; /* wiring:c4 */
5
6
  import type { FileNode } from '../types/FileNode';
6
7
  import type { LocaleCode } from '../types/ExplorerConfig';
7
8
  import { useLocale } from '../composables/useLocale';
8
9
  import { fileIconSvg } from '../lib/fileIcons';
9
10
  import { snippetSegments } from '../lib/snippet'; /* bul:s3 */
11
+ import { applyDragGhost } from '../lib/dragGhost'; /* wiring:c4 */
10
12
 
11
13
  const props = defineProps<{
12
14
  files: FileNode[];
@@ -56,18 +58,34 @@ function onCtx(n: FileNode, ev: MouseEvent) {
56
58
  }
57
59
 
58
60
  function onItemDragStart(n: FileNode, ev: DragEvent) {
61
+ /* wiring:c4 — custom ghost (name + multi-select count badge), visual only. */
62
+ applyDragGhost(
63
+ ev,
64
+ nodeDisplayName(n),
65
+ props.selected.has(n.path) ? props.selected.size : 1,
66
+ );
59
67
  emit('item-drag-start', n, ev);
60
68
  }
61
69
 
70
+ /* wiring:c4 — droptarget highlight, mirrors ListView (visual layer only). */
71
+ const dropTargetPath = ref<string | null>(null);
72
+
62
73
  function onItemDragOver(n: FileNode, ev: DragEvent) {
63
74
  if (n.type !== 'dir') return;
64
75
  if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
65
76
  ev.preventDefault();
66
77
  ev.stopPropagation();
67
78
  if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
79
+ dropTargetPath.value = n.path; /* wiring:c4 */
80
+ }
81
+
82
+ /* wiring:c4 */
83
+ function onItemDragLeave(n: FileNode) {
84
+ if (dropTargetPath.value === n.path) dropTargetPath.value = null;
68
85
  }
69
86
 
70
87
  function onItemDrop(n: FileNode, ev: DragEvent) {
88
+ dropTargetPath.value = null; /* wiring:c4 */
71
89
  if (n.type !== 'dir') return;
72
90
  if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
73
91
  ev.preventDefault();
@@ -128,7 +146,16 @@ function snippetTitle(snippet: string): string {
128
146
  </script>
129
147
 
130
148
  <template>
131
- <div class="fe-grid" :class="{ 'is-loading': loading }">
149
+ <!-- wiring:c4 — listbox semantics (multi-selectable cards as options);
150
+ localized label + busy state. Structure/layout untouched. -->
151
+ <div
152
+ class="fe-grid"
153
+ :class="{ 'is-loading': loading }"
154
+ role="listbox"
155
+ aria-multiselectable="true"
156
+ :aria-label="t('grid.aria')"
157
+ :aria-busy="loading ? 'true' : undefined"
158
+ >
132
159
  <div
133
160
  v-for="n in files"
134
161
  :key="n.path"
@@ -138,14 +165,20 @@ function snippetTitle(snippet: string): string {
138
165
  'is-dir': n.type === 'dir',
139
166
  'is-trash': n.trashed,
140
167
  'is-clipped': clipped?.has(n.path),
168
+ 'is-droptarget': dropTargetPath === n.path /* wiring:c4 */,
141
169
  }"
142
170
  tabindex="0"
171
+ role="option"
172
+ :aria-selected="isSelected(n) ? 'true' : 'false'"
173
+ :aria-label="nodeDisplayName(n) /* wiring:c4 */"
174
+ :data-fe-path="n.path /* wiring:d1 — orta-tık yeni sekme delegasyonu */"
143
175
  draggable="true"
144
176
  @click="onClick(n, $event)"
145
177
  @dblclick="onDbl(n)"
146
178
  @contextmenu="onCtx(n, $event)"
147
179
  @dragstart="onItemDragStart(n, $event)"
148
180
  @dragover="onItemDragOver(n, $event)"
181
+ @dragleave="onItemDragLeave(n) /* wiring:c4 */"
149
182
  @drop="onItemDrop(n, $event)"
150
183
  @touchstart.passive="onTouchStart(n, $event)"
151
184
  @touchend="cancelPress"