@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
@@ -1,34 +1,45 @@
1
1
  /**
2
- * useRowTouch — the one touch grammar every file view speaks (list, grid,
3
- * gallery).
2
+ * useRowTouch — the one click and touch grammar every file view speaks (list,
3
+ * grid, gallery).
4
4
  *
5
- * A mouse selects with a click and opens with a double-click. A finger has no
6
- * double-click: the browser swallows the second tap into a zoom or never sends
7
- * `dblclick` at all, so on a phone the desktop grammar left nothing openable
8
- * (issue #26). What a finger does instead is what every mobile file manager
9
- * does:
5
+ * The rule (issue #26, fourth round the reporter's words: "only clicking on
6
+ * checkbox selects it, any other click will open"), the same on every device:
10
7
  *
11
- * - a long press opens the item's menu (and, through it, selects the item);
12
- * - a tap is reported AS a tap, so the host can open instead of select.
8
+ * - the item's CHECKBOX selects: a tick adds or removes, shift+tick extends
9
+ * the range from the anchor;
10
+ * - a click or tap ANYWHERE ELSE on the item opens it — the name, the icon,
11
+ * the size column, the empty space beside the name, with or without a
12
+ * selection, with or without Ctrl/Shift held;
13
+ * - a right click (mouse) or a long press (finger) opens the item's menu;
14
+ * - the item's own controls — the star, the ⋮ button — do their own job.
13
15
  *
14
- * The views only report. What a tap means is decided once, in FilePane.
16
+ * The views only report. What a click means is decided once, in FilePane.
15
17
  *
16
- * Issue #26, second round (the reporter's rule, on EVERY device):
17
- * - a press on the item's NAME opens it mouse click or finger tap, with or
18
- * without a selection;
19
- * - a long press (finger) or a right click (mouse) opens the menu;
20
- * - the checkbox selects, exactly as before.
21
- * So the views also report whether the click landed on the name (`name`).
18
+ * How the rule got here:
19
+ * - First round: a finger has no double-click, so on a phone nothing could
20
+ * be opened at all. A tap was made to open.
21
+ * - Second round: a press on the NAME opened on every device, a press
22
+ * beside it still selected. The reporter: "need to click precisely on
23
+ * name, if a lil bit on the right then it selects file".
24
+ * - Third round: on a phone the name still needed two taps. A tap reaches a
25
+ * page as a click only at the end of the browser's compatibility sequence
26
+ * (touch → mouse move → hover → mouse down/up → click), and iOS WebKit
27
+ * stops that sequence when the hover step reveals content. So a finger
28
+ * lifted on the item opens it right there, at `touchend`, and cancels the
29
+ * emulated mouse events that would follow (`onTap`); the hover reveals are
30
+ * kept off screens that cannot hover (styles/base.css,
31
+ * `@media (hover: hover)`).
32
+ * - Fourth round: the checkbox became the only way a click selects, and the
33
+ * grid and gallery cards got one too (they had none).
22
34
  *
23
35
  * ⚠ A tap is judged from the gesture that produced the click, never from the
24
36
  * screen: `pointerType` where the browser sets it on click (Chromium, Firefox),
25
37
  * and the touchend that just preceded the click where it does not (older
26
- * WebKit). A touch laptop's trackpad therefore keeps the desktop grammar while
27
- * its screen gets the phone one. A `(pointer: coarse)` media query cannot tell
28
- * those two apart.
38
+ * WebKit). A touch laptop's trackpad and its screen are two different gestures
39
+ * a `(pointer: coarse)` media query cannot tell apart.
29
40
  *
30
41
  * ⚠ It used to live as three identical copies of the long-press timer, one per
31
- * view; the tap rule is exactly the kind of addition a copy misses.
42
+ * view; a rule like this one is exactly the kind of addition a copy misses.
32
43
  */
33
44
  import { onBeforeUnmount } from 'vue';
34
45
 
@@ -39,6 +50,15 @@ const TAP_WINDOW_MS = 800;
39
50
  /** A finger that travels further than this is scrolling, not pressing. */
40
51
  const MOVE_TOLERANCE_PX = 10;
41
52
 
