@brftech/filex-core 0.41.2 → 0.41.4

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 (34) hide show
  1. package/dist/{ArchiveViewer-Dq1c4-G5.js → ArchiveViewer-oqaEyqcB.js} +2 -2
  2. package/dist/{ArchiveViewer-Dq1c4-G5.js.map → ArchiveViewer-oqaEyqcB.js.map} +1 -1
  3. package/dist/{CsvViewer-CeHslAc0.js → CsvViewer-BfT-4WpB.js} +2 -2
  4. package/dist/{CsvViewer-CeHslAc0.js.map → CsvViewer-BfT-4WpB.js.map} +1 -1
  5. package/dist/{DrawioViewer-p03u7Oda.js → DrawioViewer-Cu2ZquNO.js} +2 -2
  6. package/dist/{DrawioViewer-p03u7Oda.js.map → DrawioViewer-Cu2ZquNO.js.map} +1 -1
  7. package/dist/{EpubViewer-D08Y-Tww.js → EpubViewer-yfAfnrYF.js} +2 -2
  8. package/dist/{EpubViewer-D08Y-Tww.js.map → EpubViewer-yfAfnrYF.js.map} +1 -1
  9. package/dist/{IpynbViewer-mG7p3Sil.js → IpynbViewer-DLSOs3yU.js} +2 -2
  10. package/dist/{IpynbViewer-mG7p3Sil.js.map → IpynbViewer-DLSOs3yU.js.map} +1 -1
  11. package/dist/{MermaidViewer-CCPg1Q3W.js → MermaidViewer-D2AsT14x.js} +2 -2
  12. package/dist/{MermaidViewer-CCPg1Q3W.js.map → MermaidViewer-D2AsT14x.js.map} +1 -1
  13. package/dist/{PsdViewer-CDEHTiUN.js → PsdViewer-DOza4QJt.js} +2 -2
  14. package/dist/{PsdViewer-CDEHTiUN.js.map → PsdViewer-DOza4QJt.js.map} +1 -1
  15. package/dist/{TiffViewer-DnWRUGOK.js → TiffViewer-24MCOn1t.js} +2 -2
  16. package/dist/{TiffViewer-DnWRUGOK.js.map → TiffViewer-24MCOn1t.js.map} +1 -1
  17. package/dist/{Viewer3D-COCQp53D.js → Viewer3D-BVjtbhhc.js} +2 -2
  18. package/dist/{Viewer3D-COCQp53D.js.map → Viewer3D-BVjtbhhc.js.map} +1 -1
  19. package/dist/filex-core.js +1 -1
  20. package/dist/filex-core.umd.cjs +46 -46
  21. package/dist/filex-core.umd.cjs.map +1 -1
  22. package/dist/{index-JUiZzLmF.js → index-B8PLGR-X.js} +9146 -9075
  23. package/dist/index-B8PLGR-X.js.map +1 -0
  24. package/dist/style.css +1 -1
  25. package/package.json +1 -1
  26. package/src/components/FilePane.vue +21 -33
  27. package/src/components/GalleryView.vue +44 -8
  28. package/src/components/GridView.vue +58 -19
  29. package/src/components/HomeView.vue +5 -4
  30. package/src/components/ItemCheck.vue +47 -0
  31. package/src/components/ListView.vue +51 -48
  32. package/src/composables/useRowTouch.ts +85 -35
  33. package/src/styles/base.css +111 -6
  34. package/dist/index-JUiZzLmF.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brftech/filex-core",
3
- "version": "0.41.2",
3
+ "version": "0.41.4",
4
4
  "description": "filex core — Vue 3 source of truth for the filex file manager (FileExplorer + ConnectionsPanel SFCs, composables, types)",
5
5
  "type": "module",
6
6
  "main": "./dist/filex-core.umd.cjs",
