@marianmeres/stuic 3.103.0 → 3.105.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.
|
@@ -418,11 +418,17 @@
|
|
|
418
418
|
let swipeStartTime = 0;
|
|
419
419
|
let isSwiping = false;
|
|
420
420
|
|
|
421
|
-
// ----
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
421
|
+
// ---- Tap vs drag discrimination for page/area activation ----
|
|
422
|
+
// Activation uses pointer events (pointerdown/pointerup) rather than a synthesized
|
|
423
|
+
// mouse `click`. Pointer events unify mouse + touch and fire reliably on a clean tap,
|
|
424
|
+
// even on the `touch-action: none` gesture surface where tap→click synthesis is
|
|
425
|
+
// unreliable (notably iOS Safari, worst of all on inner SVG shapes). A tap is a
|
|
426
|
+
// pointerup landing within TAP_SLOP_PX of its pointerdown; movement beyond that is a
|
|
427
|
+
// swipe/drag and does not activate.
|
|
428
|
+
|
|
429
|
+
const TAP_SLOP_PX = 10;
|
|
430
|
+
let _tapDownX = 0;
|
|
431
|
+
let _tapDownY = 0;
|
|
426
432
|
|
|
427
433
|
// ---- Zoom helpers ----
|
|
428
434
|
|
|
@@ -572,10 +578,6 @@
|
|
|
572
578
|
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
|
573
579
|
const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
|
|
574
580
|
|
|
575
|
-
_wasDragged = false;
|
|
576
|
-
_dragStartClientX = clientX;
|
|
577
|
-
_dragStartClientY = clientY;
|
|
578
|
-
|
|
579
581
|
if (zoomLevel > 1) {
|
|
580
582
|
// Pan mode
|
|
581
583
|
e.preventDefault();
|
|
@@ -603,18 +605,6 @@
|
|
|
603
605
|
return;
|
|
604
606
|
}
|
|
605
607
|
|
|
606
|
-
// Track drag for page click detection
|
|
607
|
-
if (!_wasDragged) {
|
|
608
|
-
const cx = "touches" in e ? (e.touches[0]?.clientX ?? 0) : e.clientX;
|
|
609
|
-
const cy = "touches" in e ? (e.touches[0]?.clientY ?? 0) : e.clientY;
|
|
610
|
-
if (
|
|
611
|
-
Math.abs(cx - _dragStartClientX) > 5 ||
|
|
612
|
-
Math.abs(cy - _dragStartClientY) > 5
|
|
613
|
-
) {
|
|
614
|
-
_wasDragged = true;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
608
|
// Pan
|
|
619
609
|
if (isPanning) {
|
|
620
610
|
e.preventDefault();
|
|
@@ -693,10 +683,21 @@
|
|
|
693
683
|
};
|
|
694
684
|
}
|
|
695
685
|
|
|
686
|
+
// ---- Tap (pointer) helpers for page/area activation ----
|
|
687
|
+
|
|
688
|
+
function handleTapDown(e: PointerEvent) {
|
|
689
|
+
_tapDownX = e.clientX;
|
|
690
|
+
_tapDownY = e.clientY;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function isTap(e: PointerEvent): boolean {
|
|
694
|
+
return Math.hypot(e.clientX - _tapDownX, e.clientY - _tapDownY) <= TAP_SLOP_PX;
|
|
695
|
+
}
|
|
696
|
+
|
|
696
697
|
// ---- Page click ----
|
|
697
698
|
|
|
698
|
-
function handlePageClick(e:
|
|
699
|
-
if (!onPageClick || !page ||
|
|
699
|
+
function handlePageClick(e: PointerEvent, page: BookPage | undefined) {
|
|
700
|
+
if (!onPageClick || !page || !isTap(e)) return;
|
|
700
701
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
701
702
|
const x = (e.clientX - rect.left) / rect.width;
|
|
702
703
|
const y = (e.clientY - rect.top) / rect.height;
|
|
@@ -769,7 +770,8 @@
|
|
|
769
770
|
<div
|
|
770
771
|
class={twMerge(!unstyled && "stuic-book-sheet-front", classPage)}
|
|
771
772
|
data-placeholder={!sheet.frontPage && sheet.backPage ? "" : undefined}
|
|
772
|
-
|
|
773
|
+
onpointerdown={handleTapDown}
|
|
774
|
+
onpointerup={(e) => handlePageClick(e, sheet.frontPage)}
|
|
773
775
|
>
|
|
774
776
|
{#if isNearby && sheet.frontPage}
|
|
775
777
|
{#if renderPage}
|
|
@@ -840,8 +842,9 @@
|
|
|
840
842
|
width={area.w}
|
|
841
843
|
height={area.h}
|
|
842
844
|
class={!unstyled ? "stuic-book-area" : undefined}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
+
onpointerdown={handleTapDown}
|
|
846
|
+
onpointerup={(e: PointerEvent) => {
|
|
847
|
+
if (!isTap(e)) return;
|
|
845
848
|
e.stopPropagation();
|
|
846
849
|
onAreaClick({ area, page: sheet.frontPage! });
|
|
847
850
|
}}
|
|
@@ -858,7 +861,8 @@
|
|
|
858
861
|
<div
|
|
859
862
|
class={twMerge(!unstyled && "stuic-book-sheet-back", classPage)}
|
|
860
863
|
data-placeholder={!sheet.backPage && sheet.frontPage ? "" : undefined}
|
|
861
|
-
|
|
864
|
+
onpointerdown={handleTapDown}
|
|
865
|
+
onpointerup={(e) => handlePageClick(e, sheet.backPage)}
|
|
862
866
|
>
|
|
863
867
|
{#if isNearby && sheet.backPage}
|
|
864
868
|
{#if renderPage}
|
|
@@ -925,8 +929,9 @@
|
|
|
925
929
|
width={area.w}
|
|
926
930
|
height={area.h}
|
|
927
931
|
class={!unstyled ? "stuic-book-area" : undefined}
|
|
928
|
-
|
|
929
|
-
|
|
932
|
+
onpointerdown={handleTapDown}
|
|
933
|
+
onpointerup={(e: PointerEvent) => {
|
|
934
|
+
if (!isTap(e)) return;
|
|
930
935
|
e.stopPropagation();
|
|
931
936
|
onAreaClick({ area, page: sheet.backPage! });
|
|
932
937
|
}}
|
|
@@ -32,6 +32,14 @@
|
|
|
32
32
|
unstyled?: boolean;
|
|
33
33
|
/** Render as rounded-full */
|
|
34
34
|
roundedFull?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Trim the icon-side horizontal padding down to the vertical padding, so a
|
|
37
|
+
* leading/trailing icon sits the same distance from the edge as a rounded icon
|
|
38
|
+
* (aspect1) button. Pair with `roundedFull` for the "rounded icon button with
|
|
39
|
+
* label" pill look (e.g. a "Back" button: prev arrow + label). Size-aware and
|
|
40
|
+
* RTL-aware (`leading` = start side, `trailing` = end side).
|
|
41
|
+
*/
|
|
42
|
+
iconEdge?: "leading" | "trailing";
|
|
35
43
|
/** Render as aspect ratio 1 */
|
|
36
44
|
aspect1?: boolean;
|
|
37
45
|
/** Icon-only button (implies aspect1, adds data-icon-button for global CSS targeting) */
|
|
@@ -104,6 +112,7 @@
|
|
|
104
112
|
raised = false,
|
|
105
113
|
unstyled = false,
|
|
106
114
|
roundedFull = false,
|
|
115
|
+
iconEdge,
|
|
107
116
|
aspect1 = false,
|
|
108
117
|
iconButton = false,
|
|
109
118
|
tooltip: _tooltip,
|
|
@@ -168,6 +177,7 @@
|
|
|
168
177
|
data-raised={!unstyled && raised ? "true" : undefined}
|
|
169
178
|
data-checked={roleSwitch && checked ? "true" : undefined}
|
|
170
179
|
data-rounded-full={!unstyled && roundedFull ? "true" : undefined}
|
|
180
|
+
data-icon-edge={!unstyled && iconEdge ? iconEdge : undefined}
|
|
171
181
|
data-aspect1={!unstyled && _isAspect1 ? "true" : undefined}
|
|
172
182
|
data-icon-button={!unstyled && _isIconButton ? "true" : undefined}
|
|
173
183
|
data-x={!unstyled && !!_xProps ? "true" : undefined}
|
|
@@ -213,6 +223,7 @@
|
|
|
213
223
|
data-raised={!unstyled && raised ? "true" : undefined}
|
|
214
224
|
data-checked={roleSwitch && checked ? "true" : undefined}
|
|
215
225
|
data-rounded-full={!unstyled && roundedFull ? "true" : undefined}
|
|
226
|
+
data-icon-edge={!unstyled && iconEdge ? iconEdge : undefined}
|
|
216
227
|
data-aspect1={!unstyled && _isAspect1 ? "true" : undefined}
|
|
217
228
|
data-icon-button={!unstyled && _isIconButton ? "true" : undefined}
|
|
218
229
|
data-x={!unstyled && !!_xProps ? "true" : undefined}
|
|
@@ -27,6 +27,14 @@ export interface Props extends Omit<HTMLButtonAttributes, "children"> {
|
|
|
27
27
|
unstyled?: boolean;
|
|
28
28
|
/** Render as rounded-full */
|
|
29
29
|
roundedFull?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Trim the icon-side horizontal padding down to the vertical padding, so a
|
|
32
|
+
* leading/trailing icon sits the same distance from the edge as a rounded icon
|
|
33
|
+
* (aspect1) button. Pair with `roundedFull` for the "rounded icon button with
|
|
34
|
+
* label" pill look (e.g. a "Back" button: prev arrow + label). Size-aware and
|
|
35
|
+
* RTL-aware (`leading` = start side, `trailing` = end side).
|
|
36
|
+
*/
|
|
37
|
+
iconEdge?: "leading" | "trailing";
|
|
30
38
|
/** Render as aspect ratio 1 */
|
|
31
39
|
aspect1?: boolean;
|
|
32
40
|
/** Icon-only button (implies aspect1, adds data-icon-button for global CSS targeting) */
|
|
@@ -20,6 +20,7 @@ A flexible button component with semantic intents, visual variants, sizes, and o
|
|
|
20
20
|
| `iconSwap` | `[string \| Snippet, string \| Snippet]` | - | Two icon states with swap animation (implies iconButton) |
|
|
21
21
|
| `x` | `boolean \| XProps` | - | Normalized "X" icon button shortcut (close/dismiss) |
|
|
22
22
|
| `nav` | `"prev" \| "next" \| ButtonNavProps` | - | Normalized prev/next icon button shortcut (arrow by default; `x` wins on conflict) |
|
|
23
|
+
| `iconEdge` | `"leading" \| "trailing"` | - | Trim icon-side padding to the y-padding (pill + edge-flush icon; pair with `roundedFull`) |
|
|
23
24
|
| `class` | `string` | - | Additional CSS classes |
|
|
24
25
|
|
|
25
26
|
## Snippet Props
|
|
@@ -134,6 +135,26 @@ Global CSS targeting for all icon buttons:
|
|
|
134
135
|
}
|
|
135
136
|
```
|
|
136
137
|
|
|
138
|
+
### Pill with edge-flush icon (`iconEdge`)
|
|
139
|
+
|
|
140
|
+
The "rounded icon button with label" look: a pill-shaped button whose leading (or
|
|
141
|
+
trailing) icon sits the same distance from the edge as a rounded icon (nav) button.
|
|
142
|
+
`iconEdge` trims the icon-side horizontal padding down to the vertical padding — it's
|
|
143
|
+
size-aware and uses logical properties, so `leading`/`trailing` follow text direction
|
|
144
|
+
(RTL-safe). It does NOT round the button on its own; pair it with `roundedFull`.
|
|
145
|
+
|
|
146
|
+
```svelte
|
|
147
|
+
<Button roundedFull iconEdge="leading">{@html iconArrowLeft({ size: 24 })} Back</Button>
|
|
148
|
+
<Button roundedFull iconEdge="trailing">Next {@html iconArrowRight({ size: 24 })}</Button>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Notes:
|
|
152
|
+
|
|
153
|
+
- `iconEdge` is composable — it only trims padding, so it also works on a
|
|
154
|
+
default-radius (non-pill) button if you want the icon flush without the pill shape.
|
|
155
|
+
- You compose the icon + label yourself in `children` (no default icon, unlike `nav`),
|
|
156
|
+
so it works with any icon. Use `size: 24` to match the nav button's arrow.
|
|
157
|
+
|
|
137
158
|
### Custom Styling
|
|
138
159
|
|
|
139
160
|
```svelte
|
|
@@ -199,6 +220,7 @@ The component uses data attributes for styling:
|
|
|
199
220
|
- `data-raised` - Present when raised
|
|
200
221
|
- `data-checked` - Present when roleSwitch is enabled and checked
|
|
201
222
|
- `data-rounded-full` - Present when roundedFull
|
|
223
|
+
- `data-icon-edge` - Set to `"leading"` or `"trailing"` when iconEdge is set
|
|
202
224
|
- `data-aspect1` - Present when aspect1 (or iconButton, or x, or nav)
|
|
203
225
|
- `data-icon-button` - Present when iconButton (or x, or nav)
|
|
204
226
|
- `data-x` - Present when x is set
|
|
@@ -422,6 +422,38 @@
|
|
|
422
422
|
padding: var(--stuic-button-padding-y-xl);
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
/* ============================================================================
|
|
426
|
+
ICON EDGE
|
|
427
|
+
Trim the icon-side horizontal padding down to the vertical padding, so a
|
|
428
|
+
leading/trailing icon sits the same distance from the edge as a rounded icon
|
|
429
|
+
(aspect1) button. Pair with [data-rounded-full] for the "rounded icon button
|
|
430
|
+
with label" pill look. Logical props => leading/trailing follow text direction.
|
|
431
|
+
============================================================================ */
|
|
432
|
+
.stuic-button[data-icon-edge="leading"][data-size="sm"] {
|
|
433
|
+
padding-inline-start: var(--stuic-button-padding-y-sm);
|
|
434
|
+
}
|
|
435
|
+
.stuic-button[data-icon-edge="leading"][data-size="md"] {
|
|
436
|
+
padding-inline-start: var(--stuic-button-padding-y-md);
|
|
437
|
+
}
|
|
438
|
+
.stuic-button[data-icon-edge="leading"][data-size="lg"] {
|
|
439
|
+
padding-inline-start: var(--stuic-button-padding-y-lg);
|
|
440
|
+
}
|
|
441
|
+
.stuic-button[data-icon-edge="leading"][data-size="xl"] {
|
|
442
|
+
padding-inline-start: var(--stuic-button-padding-y-xl);
|
|
443
|
+
}
|
|
444
|
+
.stuic-button[data-icon-edge="trailing"][data-size="sm"] {
|
|
445
|
+
padding-inline-end: var(--stuic-button-padding-y-sm);
|
|
446
|
+
}
|
|
447
|
+
.stuic-button[data-icon-edge="trailing"][data-size="md"] {
|
|
448
|
+
padding-inline-end: var(--stuic-button-padding-y-md);
|
|
449
|
+
}
|
|
450
|
+
.stuic-button[data-icon-edge="trailing"][data-size="lg"] {
|
|
451
|
+
padding-inline-end: var(--stuic-button-padding-y-lg);
|
|
452
|
+
}
|
|
453
|
+
.stuic-button[data-icon-edge="trailing"][data-size="xl"] {
|
|
454
|
+
padding-inline-end: var(--stuic-button-padding-y-xl);
|
|
455
|
+
}
|
|
456
|
+
|
|
425
457
|
/* ============================================================================
|
|
426
458
|
ICON BUTTON
|
|
427
459
|
Semantic marker for icon-only buttons. Layout is handled by [data-aspect1].
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/stuic",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.105.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "vite build && pnpm run prepack",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"prettier": "^3.8.3",
|
|
70
70
|
"prettier-plugin-svelte": "^3.5.2",
|
|
71
71
|
"publint": "^0.3.21",
|
|
72
|
-
"svelte": "^5.
|
|
72
|
+
"svelte": "^5.56.0",
|
|
73
73
|
"svelte-check": "^4.4.8",
|
|
74
74
|
"tailwindcss": "^4.3.0",
|
|
75
75
|
"tsx": "^4.22.3",
|
|
@@ -81,14 +81,14 @@
|
|
|
81
81
|
"dependencies": {
|
|
82
82
|
"@marianmeres/clog": "^3.21.0",
|
|
83
83
|
"@marianmeres/cron": "^2.0.1",
|
|
84
|
-
"@marianmeres/design-tokens": "^1.
|
|
84
|
+
"@marianmeres/design-tokens": "^1.8.0",
|
|
85
85
|
"@marianmeres/icons-fns": "^5.0.0",
|
|
86
86
|
"@marianmeres/item-collection": "^1.4.2",
|
|
87
87
|
"@marianmeres/paging-store": "^2.1.1",
|
|
88
88
|
"@marianmeres/parse-boolean": "^2.1.0",
|
|
89
89
|
"@marianmeres/ticker": "^1.17.1",
|
|
90
90
|
"@marianmeres/tree": "^2.3.0",
|
|
91
|
-
"libphonenumber-js": "^1.13.
|
|
91
|
+
"libphonenumber-js": "^1.13.4",
|
|
92
92
|
"runed": "^0.23.4",
|
|
93
93
|
"tailwind-merge": "^3.6.0"
|
|
94
94
|
}
|