53
+ /**
54
+ * What on an item is a control of its own: a press there is the control's,
55
+ * not the item's. The checkbox is one (it selects on its own click), and so
56
+ * are the star and the ⋮ button. A wrapper cell that swallows its clicks
57
+ * carries `data-fe-control` so a finger on its padding is not an open either.
58
+ */
59
+ export const ITEM_CONTROL_SELECTOR =
60
+ 'button, a, input, select, textarea, label, [role="checkbox"], [data-fe-control]';
61
+
42
62
  export interface TouchPoint {
43
63
  clientX: number;
44
64
  clientY: number;
@@ -50,30 +70,47 @@ export interface ClickMod {
50
70
  shift: boolean;
51
71
  /** The click was a finger's tap. */
52
72
  touch?: boolean;
53
- /** The click landed on the item's name. */
54
- name?: boolean;
73
+ /** The click was on the item's checkbox — the one click that selects. */
74
+ check?: boolean;
75
+ }
76
+
77
+ /** The one place a view turns a click on an item into a `ClickMod`. */
78
+ export function clickMod(ev: MouseEvent, touch: boolean): ClickMod {
79
+ return { ctrl: ev.ctrlKey || ev.metaKey, shift: ev.shiftKey, touch };
55
80
  }
56
81
 
57
82
  /**
58
- * The one place a view turns a click into a `ClickMod`. `nameSelector` is the
59
- * view's own name element (`.fe-list__name`, `.fe-grid__label`, …).
83
+ * The one place a view turns a click on an item's CHECKBOX into a `ClickMod`.
84
+ * `ctrl` because a tick adds to a selection rather than replacing it; `shift`
85
+ * passes through so a shift-tick still extends the range from the anchor.
60
86
  */
61
- export function clickMod(ev: MouseEvent, touch: boolean, nameSelector: string): ClickMod {
62
- const target = ev.target as Element | null;
63
- return {
64
- ctrl: ev.ctrlKey || ev.metaKey,
65
- shift: ev.shiftKey,
66
- touch,
67
- name: typeof target?.closest === 'function' && target.closest(nameSelector) !== null,
68
- };
87
+ export function checkMod(ev: MouseEvent): ClickMod {
88
+ return { ctrl: true, shift: ev.shiftKey, check: true };
69
89
  }
70
90
 
71
- export function useRowTouch<T>(onLongPress: (item: T, at: TouchPoint) => void) {
91
+ export interface RowTouchOptions<T> {
92
+ /** A tap on the item outside its controls: open it, before any click. */
93
+ onTap?: (item: T) => void;
94
+ }
95
+
96
+ /** Whether the press started on one of the item's own controls. */
97
+ function onControl(ev: Event): boolean {
98
+ const el = ev.target as Element | null;
99
+ if (typeof el?.closest !== 'function') return false;
100
+ const control = el.closest(ITEM_CONTROL_SELECTOR);
101
+ const item = ev.currentTarget as Element | null;
102
+ // Only a control INSIDE the item counts: the listing itself may sit in a
103
+ // label or a link on some host page, and that must not disarm every tap.
104
+ return control !== null && (item === null || typeof item.contains !== 'function' || item.contains(control));
105
+ }
106
+
107
+ export function useRowTouch<T>(onLongPress: (item: T, at: TouchPoint) => void, options: RowTouchOptions<T> = {}) {
72
108
  let timer: ReturnType<typeof setTimeout> | undefined;
73
109
  let target: T | null = null;
74
110
  let origin: TouchPoint = { clientX: 0, clientY: 0 };
75
111
  let longPressed = false;
76
112
  let tapEndedAt = 0;
113
+ let startedOnItem = false;
77
114
 
78
115
  function stopTimer() {
79
116
  if (timer) clearTimeout(timer);
@@ -87,6 +124,7 @@ export function useRowTouch<T>(onLongPress: (item: T, at: TouchPoint) => void) {
87
124
  target = item;
88
125
  longPressed = false;
89
126
  origin = { clientX: t0.clientX, clientY: t0.clientY };
127
+ startedOnItem = !onControl(ev);
90
128
  timer = setTimeout(() => {
91
129
  timer = undefined;
92
130
  if (target === null) return;
@@ -107,10 +145,22 @@ export function useRowTouch<T>(onLongPress: (item: T, at: TouchPoint) => void) {
107
145
  }
108
146
  }
109
147
 
110
- function onTouchEnd() {
148
+ /**
149
+ * ⚠ Bind it WITHOUT `.passive`: a passive listener cannot cancel the
150
+ * emulated mouse events, and the click they end in would then reach the new
151
+ * listing under the finger (FilePane's open guard catches that too).
152
+ */
153
+ function onTouchEnd(ev?: TouchEvent) {
111
154
  stopTimer();
112
- if (target !== null && !longPressed) tapEndedAt = Date.now();
155
+ const item = target;
156
+ const tapped = item !== null && !longPressed;
157
+ if (tapped) tapEndedAt = Date.now();
113
158
  target = null;
159
+ if (tapped && startedOnItem && options.onTap) {
160
+ if (ev?.cancelable) ev.preventDefault();
161
+ options.onTap(item as T);
162
+ }
163
+ startedOnItem = false;
114
164
  }
115
165
 
116
166
  /** Whether this click is a finger's tap. */
@@ -466,11 +466,22 @@ filex-explorer {
466
466
  opacity: 0;
467
467
  transition: opacity 0.12s;
468
468
  }
469
- .fe-list__row:hover .fe-list__col--star,
469
+ /* issue #26, third round — a reveal ON HOVER only where hovering exists.
470
+ iOS WebKit treats a tap whose :hover styles reveal content (an opacity
471
+ transition from 0 counts) as "the user wanted to see the hover state": it
472
+ applies the hover and swallows the click, so the first tap on a row or card
473
+ only highlighted it and a second tap was needed to open. `(hover: hover)`
474
+ keeps these reveals off touch screens, where they could never be hovered on
475
+ purpose anyway. `:focus-within` and a starred item stay visible everywhere. */
470
476
  .fe-list__row:focus-within .fe-list__col--star,
471
477
  .fe-list__col--star:has(.is-starred) {
472
478
  opacity: 1;
473
479
  }
480
+ @media (hover: hover) {
481
+ .fe-list__row:hover .fe-list__col--star {
482
+ opacity: 1;
483
+ }
484
+ }
474
485
  .fe-list__col--star .filex-star-btn.is-starred {
475
486
  color: var(--fe-warning, #f59e0b);
476
487
  }
@@ -3683,11 +3694,25 @@ filex-explorer {
3683
3694
  transition: opacity 0.12s ease, transform 0.12s ease;
3684
3695
  pointer-events: none;
3685
3696
  }
3686
- .fe-gal__card:hover .fe-gal__meta,
3687
3697
  .fe-gal__card:focus-visible .fe-gal__meta {
3688
3698
  opacity: 1;
3689
3699
  transform: translateY(0);
3690
3700
  }
3701
+ /* issue #26 — see the note on .fe-list__col--star: no hover reveal on touch.
3702
+ A screen that cannot hover shows the caption outright instead, since it
3703
+ would otherwise never be seen there. */
3704
+ @media (hover: hover) {
3705
+ .fe-gal__card:hover .fe-gal__meta {
3706
+ opacity: 1;
3707
+ transform: translateY(0);
3708
+ }
3709
+ }
3710
+ @media (hover: none) {
3711
+ .fe-gal__meta {
3712
+ opacity: 1;
3713
+ transform: none;
3714
+ }
3715
+ }
3691
3716
  .fe-gal__meta-line {
3692
3717
  overflow: hidden;
3693
3718
  text-overflow: ellipsis;
@@ -4560,9 +4585,7 @@ filex-explorer {
4560
4585
  opacity: 0;
4561
4586
  transition: opacity 120ms ease;
4562
4587
  }
4563
- .fe-grid__card:hover .fe-grid__star,
4564
4588
  .fe-grid__card:focus-within .fe-grid__star,
4565
- .fe-gal__card:hover .fe-gal__star,
4566
4589
  .fe-gal__card:focus-within .fe-gal__star,
4567
4590
  .fe-grid__star:focus-within,
4568
4591
  .fe-gal__star:focus-within,
@@ -4570,12 +4593,88 @@ filex-explorer {
4570
4593
  .fe-gal__star:has(.is-starred) {
4571
4594
  opacity: 1;
4572
4595
  }
4596
+ /* issue #26 — see the note on .fe-list__col--star: no hover reveal on touch. */
4597
+ @media (hover: hover) {
4598
+ .fe-grid__card:hover .fe-grid__star,
4599
+ .fe-gal__card:hover .fe-gal__star {
4600
+ opacity: 1;
4601
+ }
4602
+ }
4573
4603
  /* `:has()` is the modern half of the rule above; this is the same thing for
4574
4604
  engines without it — a starred chip paints itself. Both are needed: the
4575
4605
  parent wrapper carries the opacity, the child cannot raise it on its own. */
4576
4606
  .filex-star-btn.is-card.is-starred {
4577
4607
  opacity: 1;
4578
4608
  }
4609
+ /* === issue #26 — the checkbox on a grid / gallery card ================
4610
+ The one click on a card that selects; a click anywhere else opens it. The
4611
+ box itself is ItemCheck (.fe-list__check, the list's own tick). It shows
4612
+ when the card is hovered (where hovering exists — see the star note), when
4613
+ something inside the card is focused, when the card is selected, and on
4614
+ every card once anything is selected — which is how a finger, with no
4615
+ hover, finds the next box to tick after a long press selected the first.
4616
+ ⚠ Hidden means `pointer-events: none` as well: an invisible box would take
4617
+ the tap meant to open the card under it. */
4618
+ .fe-item-check {
4619
+ display: flex;
4620
+ align-items: center;
4621
+ justify-content: center;
4622
+ opacity: 0;
4623
+ pointer-events: none;
4624
+ transition: opacity 120ms ease;
4625
+ }
4626
+ .fe-grid__card.is-selected .fe-item-check,
4627
+ .fe-gal__card.is-selected .fe-item-check,
4628
+ .fe-grid.has-selection .fe-item-check,
4629
+ .fe-gal.has-selection .fe-item-check,
4630
+ .fe-grid__card:focus-within .fe-item-check,
4631
+ .fe-gal__card:focus-within .fe-item-check {
4632
+ opacity: 1;
4633
+ pointer-events: auto;
4634
+ }
4635
+ @media (hover: hover) {
4636
+ .fe-grid__card:hover .fe-item-check,
4637
+ .fe-gal__card:hover .fe-item-check {
4638
+ opacity: 1;
4639
+ pointer-events: auto;
4640
+ }
4641
+ }
4642
+ /* On a grid card the box takes the type tile's 24px slot in the card's row,
4643
+ so the name does not move when it appears; the tile steps aside. */
4644
+ .fe-grid__lead {
4645
+ position: relative;
4646
+ flex: 0 0 24px;
4647
+ width: 24px;
4648
+ height: 24px;
4649
+ display: inline-flex;
4650
+ align-items: center;
4651
+ justify-content: center;
4652
+ }
4653
+ .fe-grid__lead > .fe-item-check {
4654
+ position: absolute;
4655
+ inset: 0;
4656
+ }
4657
+ .fe-grid__lead > .fe-grid__tile {
4658
+ transition: opacity 120ms ease;
4659
+ }
4660
+ .fe-grid__card.is-selected .fe-grid__lead--pick > .fe-grid__tile,
4661
+ .fe-grid.has-selection .fe-grid__lead--pick > .fe-grid__tile,
4662
+ .fe-grid__card:focus-within .fe-grid__lead--pick > .fe-grid__tile {
4663
+ opacity: 0;
4664
+ }
4665
+ @media (hover: hover) {
4666
+ .fe-grid__card:hover .fe-grid__lead--pick > .fe-grid__tile {
4667
+ opacity: 0;
4668
+ }
4669
+ }
4670
+ /* On a gallery card it is the thumbnail's top-left corner, opposite the star. */
4671
+ .fe-gal__check {
4672
+ position: absolute;
4673
+ top: 6px;
4674
+ left: 6px;
4675
+ z-index: 2;
4676
+ }
4677
+
4579
4678
  /* The chip itself: a round translucent puck, legible over a photo thumbnail
4580
4679
  as well as over a flat icon tile. */
4581
4680
  .filex-star-btn.is-card {
@@ -5932,8 +6031,9 @@ filex-explorer {
5932
6031
  border-bottom: 1px solid var(--fe-border-soft);
5933
6032
  }
5934
6033
 
5935
- /* The tick. It selects the same thing the row does see ListView.onCheckClick
5936
- so it gets the plain control look, not an accent of its own. */
6034
+ /* The tick the one click on an item that selects (issue #26; ListView
6035
+ onCheckClick, components/ItemCheck). The plain control look, not an accent
6036
+ of its own. */
5937
6037
  .fe-list__col--check {
5938
6038
  display: flex;
5939
6039
  align-items: center;
@@ -10285,6 +10385,11 @@ filex-explorer {
10285
10385
  .fe-list__col--star {
10286
10386
  justify-content: center;
10287
10387
  }
10388
+ /* issue #26 — the star's click guard (ListView): stops the click without a
10389
+ box of its own, so the star sits where it always did. */
10390
+ .fe-list__star-hit {
10391
+ display: contents;
10392
+ }
10288
10393
  /* Location was never in the muted-value rule above it — it is a value like
10289
10394
  Type and Owner and reads like one. */
10290
10395
  .fe-list__col--location {