@brftech/filex-core 0.4.2 → 0.5.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 (51) hide show
  1. package/dist/{ArchiveViewer-B_0IQExT.js → ArchiveViewer-DKW-HyXT.js} +2 -2
  2. package/dist/{ArchiveViewer-B_0IQExT.js.map → ArchiveViewer-DKW-HyXT.js.map} +1 -1
  3. package/dist/{CsvViewer-maMfUy5i.js → CsvViewer-D2HmP2yr.js} +2 -2
  4. package/dist/{CsvViewer-maMfUy5i.js.map → CsvViewer-D2HmP2yr.js.map} +1 -1
  5. package/dist/{DrawioViewer-DP_pXI00.js → DrawioViewer-DPNvOFCs.js} +2 -2
  6. package/dist/{DrawioViewer-DP_pXI00.js.map → DrawioViewer-DPNvOFCs.js.map} +1 -1
  7. package/dist/{EpubViewer-DSaqGnVx.js → EpubViewer-ByqjNuNL.js} +2 -2
  8. package/dist/{EpubViewer-DSaqGnVx.js.map → EpubViewer-ByqjNuNL.js.map} +1 -1
  9. package/dist/{IpynbViewer-BmdBanF1.js → IpynbViewer-2iONhbuJ.js} +2 -2
  10. package/dist/{IpynbViewer-BmdBanF1.js.map → IpynbViewer-2iONhbuJ.js.map} +1 -1
  11. package/dist/{MermaidViewer-Drlkl0Z5.js → MermaidViewer-CmiU7hIT.js} +2 -2
  12. package/dist/{MermaidViewer-Drlkl0Z5.js.map → MermaidViewer-CmiU7hIT.js.map} +1 -1
  13. package/dist/{PsdViewer-gftUJrsm.js → PsdViewer-Inhl2yN9.js} +2 -2
  14. package/dist/{PsdViewer-gftUJrsm.js.map → PsdViewer-Inhl2yN9.js.map} +1 -1
  15. package/dist/{TiffViewer-0Uv0-Lxh.js → TiffViewer-C2AFFEze.js} +2 -2
  16. package/dist/{TiffViewer-0Uv0-Lxh.js.map → TiffViewer-C2AFFEze.js.map} +1 -1
  17. package/dist/{Viewer3D-Gu3kUpxV.js → Viewer3D-YdW14q_n.js} +2 -2
  18. package/dist/{Viewer3D-Gu3kUpxV.js.map → Viewer3D-YdW14q_n.js.map} +1 -1
  19. package/dist/filex-core.js +47 -25
  20. package/dist/filex-core.umd.cjs +40 -40
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/index-eARevpz0.js +9952 -0
  23. package/dist/index-eARevpz0.js.map +1 -0
  24. package/dist/index.d.ts +363 -3
  25. package/dist/style.css +1 -1
  26. package/package.json +1 -1
  27. package/src/FileExplorer.vue +269 -1
  28. package/src/components/CommandPalette.vue +12 -0
  29. package/src/components/ContextMenu.vue +114 -8
  30. package/src/components/GridView.vue +33 -1
  31. package/src/components/ListView.vue +39 -6
  32. package/src/components/OnboardingTour.vue +332 -0
  33. package/src/components/OperationsCenter.vue +284 -0
  34. package/src/components/PendingOpsTray.vue +50 -66
  35. package/src/components/QuickLook.vue +142 -0
  36. package/src/components/ShortcutSettings.vue +270 -0
  37. package/src/components/ShortcutsHelp.vue +30 -9
  38. package/src/components/ThemeGallery.vue +253 -0
  39. package/src/components/Toolbar.vue +52 -6
  40. package/src/components/UploadProgress.vue +49 -57
  41. package/src/composables/useKeyboardShortcuts.ts +321 -137
  42. package/src/composables/useOperations.ts +291 -0
  43. package/src/index.ts +45 -0
  44. package/src/lib/dragGhost.ts +41 -0
  45. package/src/lib/themes.ts +505 -0
  46. package/src/locales/en.ts +92 -0
  47. package/src/locales/tr.ts +92 -0
  48. package/src/modals/Modal.vue +41 -5
  49. package/src/styles/base.css +672 -0
  50. package/dist/index-BLocdbmc.js +0 -7993
  51. package/dist/index-BLocdbmc.js.map +0 -1