@@ -237,7 +237,7 @@ const emit = defineEmits<{
237
237
  /** The virtual `.trash` row was opened — the trash view carries the restore
238
238
  * actions and belongs to the main pane, so the host decides. */
239
239
  (e: 'open-trash'): void;
240
- (e: 'click-row', node: FileNode, mod: { ctrl: boolean; shift: boolean; touch?: boolean }): void;
240
+ (e: 'click-row', node: FileNode, mod: { ctrl: boolean; shift: boolean }): void;
241
241
  /** Right-click. `null` = empty space (nothing selected → "Paste" only). */
242
242
  (e: 'context', node: FileNode | null, ev: MouseEvent): void;
243
243
  (e: 'clear-selection'): void;
@@ -444,48 +444,36 @@ const paneSubfolders = computed(() =>
444
444
  * `useSelection` instance for this pane answers. One set of Ctrl/Shift
445
445
  * semantics for both panes — the old right pane had a simplified copy in
446
446
  * which Shift behaved as Ctrl. */
447
- /* issue #26, second round a press on the NAME opens, so the second click of
448
- * a habitual double-click lands on whatever the NEW listing put under the
447
+ /* issue #26 any click that is not on the checkbox OPENS, so the second click
448
+ * of a habitual double-click lands on whatever the NEW listing put under the
449
449
  * pointer (the folder already opened on the first click). Without this guard
450
- * that click would open or select a row the user never aimed at, and a file's
451
- * name would open twice (two viewer tabs). */
452
- const NAME_OPEN_GUARD_MS = 500;
453
- let nameOpenedAt = 0;
450
+ * that click would open a row the user never aimed at or tick its box — and
451
+ * a file would open twice (two viewer tabs). */
452
+ const OPEN_GUARD_MS = 500;
453
+ let openedAt = 0;
454
454
 
455
- function withinNameOpenGuard(): boolean {
456
- return Date.now() - nameOpenedAt < NAME_OPEN_GUARD_MS;
455
+ function withinOpenGuard(): boolean {
456
+ return Date.now() - openedAt < OPEN_GUARD_MS;
457
457
  }
458
458
 
459
459
  function onViewClick(n: FileNode, mod: ClickMod) {
460
- if (withinNameOpenGuard()) return;
461
- /* issue #26, second round — the reporter's rule on every device: a press on
462
- * the item's name opens it, mouse or finger, selection or not. Ctrl/shift
463
- * still mean "add to / extend the selection", so a modifier click on a name
464
- * selects as it always did. The checkbox selects on its own path. */
465
- if (mod.name && !mod.ctrl && !mod.shift) {
466
- nameOpenedAt = Date.now();
467
- onRowOpen(n);
460
+ if (withinOpenGuard()) return;
461
+ /* issue #26, fourth round — the reporter's rule, on every device: "only
462
+ * clicking on checkbox selects it, any other click will open". The checkbox
463
+ * is the one click that reaches the selection; a click or tap anywhere else
464
+ * on the item opens it, Ctrl/Shift held or not, selection or not. See
465
+ * composables/useRowTouch. */
466
+ if (mod.check) {
467
+ emit('activate');
468
+ emit('click-row', n, { ctrl: true, shift: mod.shift });
468
469
  return;
469
470
  }
470
- /* issue #26 — a finger has no double-click, so a TAP elsewhere on the item
471
- * is the open gesture too: with nothing selected it opens what it lands on,
472
- * exactly as a double-click would. Once something is selected (a long press
473
- * selects, via its menu) such a tap adds to or removes from the selection,
474
- * so picking several files still works. A mouse click beside the name keeps
475
- * click-to-select; see composables/useRowTouch. */
476
- if (mod.touch && !mod.ctrl && !mod.shift) {
477
- if (props.selected.size === 0) {
478
- onRowOpen(n);
479
- return;
480
- }
481
- mod = { ...mod, ctrl: true };
482
- }
483
- emit('activate');
484
- emit('click-row', n, mod);
471
+ openedAt = Date.now();
472
+ onRowOpen(n);
485
473
  }
486
474
 
487
475
  function onViewDbl(n: FileNode) {
488
- if (withinNameOpenGuard()) return;
476
+ if (withinOpenGuard()) return;
489
477
  onRowOpen(n);
490
478
  }
491
479
 
@@ -13,7 +13,8 @@ import { hasInternalDrag } from '../lib/dragOut';
13
13
  import type { FileNode } from '../types/FileNode';
14
14
  import type { LocaleCode } from '../types/ExplorerConfig';
15
15
  import { useLocale } from '../composables/useLocale';
16
- import { clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
16
+ import { checkMod, clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
17
+ import ItemCheck from './ItemCheck.vue';
17
18
  import { encryptedFolderTile, fileIconTile, isEncryptedFolder } from '../lib/fileIcons';
18
19
  import {
19
20
  createFilePreviews,
@@ -30,7 +31,7 @@ import {
30
31
  } from '../lib/dateGroups'; /* gruplama */
31
32
  import { useSortStore, type ListingOrder } from '../lib/sortOrder'; /* gruplama */
32
33
 
33
- const props = defineProps<{
34
+ const props = withDefaults(defineProps<{
34
35
  files: FileNode[];
35
36
  selected: Set<string>;
36
37
  clipped?: Set<string>;
@@ -74,7 +75,16 @@ const props = defineProps<{
74
75
  * order the tiles are not in. Omitted = an ordinary folder listing.
75
76
  */
76
77
  order?: ListingOrder;
77
- }>();
78
+ /**
79
+ * issue #26 — draw the checkbox on each card, the one click that selects.
80
+ * Default on. Same prop, same meaning as GridView's.
81
+ */
82
+ selectable?: boolean;
83
+ }>(), {
84
+ // ⚠ Not left to `undefined`: Vue casts an absent boolean prop to false, and
85
+ // the listing would lose its checkboxes wherever the host said nothing.
86
+ selectable: true,
87
+ });
78
88
 
79
89
  const emit = defineEmits<{
80
90
  (e: 'click-card', node: FileNode, mod: ClickMod): void;
@@ -158,8 +168,17 @@ function isSelected(n: FileNode): boolean {
158
168
  return props.selected.has(n.path);
159
169
  }
160
170
 
171
+ /**
172
+ * issue #26 — the card's checkbox is the one click that selects (a click
173
+ * anywhere else on the card opens it), routed exactly like the list row's:
174
+ * one `click-card` emit marked `check`, one `useSelection.click`.
175
+ */
176
+ function onCheckClick(n: FileNode, ev: MouseEvent) {
177
+ emit('click-card', n, checkMod(ev));
178
+ }
179
+
161
180
  function onClick(n: FileNode, ev: MouseEvent) {
162
- emit('click-card', n, clickMod(ev, touch.isTap(ev), '.fe-gal__label'));
181
+ emit('click-card', n, clickMod(ev, touch.isTap(ev)));
163
182
  }
164
183
 
165
184
  function onDbl(n: FileNode) {
@@ -207,8 +226,13 @@ function onItemDrop(n: FileNode, ev: DragEvent) {
207
226
  }
208
227
 
209
228
  /* Long press → the card's menu; a tap is reported as a tap (issue #26). */
210
- const touch = useRowTouch<FileNode>((n, at) =>
211
- emit('context-card', n, { ...at, preventDefault: () => {}, stopPropagation: () => {} } as unknown as MouseEvent),
229
+ const touch = useRowTouch<FileNode>(
230
+ (n, at) =>
231
+ emit('context-card', n, { ...at, preventDefault: () => {}, stopPropagation: () => {} } as unknown as MouseEvent),
232
+ {
233
+ // A finger lifted on the item, off its controls, opens at touchend — see useRowTouch.
234
+ onTap: (n) => emit('click-card', n, { ctrl: false, shift: false, touch: true }),
235
+ },
212
236
  );
213
237
 
214
238
  /**
@@ -233,7 +257,7 @@ function metaFor(n: FileNode): string {
233
257
  <template>
234
258
  <div
235
259
  class="fe-gal"
236
- :class="{ 'is-loading': loading }"
260
+ :class="{ 'is-loading': loading, 'has-selection': selectable && selected.size > 0 }"
237
261
  role="listbox"
238
262
  aria-multiselectable="true"
239
263
  :aria-label="t('gallery.aria')"
@@ -268,7 +292,7 @@ function metaFor(n: FileNode): string {
268
292
  @dragleave="onItemDragLeave(n)"
269
293
  @drop="onItemDrop(n, $event)"
270
294
  @touchstart.passive="touch.onTouchStart(n, $event)"
271
- @touchend.passive="touch.onTouchEnd"
295
+ @touchend="touch.onTouchEnd"
272
296
  @touchmove.passive="touch.onTouchMove"
273
297
  >
274
298
  <div class="fe-gal__thumb" :ref="(el) => bindPreview(el, n)">
@@ -346,6 +370,18 @@ function metaFor(n: FileNode): string {
346
370
  class="fe-thumb__play"
347
371
  aria-hidden="true"
348
372
  ></span>
373
+ <!-- issue #26 — the card's checkbox, the one click that selects: the
374
+ corner opposite the star, on the same reveal rule plus "anything
375
+ is selected" (styles/base.css .fe-item-check). -->
376
+ <span
377
+ v-if="selectable"
378
+ class="fe-item-check fe-gal__check"
379
+ data-fe-control
380
+ @click.stop="onCheckClick(n, $event)"
381
+ @dblclick.stop
382
+ >
383
+ <ItemCheck :on="isSelected(n)" :label="nodeDisplayName(n)" />
384
+ </span>
349
385
  <div v-if="canStar(n)" class="fe-gal__star" @click.stop @dblclick.stop>
350
386
  <StarButton
351
387
  :starred="!!starredIds?.has(n.id!)"
@@ -20,7 +20,8 @@ import type { FileNode } from '../types/FileNode';
20
20
  import { hasInternalDrag } from '../lib/dragOut';
21
21
  import type { LocaleCode } from '../types/ExplorerConfig';
22
22
  import { useLocale } from '../composables/useLocale';
23
- import { clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
23
+ import { checkMod, clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
24
+ import ItemCheck from './ItemCheck.vue';
24
25
  import { encryptedFolderTile, fileIconTile, isEncryptedFolder } from '../lib/fileIcons';
25
26
  import {
26
27
  createFilePreviews,
@@ -38,7 +39,7 @@ import StarButton from './StarButton.vue';
38
39
  import { snippetSegments } from '../lib/snippet'; /* bul:s3 */
39
40
  import { applyDragGhost } from '../lib/dragGhost'; /* wiring:c4 */
40
41
 
41
- const props = defineProps<{
42
+ const props = withDefaults(defineProps<{
42
43
  files: FileNode[];
43
44
  selected: Set<string>;
44
45
  clipped?: Set<string>;
@@ -108,7 +109,17 @@ const props = defineProps<{
108
109
  * type tiles. Absent/false = the ordinary case.
109
110
  */
110
111
  e2eActive?: boolean;
111
- }>();
112
+ /**
113
+ * issue #26 — draw the checkbox on each card, the one click that selects.
114
+ * Default on. Home passes false: its cards open on a click and there is no
115
+ * selection there to put anything into.
116
+ */
117
+ selectable?: boolean;
118
+ }>(), {
119
+ // ⚠ Not left to `undefined`: Vue casts an absent boolean prop to false, and
120
+ // the listing would lose its checkboxes wherever the host said nothing.
121
+ selectable: true,
122
+ });
112
123
 
113
124
  const emit = defineEmits<{
114
125
  (e: 'click-card', node: FileNode, mod: ClickMod): void;
@@ -267,8 +278,17 @@ function isSelected(n: FileNode): boolean {
267
278
  return props.selected.has(n.path);
268
279
  }
269
280
 
281
+ /**
282
+ * issue #26 — the card's checkbox is the one click that selects (a click
283
+ * anywhere else on the card opens it), routed exactly like the list row's:
284
+ * one `click-card` emit marked `check`, one `useSelection.click`.
285
+ */
286
+ function onCheckClick(n: FileNode, ev: MouseEvent) {
287
+ emit('click-card', n, checkMod(ev));
288
+ }
289
+
270
290
  function onClick(n: FileNode, ev: MouseEvent) {
271
- emit('click-card', n, clickMod(ev, touch.isTap(ev), '.fe-grid__label'));
291
+ emit('click-card', n, clickMod(ev, touch.isTap(ev)));
272
292
  }
273
293
 
274
294
  function onDbl(n: FileNode) {
@@ -329,8 +349,13 @@ function onItemDrop(n: FileNode, ev: DragEvent) {
329
349
  }
330
350
 
331
351
  /* Long press → the card's menu; a tap is reported as a tap (issue #26). */
332
- const touch = useRowTouch<FileNode>((n, at) =>
333
- emit('context-card', n, { ...at, preventDefault: () => {}, stopPropagation: () => {} } as unknown as MouseEvent),
352
+ const touch = useRowTouch<FileNode>(
353
+ (n, at) =>
354
+ emit('context-card', n, { ...at, preventDefault: () => {}, stopPropagation: () => {} } as unknown as MouseEvent),
355
+ {
356
+ // A finger lifted on the item, off its controls, opens at touchend — see useRowTouch.
357
+ onTap: (n) => emit('click-card', n, { ctrl: false, shift: false, touch: true }),
358
+ },
334
359
  );
335
360
 
336
361
  /**
@@ -382,7 +407,7 @@ function snippetTitle(snippet: string): string {
382
407
  localized label + busy state. Structure/layout untouched. -->
383
408
  <div
384
409
  class="fe-grid"
385
- :class="{ 'is-loading': loading }"
410
+ :class="{ 'is-loading': loading, 'has-selection': selectable && selected.size > 0 }"
386
411
  role="listbox"
387
412
  aria-multiselectable="true"
388
413
  :aria-label="t('grid.aria')"
@@ -418,7 +443,7 @@ function snippetTitle(snippet: string): string {
418
443
  @dragleave="onItemDragLeave(n) /* wiring:c4 */"
419
444
  @drop="onItemDrop(n, $event)"
420
445
  @touchstart.passive="touch.onTouchStart(n, $event)"
421
- @touchend.passive="touch.onTouchEnd"
446
+ @touchend="touch.onTouchEnd"
422
447
  @touchmove.passive="touch.onTouchMove"
423
448
  >
424
449
  <!-- gorunum:v1 — the preview, files only: 184×108. A thumbnail when
@@ -496,7 +521,7 @@ function snippetTitle(snippet: string): string {
496
521
  the person looking for what they starred, so the chip is always
497
522
  painted once the file IS starred and only appears on hover/focus
498
523
  otherwise (see .fe-grid__star in styles/base.css). @click.stop so
499
- starring never doubles as a card selection. -->
524
+ starring never doubles as opening the card. -->
500
525
  <!-- gorunum:v1-preview — a frame lifted out of a video is, on a card,
501
526
  indistinguishable from a photograph. The badge is the difference,
502
527
  and it is drawn only over a real frame: a video that fell back to
@@ -525,16 +550,30 @@ function snippetTitle(snippet: string): string {
525
550
  On a file it is the 56px footer under the preview; on a folder it is
526
551
  the whole 56px card. -->
527
552
  <div class="fe-grid__foot">
528
- <!-- eslint-disable-next-line vue/no-v-htmlstatic markup from lib/fileIcons -->
529
- <span
530
- v-if="isEncryptedFolder(n)"
531
- class="fe-grid__tile fe-grid__tile--svg"
532
- role="img"
533
- :aria-label="t('e2e.badge')"
534
- v-html="encryptedFolderTile()"
535
- ></span>
536
- <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
537
- <span v-else class="fe-grid__tile fe-grid__tile--svg" v-html="fileIconTile(n)"></span>
553
+ <!-- issue #26the checkbox takes the tile's place while it shows
554
+ (hovered, focused, selected, or once anything is selected), so
555
+ the name does not move when it appears. -->
556
+ <span class="fe-grid__lead" :class="{ 'fe-grid__lead--pick': selectable }">
557
+ <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
558
+ <span
559
+ v-if="isEncryptedFolder(n)"
560
+ class="fe-grid__tile fe-grid__tile--svg"
561
+ role="img"
562
+ :aria-label="t('e2e.badge')"
563
+ v-html="encryptedFolderTile()"
564
+ ></span>
565
+ <!-- eslint-disable-next-line vue/no-v-html — static markup from lib/fileIcons -->
566
+ <span v-else class="fe-grid__tile fe-grid__tile--svg" v-html="fileIconTile(n)"></span>
567
+ <span
568
+ v-if="selectable"
569
+ class="fe-item-check"
570
+ data-fe-control
571
+ @click.stop="onCheckClick(n, $event)"
572
+ @dblclick.stop
573
+ >
574
+ <ItemCheck :on="isSelected(n)" :label="nodeDisplayName(n)" />
575
+ </span>
576
+ </span>
538
577
  <div class="fe-grid__main">
539
578
  <div class="fe-grid__label" :title="n.basename">
540
579
  {{ nodeDisplayName(n) }}
@@ -152,10 +152,9 @@ function storageCaption(s: HomeStorage): string {
152
152
  /**
153
153
  * A card opens what it names.
154
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.
155
+ * A single click, exactly as in the listing behind it (issue #26: a click
156
+ * anywhere but the checkbox opens). Home's cards carry no checkbox at all
157
+ * there is no selection here to tick anything into (see NO_SELECTION).
159
158
  *
160
159
  * ⚠ Which is why the double click has to be swallowed: a real double click
161
160
  * fires `click-card` twice, and two opens of a file are two viewer tabs. The
@@ -246,6 +245,7 @@ function openNode(n: FileNode) {
246
245
  v-if="shownRecent.length"
247
246
  :files="shownRecent"
248
247
  :selected="NO_SELECTION"
248
+ :selectable="false"
249
249
  :locale="locale"
250
250
  :thumb-src="thumbSrc"
251
251
  @click-card="openNode"
@@ -292,6 +292,7 @@ function openNode(n: FileNode) {
292
292
  v-if="shownStarred.length"
293
293
  :files="shownStarred"
294
294
  :selected="NO_SELECTION"
295
+ :selectable="false"
295
296
  :locale="locale"
296
297
  :thumb-src="thumbSrc"
297
298
  @click-card="openNode"
@@ -0,0 +1,47 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * ItemCheck — the checkbox on a listing item: the list row's tick column and
4
+ * the grid and gallery cards' corner (issue #26, fourth round: "only clicking
5
+ * on checkbox selects it"). One element for the three views, so the tick a
6
+ * card shows cannot drift from the one a row shows.
7
+ *
8
+ * The view binds the click on the wrapper around it and routes it through
9
+ * `checkMod` — the same `click-row` emit and the same `useSelection.click`
10
+ * every selection goes through, so shift ranges and toggles are one
11
+ * implementation, not two.
12
+ *
13
+ * ⚠ A button with role="checkbox", NOT an <input type="checkbox">. The input
14
+ * owns a `checked` state of its own: the browser flips it before the click
15
+ * handler runs and, if the default is prevented, flips it back AFTER Vue has
16
+ * patched — so the row went selected while the box stayed empty (measured:
17
+ * rowSelected=true, domChecked=false). Drawing the state from the selection
18
+ * leaves nothing to drift. Space and Enter still tick it, because a button
19
+ * fires `click` for both.
20
+ *
21
+ * ⚠ A mouse press does not move focus onto it (`mousedown.prevent`). The tick
22
+ * is now the ONLY click that selects, so "tick, then Space" is the way to
23
+ * quick-look a file — and Space on a focused button is that button's own
24
+ * activation, which the shortcut layer rightly leaves alone: the peek would
25
+ * never open and the tick would flip back off. Tab still reaches it, and Space
26
+ * still ticks it from the keyboard.
27
+ */
28
+ defineProps<{
29
+ /** Whether the item is selected. */
30
+ on: boolean;
31
+ /** The item's display name — the checkbox's accessible name. */
32
+ label: string;
33
+ }>();
34
+ </script>
35
+
36
+ <template>
37
+ <button
38
+ type="button"
39
+ class="fe-list__check"
40
+ :class="{ 'is-on': on }"
41
+ role="checkbox"
42
+ :aria-checked="on ? 'true' : 'false'"
43
+ :aria-label="label"
44
+ :title="label"
45
+ @mousedown.prevent
46
+ ></button>
47
+ </template>
@@ -11,7 +11,8 @@ import { hasInternalDrag } from '../lib/dragOut';
11
11
  import type { FileNode } from '../types/FileNode';
12
12
  import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
13
13
  import { useLocale } from '../composables/useLocale';
14
- import { clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
14
+ import { checkMod, clickMod, useRowTouch, type ClickMod } from '../composables/useRowTouch';
15
+ import ItemCheck from './ItemCheck.vue';
15
16
  import {
16
17
  arrivedFromOutside,
17
18
  ownedByViewer,
@@ -174,7 +175,7 @@ function isSelected(n: FileNode): boolean {
174
175
  }
175
176
 
176
177
  function onRowClick(n: FileNode, ev: MouseEvent) {
177
- emit('click-row', n, clickMod(ev, touch.isTap(ev), '.fe-list__name'));
178
+ emit('click-row', n, clickMod(ev, touch.isTap(ev)));
178
179
  }
179
180
 
180
181
  function onRowDbl(n: FileNode) {
@@ -212,17 +213,14 @@ function onRowMenu(n: FileNode, ev: MouseEvent) {
212
213
  }
213
214
 
214
215
  /**
215
- * gorunum:v1 — the row checkbox drives THE SAME selection the row click
216
- * drives: one `click-row` emit, one `useSelection.click`, one anchor. A
217
- * checkbox that kept its own Set would disagree with the row the first time
218
- * someone mixed the two, and the context menu acts on the row's set.
219
- *
220
- * `ctrl: true` because ticking a box adds to a selection rather than replacing
221
- * it; `shift` is passed through so shift-clicking a box still extends the
222
- * range from the anchor (the composable answers shift before ctrl).
216
+ * issue #26 — the checkbox is the one click that selects (a click anywhere
217
+ * else on the row opens it). It goes out as the same `click-row` emit every
218
+ * other gesture uses, marked `check`, so FilePane routes it to the one
219
+ * `useSelection.click` one anchor, one set, the one the context menu acts
220
+ * on. See composables/useRowTouch `checkMod`.
223
221
  */
224
222
  function onCheckClick(n: FileNode, ev: MouseEvent) {
225
- emit('click-row', n, { ctrl: true, shift: ev.shiftKey });
223
+ emit('click-row', n, checkMod(ev));
226
224
  }
227
225
 
228
226
  function onItemDragStart(n: FileNode, ev: DragEvent) {
@@ -266,8 +264,13 @@ function onItemDrop(n: FileNode, ev: DragEvent) {
266
264
  }
267
265
 
268
266
  /* Long press → the row's menu; a tap is reported as a tap (issue #26). */
269
- const touch = useRowTouch<FileNode>((n, at) =>
270
- emit('context-row', n, { ...at, preventDefault: () => {}, stopPropagation: () => {} } as unknown as MouseEvent),
267
+ const touch = useRowTouch<FileNode>(
268
+ (n, at) =>
269
+ emit('context-row', n, { ...at, preventDefault: () => {}, stopPropagation: () => {} } as unknown as MouseEvent),
270
+ {
271
+ // A finger lifted on the item, off its controls, opens at touchend — see useRowTouch.
272
+ onTap: (n) => emit('click-row', n, { ctrl: false, shift: false, touch: true }),
273
+ },
271
274
  );
272
275
 
273
276
  function keepGlyph(b: 'kept' | 'syncing' | 'cloud' | 'partial'): string {
@@ -1226,31 +1229,21 @@ const segments = computed<DateRun<FileNode>[]>(() =>
1226
1229
  @dragleave="onItemDragLeave(n) /* wiring:c4 */"
1227
1230
  @drop="onItemDrop(n, $event)"
1228
1231
  @touchstart.passive="touch.onTouchStart(n, $event)"
1229
- @touchend.passive="touch.onTouchEnd"
1232
+ @touchend="touch.onTouchEnd"
1230
1233
  @touchmove.passive="touch.onTouchMove"
1231
1234
  >
1232
- <!-- gorunum:v1 — the tick drives the SAME selection the row click
1233
- does (onCheckClick click-row useSelection.click), so shift
1234
- ranges and ctrl toggles are one implementation, not two.
1235
- A button with role="checkbox", NOT an <input type="checkbox">.
1236
- The input owns a `checked` state of its own: the browser flips it
1237
- before the click handler runs and, if the default is prevented,
1238
- flips it back AFTER Vue has patched — so the row went selected
1239
- while the box stayed empty (measured: rowSelected=true,
1240
- domChecked=false). Drawing the state from the selection leaves
1241
- nothing to drift. Space and Enter still tick it, because a button
1242
- fires `click` for both. -->
1243
- <div class="fe-list__col fe-list__col--check" role="gridcell" @click.stop @dblclick.stop>
1244
- <button
1245
- type="button"
1246
- class="fe-list__check"
1247
- :class="{ 'is-on': isSelected(n) }"
1248
- role="checkbox"
1249
- :aria-checked="isSelected(n) ? 'true' : 'false'"
1250
- :aria-label="nodeDisplayName(n)"
1251
- :title="nodeDisplayName(n)"
1252
- @click.stop="onCheckClick(n, $event)"
1253
- ></button>
1235
+ <!-- issue #26 — the tick is the one click that selects; the whole
1236
+ cell is its target, so a click that just misses the box does not
1237
+ open the row instead. `data-fe-control` keeps a finger on the
1238
+ cell from counting as a tap on the row (useRowTouch). -->
1239
+ <div
1240
+ class="fe-list__col fe-list__col--check"
1241
+ role="gridcell"
1242
+ data-fe-control
1243
+ @click.stop="onCheckClick(n, $event)"
1244
+ @dblclick.stop
1245
+ >
1246
+ <ItemCheck :on="isSelected(n)" :label="nodeDisplayName(n)" />
1254
1247
  </div>
1255
1248
  <div class="fe-list__col fe-list__col--name" role="gridcell" :style="colStyle('name')">
1256
1249
  <!-- ikon:emoji — an encrypted folder is still a FOLDER: it keeps the
@@ -1323,25 +1316,35 @@ const segments = computed<DateRun<FileNode>[]>(() =>
1323
1316
  class="fe-list__col fe-list__col--star"
1324
1317
  role="gridcell"
1325
1318
  :style="colStyle(id)"
1326
- @click.stop
1327
1319
  >
1328
- <StarButton
1320
+ <!-- issue #26 — only the star itself is a control; the rest of the
1321
+ cell (all of it on a folder, which has no star) opens the row
1322
+ like any other empty space on it. `display: contents`, so the
1323
+ wrapper stops the click without moving the star. -->
1324
+ <span
1329
1325
  v-if="starEnabled !== false && typeof n.id === 'number' && n.type === 'file'"
1330
- :starred="!!starredIds?.has(n.id)"
1331
- :node-id="n.id"
1332
- :api-base="apiBase"
1333
- :auth-headers="authHeaders"
1334
- :auth-credentials="authCredentials"
1335
- :locale="locale"
1336
- compact
1337
- @change="(val: boolean) => emit('star-change', n, val)"
1338
- />
1326
+ class="fe-list__star-hit"
1327
+ data-fe-control
1328
+ @click.stop
1329
+ @dblclick.stop
1330
+ >
1331
+ <StarButton
1332
+ :starred="!!starredIds?.has(n.id)"
1333
+ :node-id="n.id"
1334
+ :api-base="apiBase"
1335
+ :auth-headers="authHeaders"
1336
+ :auth-credentials="authCredentials"
1337
+ :locale="locale"
1338
+ compact
1339
+ @change="(val: boolean) => emit('star-change', n, val)"
1340
+ />
1341
+ </span>
1339
1342
  </div>
1340
1343
  <div v-else :class="colClass(id)" role="gridcell" :style="colStyle(id)" :title="cellTitle(id, n)">
1341
1344
  {{ cellText(id, n) }}
1342
1345
  </div>
1343
1346
  </template>
1344
- <div class="fe-list__col fe-list__col--menu" role="gridcell" @click.stop @dblclick.stop>
1347
+ <div class="fe-list__col fe-list__col--menu" role="gridcell" data-fe-control @click.stop @dblclick.stop>
1345
1348
  <button
1346
1349
  type="button"
1347
1350
  class="fe-list__menu"