@@ -12,6 +12,7 @@ import type { LocaleCode } from '../types/ExplorerConfig';
12
12
  import { useLocale } from '../composables/useLocale';
13
13
  import { fileIconSvg } from '../lib/fileIcons';
14
14
  import { matchedInContent, snippetSegments } from '../lib/snippet'; /* bul:s3 */
15
+ import { applyDragGhost } from '../lib/dragGhost'; /* wiring:c4 */
15
16
  import StarButton from './StarButton.vue';
16
17
 
17
18
  const props = defineProps<{
@@ -71,18 +72,38 @@ function onRowCtx(n: FileNode, ev: MouseEvent) {
71
72
  }
72
73
 
73
74
  function onItemDragStart(n: FileNode, ev: DragEvent) {
75
+ /* wiring:c4 — custom ghost (name + multi-select count badge). Visual
76
+ * only; the payload/selection logic stays in FileExplorer. */
77
+ applyDragGhost(
78
+ ev,
79
+ nodeDisplayName(n),
80
+ props.selected.has(n.path) ? props.selected.size : 1,
81
+ );
74
82
  emit('item-drag-start', n, ev);
75
83
  }
76
84
 
85
+ /* wiring:c4 — droptarget highlight: the folder row currently hovered by an
86
+ * internal drag gets `.is-droptarget`. Purely visual — the accept/deny
87
+ * decision below is untouched (files never preventDefault, so the browser
88
+ * shows the native no-drop cursor on invalid targets). */
89
+ const dropTargetPath = ref<string | null>(null);
90
+
77
91
  function onItemDragOver(n: FileNode, ev: DragEvent) {
78
92
  if (n.type !== 'dir') return;
79
93
  if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
80
94
  ev.preventDefault();
81
95
  ev.stopPropagation();
82
96
  if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
97
+ dropTargetPath.value = n.path; /* wiring:c4 */
98
+ }
99
+
100
+ /* wiring:c4 */
101
+ function onItemDragLeave(n: FileNode) {
102
+ if (dropTargetPath.value === n.path) dropTargetPath.value = null;
83
103
  }
84
104
 
85
105
  function onItemDrop(n: FileNode, ev: DragEvent) {
106
+ dropTargetPath.value = null; /* wiring:c4 */
86
107
  if (n.type !== 'dir') return;
87
108
  if (!ev.dataTransfer?.types.includes('application/x-brf-files')) return;
88
109
  ev.preventDefault();
@@ -292,9 +313,17 @@ const segments = computed<Segment[]>(() => {
292
313
  </script>
293
314
 
294
315
  <template>
295
- <div class="fe-list" :class="{ 'is-loading': loading, 'has-star-col': !!apiBase || apiBase === '' }">
316
+ <!-- wiring:c4 grid semantics: container role + localized labels; rows
317
+ carry aria-selected below. Structure/layout untouched. -->
318
+ <div
319
+ class="fe-list"
320
+ :class="{ 'is-loading': loading, 'has-star-col': !!apiBase || apiBase === '' }"
321
+ role="grid"
322
+ :aria-label="t('list.aria')"
323
+ :aria-busy="loading ? 'true' : undefined"
324
+ >
296
325
  <div class="fe-list__head" role="row">
297
- <div class="fe-list__col fe-list__col--star" role="columnheader" aria-label="Star"></div>
326
+ <div class="fe-list__col fe-list__col--star" role="columnheader" :aria-label="t('col.star') /* wiring:c4 */"></div>
298
327
  <div class="fe-list__col fe-list__col--name" role="columnheader" :aria-sort="ariaSort('name')">
299
328
  <button type="button" class="fe-list__sort" :title="t('col.sort')" @click="toggleSort('name')">
300
329
  {{ t('col.name') }}
@@ -326,21 +355,25 @@ const segments = computed<Segment[]>(() => {
326
355
  'is-dir': n.type === 'dir',
327
356
  'is-trash': n.trashed,
328
357
  'is-clipped': clipped?.has(n.path),
358
+ 'is-droptarget': dropTargetPath === n.path /* wiring:c4 */,
329
359
  }"
330
360
  role="row"
331
361
  tabindex="0"
362
+ :aria-selected="isSelected(n) ? 'true' : 'false'"
363
+ :aria-label="nodeDisplayName(n) /* wiring:c4 */"
332
364
  draggable="true"
333
365
  @click="onRowClick(n, $event)"
334
366
  @dblclick="onRowDbl(n)"
335
367
  @contextmenu="onRowCtx(n, $event)"
336
368
  @dragstart="onItemDragStart(n, $event)"
337
369
  @dragover="onItemDragOver(n, $event)"
370
+ @dragleave="onItemDragLeave(n) /* wiring:c4 */"
338
371
  @drop="onItemDrop(n, $event)"
339
372
  @touchstart.passive="onTouchStart(n, $event)"
340
373
  @touchend="cancelPress"
341
374
  @touchmove="cancelPress"
342
375
  >
343
- <div class="fe-list__col fe-list__col--star" @click.stop>
376
+ <div class="fe-list__col fe-list__col--star" role="gridcell" @click.stop>
344
377
  <StarButton
345
378
  v-if="typeof n.id === 'number' && n.type === 'file'"
346
379
  :starred="!!starredIds?.has(n.id)"
@@ -351,7 +384,7 @@ const segments = computed<Segment[]>(() => {
351
384
  @change="(val: boolean) => emit('star-change', n, val)"
352
385
  />
353
386
  </div>
354
- <div class="fe-list__col fe-list__col--name">
387
+ <div class="fe-list__col fe-list__col--name" role="gridcell">
355
388
  <span v-if="specialEmojiFor(n)" class="fe-list__icon" aria-hidden="true">{{ specialEmojiFor(n) }}</span>
356
389
  <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
357
390
  <span v-else class="fe-list__icon fe-list__icon--svg" aria-hidden="true" v-html="fileIconSvg(n)"></span>
@@ -375,10 +408,10 @@ const segments = computed<Segment[]>(() => {
375
408
  </span>
376
409
  </div>
377
410
  </div>
378
- <div class="fe-list__col fe-list__col--size">
411
+ <div class="fe-list__col fe-list__col--size" role="gridcell">
379
412
  {{ formatSize(n.size) }}
380
413
  </div>
381
- <div class="fe-list__col fe-list__col--mod">
414
+ <div class="fe-list__col fe-list__col--mod" role="gridcell">
382
415
  {{ displayDate(n.last_modified) }}
383
416
  </div>
384
417
  </div>
@@ -0,0 +1,332 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * OnboardingTour — wiring:c4 first-use coach-mark tour.
4
+ *
5
+ * A dependency-free spotlight walkthrough: each step targets a live DOM
6
+ * element inside the explorer root (resolved lazily — steps whose target
7
+ * is missing are skipped, so confined embeds with hidden surfaces never
8
+ * show a dangling spotlight). Positioning is hand-rolled with
9
+ * getBoundingClientRect + fixed overlay; the spotlight is a rounded
10
+ * cutout drawn with a huge box-shadow.
11
+ *
12
+ * Teleported under <body> (same reason as ContextMenu: transformed embed
13
+ * ancestors break position:fixed) and re-uses the `.fe-ctx-backdrop--theme-*`
14
+ * classes so the CSS-variable theme cascade applies outside the `.fe` tree.
15
+ *
16
+ * Persistence (`filex.tourDone`) is the PARENT's job — the tour only
17
+ * emits `close`; FileExplorer decides when it counts as done.
18
+ */
19
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
20
+ import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
21
+ import { useLocale } from '../composables/useLocale';
22
+
23
+ const props = defineProps<{
24
+ open: boolean;
25
+ locale: LocaleCode;
26
+ /** Explorer root — targets are resolved inside it only. */
27
+ root: HTMLElement | null;
28
+ theme?: ThemeMode;
29
+ }>();
30
+
31
+ const emit = defineEmits<{
32
+ (e: 'close'): void;
33
+ }>();
34
+
35
+ const { t } = useLocale(() => props.locale);
36
+
37
+ // ------------------------------------------------------------------
38
+ // Step definitions. `target` returns the element to spotlight (null =
39
+ // centered card, e.g. the closing shortcuts step). Buttons without a
40
+ // stable class are found via their localized title attribute — the
41
+ // toolbar gives every icon-only button a title from the same catalogue.
42
+ // ------------------------------------------------------------------
43
+
44
+ function byTitle(root: HTMLElement, label: string): HTMLElement | null {
45
+ const nodes = root.querySelectorAll<HTMLElement>('button[title]');
46
+ for (const el of Array.from(nodes)) {
47
+ if (el.getAttribute('title') === label) return el;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ interface TourStep {
53
+ id: string;
54
+ titleKey: string;
55
+ descKey: string;
56
+ target: (root: HTMLElement) => HTMLElement | null;
57
+ }
58
+
59
+ const STEPS: TourStep[] = [
60
+ {
61
+ id: 'nav',
62
+ titleKey: 'tour.step.nav.title',
63
+ descKey: 'tour.step.nav.desc',
64
+ target: (r) => r.querySelector<HTMLElement>('.fe-breadcrumb'),
65
+ },
66
+ {
67
+ id: 'upload',
68
+ titleKey: 'tour.step.upload.title',
69
+ descKey: 'tour.step.upload.desc',
70
+ target: (r) =>
71
+ byTitle(r, t('toolbar.upload')) || r.querySelector<HTMLElement>('.fe-fab'),
72
+ },
73
+ {
74
+ id: 'search',
75
+ titleKey: 'tour.step.search.title',
76
+ descKey: 'tour.step.search.desc',
77
+ target: (r) =>
78
+ r.querySelector<HTMLElement>('.fe-search__input') ||
79
+ r.querySelector<HTMLElement>('.fe-toolbar__search-toggle'),
80
+ },
81
+ {
82
+ id: 'view',
83
+ titleKey: 'tour.step.view.title',
84
+ descKey: 'tour.step.view.desc',
85
+ target: (r) => r.querySelector<HTMLElement>('.fe-toolbar__view'),
86
+ },
87
+ {
88
+ id: 'share',
89
+ titleKey: 'tour.step.share.title',
90
+ descKey: 'tour.step.share.desc',
91
+ target: (r) => r.querySelector<HTMLElement>('.fe__body'),
92
+ },
93
+ {
94
+ id: 'help',
95
+ titleKey: 'tour.step.help.title',
96
+ descKey: 'tour.step.help.desc',
97
+ target: () => null,
98
+ },
99
+ ];
100
+
101
+ // Steps re-resolved on every open so surfaces that appeared/disappeared
102
+ // since mount (narrow-mode collapse, RBAC hiding upload…) are honored.
103
+ const activeSteps = ref<TourStep[]>([]);
104
+ const stepIdx = ref(0);
105
+
106
+ const step = computed<TourStep | null>(() => activeSteps.value[stepIdx.value] ?? null);
107
+ const total = computed(() => activeSteps.value.length);
108
+
109
+ function resolveSteps() {
110
+ const r = props.root;
111
+ activeSteps.value = STEPS.filter((s) => {
112
+ if (s.id === 'help') return true; // centered card — no DOM dependency
113
+ if (!r) return false;
114
+ const el = s.target(r);
115
+ // Element must exist AND be laid out (visible) — a display:none
116
+ // toolbar section in narrow mode yields a 0×0 rect.
117
+ if (!el) return false;
118
+ const rect = el.getBoundingClientRect();
119
+ return rect.width > 0 && rect.height > 0;
120
+ });
121
+ }
122
+
123
+ // ------------------------------------------------------------------
124
+ // Spotlight + card geometry
125
+ // ------------------------------------------------------------------
126
+
127
+ interface Rect {
128
+ top: number;
129
+ left: number;
130
+ width: number;
131
+ height: number;
132
+ }
133
+ const spot = ref<Rect | null>(null);
134
+ const cardStyle = ref<Record<string, string>>({});
135
+ const cardEl = ref<HTMLElement | null>(null);
136
+
137
+ const PAD = 6; // spotlight breathing room around the target
138
+ const GAP = 12; // gap between spotlight and the card
139
+
140
+ async function place() {
141
+ const s = step.value;
142
+ const r = props.root;
143
+ if (!s) return;
144
+ const el = r ? s.target(r) : null;
145
+ if (!el) {
146
+ spot.value = null; // centered card
147
+ cardStyle.value = {};
148
+ return;
149
+ }
150
+ const rect = el.getBoundingClientRect();
151
+ if (rect.width <= 0 && rect.height <= 0) {
152
+ spot.value = null;
153
+ cardStyle.value = {};
154
+ return;
155
+ }
156
+ spot.value = {
157
+ top: rect.top - PAD,
158
+ left: rect.left - PAD,
159
+ width: rect.width + PAD * 2,
160
+ height: rect.height + PAD * 2,
161
+ };
162
+ // Card: below the spotlight when there's room, above otherwise;
163
+ // clamped into the viewport horizontally.
164
+ await nextTick();
165
+ const vw = window.innerWidth;
166
+ const vh = window.innerHeight;
167
+ const cw = Math.min(340, vw - 24);
168
+ const ch = cardEl.value?.offsetHeight ?? 160;
169
+ let top = rect.bottom + PAD + GAP;
170
+ if (top + ch > vh - 12) top = Math.max(12, rect.top - PAD - GAP - ch);
171
+ let left = rect.left + rect.width / 2 - cw / 2;
172
+ left = Math.min(Math.max(12, left), vw - cw - 12);
173
+ cardStyle.value = {
174
+ top: `${Math.round(top)}px`,
175
+ left: `${Math.round(left)}px`,
176
+ width: `${cw}px`,
177
+ };
178
+ }
179
+
180
+ function onViewportChange() {
181
+ if (props.open) void place();
182
+ }
183
+
184
+ // ------------------------------------------------------------------
185
+ // Flow
186
+ // ------------------------------------------------------------------
187
+
188
+ function next() {
189
+ if (stepIdx.value >= total.value - 1) {
190
+ emit('close');
191
+ return;
192
+ }
193
+ stepIdx.value += 1;
194
+ }
195
+
196
+ function back() {
197
+ if (stepIdx.value > 0) stepIdx.value -= 1;
198
+ }
199
+
200
+ function skip() {
201
+ emit('close');
202
+ }
203
+
204
+ function onKey(e: KeyboardEvent) {
205
+ if (!props.open) return;
206
+ if (e.key === 'Escape') {
207
+ e.stopPropagation();
208
+ skip();
209
+ } else if (e.key === 'ArrowRight' || e.key === 'Enter') {
210
+ e.preventDefault();
211
+ next();
212
+ } else if (e.key === 'ArrowLeft') {
213
+ e.preventDefault();
214
+ back();
215
+ }
216
+ }
217
+
218
+ watch(
219
+ () => props.open,
220
+ async (v) => {
221
+ if (v) {
222
+ stepIdx.value = 0;
223
+ resolveSteps();
224
+ await nextTick();
225
+ await place();
226
+ cardEl.value?.focus();
227
+ }
228
+ },
229
+ );
230
+
231
+ watch(stepIdx, async () => {
232
+ await place();
233
+ cardEl.value?.focus();
234
+ });
235
+
236
+ onMounted(() => {
237
+ window.addEventListener('resize', onViewportChange);
238
+ window.addEventListener('scroll', onViewportChange, true);
239
+ });
240
+ onBeforeUnmount(() => {
241
+ window.removeEventListener('resize', onViewportChange);
242
+ window.removeEventListener('scroll', onViewportChange, true);
243
+ });
244
+
245
+ // Theme cascade outside `.fe` — same pattern as ContextMenu.
246
+ const prefersDark = ref(false);
247
+ let mq: MediaQueryList | undefined;
248
+ function syncPrefersDark(e?: MediaQueryListEvent | MediaQueryList) {
249
+ prefersDark.value = !!(e && 'matches' in e && e.matches);
250
+ }
251
+ onMounted(() => {
252
+ if (typeof window === 'undefined') return;
253
+ mq = window.matchMedia('(prefers-color-scheme: dark)');
254
+ syncPrefersDark(mq);
255
+ mq.addEventListener?.('change', syncPrefersDark);
256
+ });
257
+ onBeforeUnmount(() => {
258
+ mq?.removeEventListener?.('change', syncPrefersDark);
259
+ });
260
+ const themeClass = computed(() => `fe-ctx-backdrop--theme-${props.theme || 'auto'}`);
261
+ </script>
262
+
263
+ <template>
264
+ <Teleport to="body">
265
+ <transition name="fe-tour">
266
+ <div
267
+ v-if="open && step"
268
+ class="fe-tour"
269
+ :class="themeClass"
270
+ :data-prefers-dark="prefersDark ? '1' : '0'"
271
+ role="dialog"
272
+ aria-modal="true"
273
+ :aria-label="t('tour.aria')"
274
+ @keydown="onKey"
275
+ >
276
+ <!-- Spotlight: rounded cutout via huge box-shadow. Centered steps
277
+ (no target) use a full-dim overlay instead. -->
278
+ <div
279
+ v-if="spot"
280
+ class="fe-tour__spot"
281
+ :style="{
282
+ top: spot.top + 'px',
283
+ left: spot.left + 'px',
284
+ width: spot.width + 'px',
285
+ height: spot.height + 'px',
286
+ }"
287
+ aria-hidden="true"
288
+ />
289
+ <div v-else class="fe-tour__dim" aria-hidden="true" />
290
+
291
+ <div
292
+ ref="cardEl"
293
+ class="fe-tour__card"
294
+ :class="{ 'fe-tour__card--center': !spot }"
295
+ :style="spot ? cardStyle : {}"
296
+ tabindex="-1"
297
+ >
298
+ <p class="fe-tour__progress" aria-hidden="true">
299
+ {{ t('tour.progress', { n: stepIdx + 1, m: total }) }}
300
+ </p>
301
+ <h3 class="fe-tour__title">{{ t(step.titleKey) }}</h3>
302
+ <p class="fe-tour__desc">{{ t(step.descKey) }}</p>
303
+ <div class="fe-tour__dots" aria-hidden="true">
304
+ <span
305
+ v-for="(s, i) in activeSteps"
306
+ :key="s.id"
307
+ class="fe-tour__dot"
308
+ :class="{ 'is-active': i === stepIdx }"
309
+ />
310
+ </div>
311
+ <div class="fe-tour__actions">
312
+ <button type="button" class="fe-btn fe-tour__skip" @click="skip">
313
+ {{ t('tour.skip') }}
314
+ </button>
315
+ <span class="fe-tour__actions-spacer" />
316
+ <button
317
+ v-if="stepIdx > 0"
318
+ type="button"
319
+ class="fe-btn"
320
+ @click="back"
321
+ >
322
+ {{ t('tour.back') }}
323
+ </button>
324
+ <button type="button" class="fe-btn fe-btn--primary" @click="next">
325
+ {{ stepIdx >= total - 1 ? t('tour.done') : t('tour.next') }}
326
+ </button>
327
+ </div>
328
+ </div>
329
+ </div>
330
+ </transition>
331
+ </Teleport>
332
+ </template>