@pk-design/react-tailwind 1.9.0 → 1.10.1
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.
- package/dist/cjs/components/BottomNavigation/BottomNavigation.js +14 -5
- package/dist/cjs/components/BottomNavigation/BottomNavigation.js.map +1 -1
- package/dist/cjs/components/Drawer/Drawer.js +6 -2
- package/dist/cjs/components/Drawer/Drawer.js.map +1 -1
- package/dist/cjs/components/MobileHeader/MobileHeader.js +23 -4
- package/dist/cjs/components/MobileHeader/MobileHeader.js.map +1 -1
- package/dist/cjs/components/PullToRefresh/PullToRefresh.js +145 -57
- package/dist/cjs/components/PullToRefresh/PullToRefresh.js.map +1 -1
- package/dist/cjs/components/StickyActionBar/StickyActionBar.js +2 -2
- package/dist/cjs/components/StickyActionBar/StickyActionBar.js.map +1 -1
- package/dist/cjs/hooks/use-breakpoint.js +48 -0
- package/dist/cjs/hooks/use-breakpoint.js.map +1 -0
- package/dist/cjs/index.js +5 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/es/components/BottomNavigation/BottomNavigation.js +14 -5
- package/dist/es/components/BottomNavigation/BottomNavigation.js.map +1 -1
- package/dist/es/components/Drawer/Drawer.js +6 -2
- package/dist/es/components/Drawer/Drawer.js.map +1 -1
- package/dist/es/components/MobileHeader/MobileHeader.js +23 -4
- package/dist/es/components/MobileHeader/MobileHeader.js.map +1 -1
- package/dist/es/components/PullToRefresh/PullToRefresh.js +146 -58
- package/dist/es/components/PullToRefresh/PullToRefresh.js.map +1 -1
- package/dist/es/components/StickyActionBar/StickyActionBar.js +2 -2
- package/dist/es/components/StickyActionBar/StickyActionBar.js.map +1 -1
- package/dist/es/hooks/use-breakpoint.js +43 -0
- package/dist/es/hooks/use-breakpoint.js.map +1 -0
- package/dist/es/index.js +1 -0
- package/dist/es/index.js.map +1 -1
- package/dist/react-tailwind.cjs.js +231 -70
- package/dist/react-tailwind.cjs.js.map +1 -1
- package/dist/react-tailwind.es.js +229 -72
- package/dist/react-tailwind.es.js.map +1 -1
- package/dist/types/components/BottomNavigation/BottomNavigation.d.ts +1 -1
- package/dist/types/components/BottomNavigation/BottomNavigation.types.d.ts +28 -3
- package/dist/types/components/Drawer/Drawer.types.d.ts +8 -4
- package/dist/types/components/MobileHeader/MobileHeader.d.ts +1 -1
- package/dist/types/components/MobileHeader/MobileHeader.types.d.ts +13 -0
- package/dist/types/components/PullToRefresh/PullToRefresh.d.ts +1 -1
- package/dist/types/components/PullToRefresh/PullToRefresh.types.d.ts +45 -6
- package/dist/types/components/StickyActionBar/StickyActionBar.types.d.ts +12 -0
- package/dist/types/hooks/use-breakpoint.d.ts +27 -0
- package/dist/types/hooks/use-breakpoint.test.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/test-utils/mock-viewport.d.ts +7 -0
- package/package.json +1 -1
|
@@ -1,79 +1,167 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
|
-
import { useRef, useState,
|
|
2
|
+
import { useRef, useState, useLayoutEffect, useEffect } from 'react';
|
|
3
3
|
import { Loader } from '../Loader/index.js';
|
|
4
4
|
import { buildClassName } from '../../utils/build-classname.js';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// Resistance applied to the finger's travel, so the pull feels elastic.
|
|
7
|
+
const RESISTANCE = 0.45;
|
|
8
|
+
// Finger travel (px) before deciding whether a gesture is a pull or a sideways swipe.
|
|
9
|
+
const AXIS_LOCK_SLOP = 8;
|
|
10
|
+
// Overlay badge size and the gap it rests at below the container's top edge.
|
|
11
|
+
const BADGE_SIZE = 40;
|
|
12
|
+
const BADGE_TOP_GAP = 10;
|
|
13
|
+
function PullArrowIcon({ progress, className }) {
|
|
14
|
+
return (jsx("svg", { className: className, style: { transform: `rotate(${Math.min(180, progress * 180)}deg)` }, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 14l-7 7m0 0l-7-7m7 7V3" }) }));
|
|
9
15
|
}
|
|
10
|
-
function PullToRefresh({ onRefresh, pullThreshold = 70, maxPullDistance = 120, disabled = false, refreshing: externalRefreshing, pullingContent, refreshingContent, children, className = '', contentClass = '', }) {
|
|
16
|
+
function PullToRefresh({ onRefresh, onError, variant = 'push', pullThreshold = 70, maxPullDistance = 120, disabled = false, refreshing: externalRefreshing, pullingContent, refreshingContent, pullLabel = 'Pull down to refresh', releaseLabel = 'Release to refresh', refreshingLabel = 'Refreshing...', refreshedLabel = 'Content refreshed', children, className = '', contentClass = '', }) {
|
|
11
17
|
const containerRef = useRef(null);
|
|
12
|
-
const startYRef = useRef(0);
|
|
13
|
-
const isPullingRef = useRef(false);
|
|
14
18
|
const [pullDistance, setPullDistance] = useState(0);
|
|
19
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
15
20
|
const [internalRefreshing, setInternalRefreshing] = useState(false);
|
|
21
|
+
const [announcement, setAnnouncement] = useState('');
|
|
16
22
|
const isRefreshing = externalRefreshing !== undefined ? externalRefreshing : internalRefreshing;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
// The touch listeners are attached once, natively (see below), so they read
|
|
24
|
+
// the latest props and state through this ref instead of stale closures.
|
|
25
|
+
const latest = useRef({
|
|
26
|
+
disabled,
|
|
27
|
+
isRefreshing,
|
|
28
|
+
pullThreshold,
|
|
29
|
+
maxPullDistance,
|
|
30
|
+
onRefresh,
|
|
31
|
+
onError,
|
|
32
|
+
refreshedLabel,
|
|
33
|
+
});
|
|
34
|
+
useLayoutEffect(() => {
|
|
35
|
+
latest.current = {
|
|
36
|
+
disabled,
|
|
37
|
+
isRefreshing,
|
|
38
|
+
pullThreshold,
|
|
39
|
+
maxPullDistance,
|
|
40
|
+
onRefresh,
|
|
41
|
+
onError,
|
|
42
|
+
refreshedLabel,
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
useEffect(() => {
|
|
29
46
|
const container = containerRef.current;
|
|
30
|
-
if (!container
|
|
31
|
-
return;
|
|
32
|
-
const currentY = e.touches[0].clientY;
|
|
33
|
-
const rawDistance = currentY - startYRef.current;
|
|
34
|
-
if (rawDistance > 0) {
|
|
35
|
-
// Rubber-band resistance formula
|
|
36
|
-
const dampened = Math.min(maxPullDistance, rawDistance * 0.45);
|
|
37
|
-
setPullDistance(dampened);
|
|
38
|
-
if (e.cancelable) {
|
|
39
|
-
e.preventDefault();
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
setPullDistance(0);
|
|
44
|
-
}
|
|
45
|
-
}, [disabled, isRefreshing, maxPullDistance]);
|
|
46
|
-
const handleTouchEnd = useCallback(async () => {
|
|
47
|
-
if (!isPullingRef.current || disabled || isRefreshing)
|
|
47
|
+
if (!container)
|
|
48
48
|
return;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
let mounted = true;
|
|
50
|
+
let pulling = false;
|
|
51
|
+
let axis = 'pending';
|
|
52
|
+
let startX = 0;
|
|
53
|
+
let startY = 0;
|
|
54
|
+
let distance = 0;
|
|
55
|
+
const setDistance = (next) => {
|
|
56
|
+
distance = next;
|
|
57
|
+
setPullDistance(next);
|
|
58
|
+
};
|
|
59
|
+
// Ends a gesture without refreshing: a cancelled touch, a sideways swipe,
|
|
60
|
+
// or content that scrolled away from the top.
|
|
61
|
+
const reset = () => {
|
|
62
|
+
pulling = false;
|
|
63
|
+
setIsDragging(false);
|
|
64
|
+
setDistance(0);
|
|
65
|
+
};
|
|
66
|
+
const refresh = async () => {
|
|
67
|
+
const { pullThreshold: threshold, onRefresh: run, onError: report } = latest.current;
|
|
68
|
+
setDistance(threshold);
|
|
52
69
|
setInternalRefreshing(true);
|
|
53
70
|
try {
|
|
54
|
-
await
|
|
71
|
+
await run();
|
|
72
|
+
if (mounted)
|
|
73
|
+
setAnnouncement(latest.current.refreshedLabel);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
report === null || report === void 0 ? void 0 : report(error);
|
|
55
77
|
}
|
|
56
78
|
finally {
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
if (mounted) {
|
|
80
|
+
setInternalRefreshing(false);
|
|
81
|
+
setDistance(0);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const handleTouchStart = (e) => {
|
|
86
|
+
var _a, _b;
|
|
87
|
+
const { disabled: isDisabled, isRefreshing: busy } = latest.current;
|
|
88
|
+
if (isDisabled || busy || container.scrollTop > 0 || e.touches.length !== 1)
|
|
89
|
+
return;
|
|
90
|
+
startX = (_a = e.touches[0].clientX) !== null && _a !== void 0 ? _a : 0;
|
|
91
|
+
startY = (_b = e.touches[0].clientY) !== null && _b !== void 0 ? _b : 0;
|
|
92
|
+
axis = 'pending';
|
|
93
|
+
pulling = true;
|
|
94
|
+
setAnnouncement('');
|
|
95
|
+
setIsDragging(true);
|
|
96
|
+
setDistance(0);
|
|
97
|
+
};
|
|
98
|
+
const handleTouchMove = (e) => {
|
|
99
|
+
var _a, _b, _c, _d;
|
|
100
|
+
if (!pulling)
|
|
101
|
+
return;
|
|
102
|
+
if (latest.current.disabled || latest.current.isRefreshing || container.scrollTop > 0) {
|
|
103
|
+
reset();
|
|
104
|
+
return;
|
|
59
105
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
106
|
+
const dx = ((_b = (_a = e.touches[0]) === null || _a === void 0 ? void 0 : _a.clientX) !== null && _b !== void 0 ? _b : 0) - startX;
|
|
107
|
+
const dy = ((_d = (_c = e.touches[0]) === null || _c === void 0 ? void 0 : _c.clientY) !== null && _d !== void 0 ? _d : 0) - startY;
|
|
108
|
+
if (axis === 'pending') {
|
|
109
|
+
if (Math.abs(dx) < AXIS_LOCK_SLOP && Math.abs(dy) < AXIS_LOCK_SLOP)
|
|
110
|
+
return;
|
|
111
|
+
axis = dy > 0 && Math.abs(dy) > Math.abs(dx) ? 'vertical' : 'horizontal';
|
|
112
|
+
// A sideways swipe or an upward scroll belongs to the content, not to us.
|
|
113
|
+
if (axis === 'horizontal') {
|
|
114
|
+
reset();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const next = Math.min(latest.current.maxPullDistance, Math.max(0, dy) * RESISTANCE);
|
|
119
|
+
setDistance(next);
|
|
120
|
+
// Needs the non-passive listener: stops the browser's own pull-to-refresh/overscroll.
|
|
121
|
+
if (next > 0 && e.cancelable)
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
};
|
|
124
|
+
const handleTouchEnd = () => {
|
|
125
|
+
if (!pulling)
|
|
126
|
+
return;
|
|
127
|
+
pulling = false;
|
|
128
|
+
setIsDragging(false);
|
|
129
|
+
if (distance >= latest.current.pullThreshold)
|
|
130
|
+
void refresh();
|
|
131
|
+
else
|
|
132
|
+
setDistance(0);
|
|
133
|
+
};
|
|
134
|
+
// React registers touch listeners as passive, where preventDefault() is
|
|
135
|
+
// ignored, so these are attached natively with touchmove non-passive.
|
|
136
|
+
container.addEventListener('touchstart', handleTouchStart, { passive: true });
|
|
137
|
+
container.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
138
|
+
container.addEventListener('touchend', handleTouchEnd);
|
|
139
|
+
container.addEventListener('touchcancel', reset);
|
|
140
|
+
return () => {
|
|
141
|
+
mounted = false;
|
|
142
|
+
container.removeEventListener('touchstart', handleTouchStart);
|
|
143
|
+
container.removeEventListener('touchmove', handleTouchMove);
|
|
144
|
+
container.removeEventListener('touchend', handleTouchEnd);
|
|
145
|
+
container.removeEventListener('touchcancel', reset);
|
|
146
|
+
};
|
|
147
|
+
}, []);
|
|
65
148
|
const progress = Math.min(1, pullDistance / pullThreshold);
|
|
66
149
|
const isReady = pullDistance >= pullThreshold;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
150
|
+
const offset = isRefreshing ? pullThreshold : pullDistance;
|
|
151
|
+
// Follow the finger exactly while dragging; animate only the settle afterwards.
|
|
152
|
+
const motionClass = isDragging
|
|
153
|
+
? 'transition-none'
|
|
154
|
+
: 'transition-transform duration-200 ease-out motion-reduce:transition-none';
|
|
155
|
+
return (jsxs("div", { ref: containerRef, className: buildClassName('relative overflow-y-auto overscroll-none', className), "data-testid": "pull-to-refresh-container", children: [jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: isRefreshing ? refreshingLabel : announcement }), variant === 'overlay' ? (jsx("div", { className: buildClassName('pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center', motionClass), style: {
|
|
156
|
+
transform: `translateY(${offset - BADGE_SIZE + BADGE_TOP_GAP}px)`,
|
|
157
|
+
opacity: offset > 0 ? 1 : 0,
|
|
158
|
+
}, "aria-hidden": "true", "data-testid": "pull-to-refresh-indicator", children: jsx("div", { className: "grid size-10 place-items-center rounded-full bg-[var(--ui-primary)] text-white shadow-lg", children: isRefreshing
|
|
159
|
+
? (refreshingContent !== null && refreshingContent !== void 0 ? refreshingContent : jsx(Loader, { size: "xs", className: "text-white" }))
|
|
160
|
+
: (pullingContent !== null && pullingContent !== void 0 ? pullingContent : jsx(PullArrowIcon, { progress: progress, className: "size-4" })) }) })) : (
|
|
161
|
+
// Absolutely positioned, so the content's own translate is the only shift.
|
|
162
|
+
jsx("div", { className: "pointer-events-none absolute inset-x-0 top-0 z-20 flex items-center justify-center overflow-hidden", style: { height: `${offset}px`, opacity: offset > 0 ? 1 : 0 }, "aria-hidden": "true", "data-testid": "pull-to-refresh-indicator", children: jsx("div", { className: "flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-white dark:bg-gray-800 border border-[var(--ui-border)] shadow-md text-xs font-semibold text-gray-700 dark:text-gray-200", children: isRefreshing
|
|
163
|
+
? (refreshingContent !== null && refreshingContent !== void 0 ? refreshingContent : (jsxs(Fragment, { children: [jsx(Loader, { size: "sm" }), jsx("span", { children: refreshingLabel })] })))
|
|
164
|
+
: (pullingContent !== null && pullingContent !== void 0 ? pullingContent : (jsxs(Fragment, { children: [jsx(PullArrowIcon, { progress: progress, className: "size-4 text-[var(--ui-primary)] dark:text-blue-400" }), jsx("span", { children: isReady ? releaseLabel : pullLabel })] }))) }) })), jsx("div", { className: buildClassName(variant === 'push' && motionClass, contentClass), style: variant === 'push' ? { transform: `translateY(${offset}px)` } : undefined, "data-testid": "pull-to-refresh-content", children: children })] }));
|
|
77
165
|
}
|
|
78
166
|
PullToRefresh.displayName = 'PullToRefresh';
|
|
79
167
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PullToRefresh.js","sources":["../../../../../src/components/PullToRefresh/PullToRefresh.tsx"],"sourcesContent":["import React, { useRef, useState, useCallback } from 'react'\nimport type { PullToRefreshProps } from './PullToRefresh.types'\nimport { Loader } from '../Loader'\nimport { buildClassName } from '../../utils/build-classname'\n\nfunction PullArrowIcon({ progress }: { progress: number }) {\n const rotation = Math.min(180, progress * 180)\n return (\n <svg\n className=\"size-4 text-blue-600 dark:text-blue-400 transition-transform duration-150\"\n style={{ transform: `rotate(${rotation}deg)` }}\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2.5}\n d=\"M19 14l-7 7m0 0l-7-7m7 7V3\"\n />\n </svg>\n )\n}\n\nexport function PullToRefresh({\n onRefresh,\n pullThreshold = 70,\n maxPullDistance = 120,\n disabled = false,\n refreshing: externalRefreshing,\n pullingContent,\n refreshingContent,\n children,\n className = '',\n contentClass = '',\n}: PullToRefreshProps) {\n const containerRef = useRef<HTMLDivElement>(null)\n const startYRef = useRef<number>(0)\n const isPullingRef = useRef<boolean>(false)\n\n const [pullDistance, setPullDistance] = useState<number>(0)\n const [internalRefreshing, setInternalRefreshing] = useState<boolean>(false)\n\n const isRefreshing = externalRefreshing !== undefined ? externalRefreshing : internalRefreshing\n\n const handleTouchStart = useCallback(\n (e: React.TouchEvent<HTMLDivElement>) => {\n if (disabled || isRefreshing) return\n const container = containerRef.current\n if (container && container.scrollTop === 0) {\n startYRef.current = e.touches[0].clientY\n isPullingRef.current = true\n }\n },\n [disabled, isRefreshing],\n )\n\n const handleTouchMove = useCallback(\n (e: React.TouchEvent<HTMLDivElement>) => {\n if (!isPullingRef.current || disabled || isRefreshing) return\n const container = containerRef.current\n if (!container || container.scrollTop > 0) return\n\n const currentY = e.touches[0].clientY\n const rawDistance = currentY - startYRef.current\n\n if (rawDistance > 0) {\n // Rubber-band resistance formula\n const dampened = Math.min(maxPullDistance, rawDistance * 0.45)\n setPullDistance(dampened)\n if (e.cancelable) {\n e.preventDefault()\n }\n } else {\n setPullDistance(0)\n }\n },\n [disabled, isRefreshing, maxPullDistance],\n )\n\n const handleTouchEnd = useCallback(async () => {\n if (!isPullingRef.current || disabled || isRefreshing) return\n isPullingRef.current = false\n\n if (pullDistance >= pullThreshold) {\n setPullDistance(pullThreshold)\n setInternalRefreshing(true)\n try {\n await onRefresh()\n } finally {\n setInternalRefreshing(false)\n setPullDistance(0)\n }\n } else {\n setPullDistance(0)\n }\n }, [disabled, isRefreshing, onRefresh, pullDistance, pullThreshold])\n\n const progress = Math.min(1, pullDistance / pullThreshold)\n const isReady = pullDistance >= pullThreshold\n\n return (\n <div\n ref={containerRef}\n onTouchStart={handleTouchStart}\n onTouchMove={handleTouchMove}\n onTouchEnd={handleTouchEnd}\n className={buildClassName('relative overflow-y-auto overscroll-none', className)}\n data-testid=\"pull-to-refresh-container\"\n >\n {/* Pull / Refresh Indicator Header */}\n <div\n className=\"left-0 right-0 top-0 flex items-center justify-center pointer-events-none transition-all duration-150 z-20\"\n style={{\n height: isRefreshing ? `${pullThreshold}px` : `${pullDistance}px`,\n opacity: pullDistance > 0 || isRefreshing ? 1 : 0,\n }}\n data-testid=\"pull-to-refresh-indicator\"\n >\n <div className=\"flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-white dark:bg-gray-800 border border-[var(--ui-border)] shadow-md text-xs font-semibold text-gray-700 dark:text-gray-200\">\n {isRefreshing ? (\n refreshingContent || (\n <>\n <Loader size=\"sm\" />\n <span>Refreshing...</span>\n </>\n )\n ) : pullingContent ? (\n pullingContent\n ) : (\n <>\n <PullArrowIcon progress={progress} />\n <span>{isReady ? 'Release to refresh' : 'Pull down to refresh'}</span>\n </>\n )}\n </div>\n </div>\n\n {/* Main Content */}\n <div\n className={buildClassName('transition-transform duration-150', contentClass)}\n style={{\n transform: isRefreshing\n ? `translateY(${pullThreshold}px)`\n : pullDistance > 0\n ? `translateY(${pullDistance}px)`\n : 'translateY(0px)',\n }}\n data-testid=\"pull-to-refresh-content\"\n >\n {children}\n </div>\n </div>\n )\n}\n\nPullToRefresh.displayName = 'PullToRefresh'\n"],"names":["_jsx","_jsxs","_Fragment"],"mappings":";;;;;AAKA,SAAS,aAAa,CAAC,EAAE,QAAQ,EAAwB,EAAA;AACvD,IAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,GAAG,GAAG,CAAC;IAC9C,QACEA,aACE,SAAS,EAAC,2EAA2E,EACrF,KAAK,EAAE,EAAE,SAAS,EAAE,CAAA,OAAA,EAAU,QAAQ,MAAM,EAAE,EAC9C,IAAI,EAAC,MAAM,EACX,OAAO,EAAC,WAAW,EACnB,MAAM,EAAC,cAAc,EAAA,QAAA,EAErBA,GAAA,CAAA,MAAA,EAAA,EACE,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,WAAW,EAAE,GAAG,EAChB,CAAC,EAAC,4BAA4B,EAAA,CAC9B,EAAA,CACE;AAEV;AAEM,SAAU,aAAa,CAAC,EAC5B,SAAS,EACT,aAAa,GAAG,EAAE,EAClB,eAAe,GAAG,GAAG,EACrB,QAAQ,GAAG,KAAK,EAChB,UAAU,EAAE,kBAAkB,EAC9B,cAAc,EACd,iBAAiB,EACjB,QAAQ,EACR,SAAS,GAAG,EAAE,EACd,YAAY,GAAG,EAAE,GACE,EAAA;AACnB,IAAA,MAAM,YAAY,GAAG,MAAM,CAAiB,IAAI,CAAC;AACjD,IAAA,MAAM,SAAS,GAAG,MAAM,CAAS,CAAC,CAAC;AACnC,IAAA,MAAM,YAAY,GAAG,MAAM,CAAU,KAAK,CAAC;IAE3C,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAS,CAAC,CAAC;IAC3D,MAAM,CAAC,kBAAkB,EAAE,qBAAqB,CAAC,GAAG,QAAQ,CAAU,KAAK,CAAC;AAE5E,IAAA,MAAM,YAAY,GAAG,kBAAkB,KAAK,SAAS,GAAG,kBAAkB,GAAG,kBAAkB;AAE/F,IAAA,MAAM,gBAAgB,GAAG,WAAW,CAClC,CAAC,CAAmC,KAAI;QACtC,IAAI,QAAQ,IAAI,YAAY;YAAE;AAC9B,QAAA,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO;QACtC,IAAI,SAAS,IAAI,SAAS,CAAC,SAAS,KAAK,CAAC,EAAE;YAC1C,SAAS,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO;AACxC,YAAA,YAAY,CAAC,OAAO,GAAG,IAAI;QAC7B;AACF,IAAA,CAAC,EACD,CAAC,QAAQ,EAAE,YAAY,CAAC,CACzB;AAED,IAAA,MAAM,eAAe,GAAG,WAAW,CACjC,CAAC,CAAmC,KAAI;AACtC,QAAA,IAAI,CAAC,YAAY,CAAC,OAAO,IAAI,QAAQ,IAAI,YAAY;YAAE;AACvD,QAAA,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO;AACtC,QAAA,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,SAAS,GAAG,CAAC;YAAE;QAE3C,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO;AACrC,QAAA,MAAM,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC,OAAO;AAEhD,QAAA,IAAI,WAAW,GAAG,CAAC,EAAE;;AAEnB,YAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,eAAe,EAAE,WAAW,GAAG,IAAI,CAAC;YAC9D,eAAe,CAAC,QAAQ,CAAC;AACzB,YAAA,IAAI,CAAC,CAAC,UAAU,EAAE;gBAChB,CAAC,CAAC,cAAc,EAAE;YACpB;QACF;aAAO;YACL,eAAe,CAAC,CAAC,CAAC;QACpB;IACF,CAAC,EACD,CAAC,QAAQ,EAAE,YAAY,EAAE,eAAe,CAAC,CAC1C;AAED,IAAA,MAAM,cAAc,GAAG,WAAW,CAAC,YAAW;AAC5C,QAAA,IAAI,CAAC,YAAY,CAAC,OAAO,IAAI,QAAQ,IAAI,YAAY;YAAE;AACvD,QAAA,YAAY,CAAC,OAAO,GAAG,KAAK;AAE5B,QAAA,IAAI,YAAY,IAAI,aAAa,EAAE;YACjC,eAAe,CAAC,aAAa,CAAC;YAC9B,qBAAqB,CAAC,IAAI,CAAC;AAC3B,YAAA,IAAI;gBACF,MAAM,SAAS,EAAE;YACnB;oBAAU;gBACR,qBAAqB,CAAC,KAAK,CAAC;gBAC5B,eAAe,CAAC,CAAC,CAAC;YACpB;QACF;aAAO;YACL,eAAe,CAAC,CAAC,CAAC;QACpB;AACF,IAAA,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC;AAEpE,IAAA,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,GAAG,aAAa,CAAC;AAC1D,IAAA,MAAM,OAAO,GAAG,YAAY,IAAI,aAAa;AAE7C,IAAA,QACEC,IAAA,CAAA,KAAA,EAAA,EACE,GAAG,EAAE,YAAY,EACjB,YAAY,EAAE,gBAAgB,EAC9B,WAAW,EAAE,eAAe,EAC5B,UAAU,EAAE,cAAc,EAC1B,SAAS,EAAE,cAAc,CAAC,0CAA0C,EAAE,SAAS,CAAC,EAAA,aAAA,EACpE,2BAA2B,EAAA,QAAA,EAAA,CAGvCD,aACE,SAAS,EAAC,4GAA4G,EACtH,KAAK,EAAE;AACL,oBAAA,MAAM,EAAE,YAAY,GAAG,CAAA,EAAG,aAAa,CAAA,EAAA,CAAI,GAAG,CAAA,EAAG,YAAY,CAAA,EAAA,CAAI;AACjE,oBAAA,OAAO,EAAE,YAAY,GAAG,CAAC,IAAI,YAAY,GAAG,CAAC,GAAG,CAAC;AAClD,iBAAA,EAAA,aAAA,EACW,2BAA2B,EAAA,QAAA,EAEvCA,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,gLAAgL,EAAA,QAAA,EAC5L,YAAY,IACX,iBAAiB,KACfC,4BACED,GAAA,CAAC,MAAM,EAAA,EAAC,IAAI,EAAC,IAAI,EAAA,CAAG,EACpBA,GAAA,CAAA,MAAA,EAAA,EAAA,QAAA,EAAA,eAAA,EAAA,CAA0B,IACzB,CACJ,IACC,cAAc,IAChB,cAAc,KAEdC,IAAA,CAAAC,QAAA,EAAA,EAAA,QAAA,EAAA,CACEF,GAAA,CAAC,aAAa,EAAA,EAAC,QAAQ,EAAE,QAAQ,EAAA,CAAI,EACrCA,wBAAO,OAAO,GAAG,oBAAoB,GAAG,sBAAsB,GAAQ,CAAA,EAAA,CACrE,CACJ,EAAA,CACG,EAAA,CACF,EAGNA,GAAA,CAAA,KAAA,EAAA,EACE,SAAS,EAAE,cAAc,CAAC,mCAAmC,EAAE,YAAY,CAAC,EAC5E,KAAK,EAAE;AACL,oBAAA,SAAS,EAAE;0BACP,CAAA,WAAA,EAAc,aAAa,CAAA,GAAA;0BAC3B,YAAY,GAAG;8BACb,CAAA,WAAA,EAAc,YAAY,CAAA,GAAA;AAC5B,8BAAE,iBAAiB;AACxB,iBAAA,EAAA,aAAA,EACW,yBAAyB,EAAA,QAAA,EAEpC,QAAQ,EAAA,CACL,CAAA,EAAA,CACF;AAEV;AAEA,aAAa,CAAC,WAAW,GAAG,eAAe;;;;"}
|
|
1
|
+
{"version":3,"file":"PullToRefresh.js","sources":["../../../../../src/components/PullToRefresh/PullToRefresh.tsx"],"sourcesContent":["import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'\nimport type { PullToRefreshProps } from './PullToRefresh.types'\nimport { Loader } from '../Loader'\nimport { buildClassName } from '../../utils/build-classname'\n\n// Resistance applied to the finger's travel, so the pull feels elastic.\nconst RESISTANCE = 0.45\n// Finger travel (px) before deciding whether a gesture is a pull or a sideways swipe.\nconst AXIS_LOCK_SLOP = 8\n// Overlay badge size and the gap it rests at below the container's top edge.\nconst BADGE_SIZE = 40\nconst BADGE_TOP_GAP = 10\n\nfunction PullArrowIcon({ progress, className }: { progress: number; className?: string }) {\n return (\n <svg\n className={className}\n style={{ transform: `rotate(${Math.min(180, progress * 180)}deg)` }}\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2.5}\n d=\"M19 14l-7 7m0 0l-7-7m7 7V3\"\n />\n </svg>\n )\n}\n\ntype Axis = 'pending' | 'vertical' | 'horizontal'\n\nexport function PullToRefresh({\n onRefresh,\n onError,\n variant = 'push',\n pullThreshold = 70,\n maxPullDistance = 120,\n disabled = false,\n refreshing: externalRefreshing,\n pullingContent,\n refreshingContent,\n pullLabel = 'Pull down to refresh',\n releaseLabel = 'Release to refresh',\n refreshingLabel = 'Refreshing...',\n refreshedLabel = 'Content refreshed',\n children,\n className = '',\n contentClass = '',\n}: PullToRefreshProps) {\n const containerRef = useRef<HTMLDivElement>(null)\n const [pullDistance, setPullDistance] = useState(0)\n const [isDragging, setIsDragging] = useState(false)\n const [internalRefreshing, setInternalRefreshing] = useState(false)\n const [announcement, setAnnouncement] = useState('')\n\n const isRefreshing = externalRefreshing !== undefined ? externalRefreshing : internalRefreshing\n\n // The touch listeners are attached once, natively (see below), so they read\n // the latest props and state through this ref instead of stale closures.\n const latest = useRef({\n disabled,\n isRefreshing,\n pullThreshold,\n maxPullDistance,\n onRefresh,\n onError,\n refreshedLabel,\n })\n useLayoutEffect(() => {\n latest.current = {\n disabled,\n isRefreshing,\n pullThreshold,\n maxPullDistance,\n onRefresh,\n onError,\n refreshedLabel,\n }\n })\n\n useEffect(() => {\n const container = containerRef.current\n if (!container) return\n\n let mounted = true\n let pulling = false\n let axis: Axis = 'pending'\n let startX = 0\n let startY = 0\n let distance = 0\n\n const setDistance = (next: number) => {\n distance = next\n setPullDistance(next)\n }\n\n // Ends a gesture without refreshing: a cancelled touch, a sideways swipe,\n // or content that scrolled away from the top.\n const reset = () => {\n pulling = false\n setIsDragging(false)\n setDistance(0)\n }\n\n const refresh = async () => {\n const { pullThreshold: threshold, onRefresh: run, onError: report } = latest.current\n setDistance(threshold)\n setInternalRefreshing(true)\n try {\n await run()\n if (mounted) setAnnouncement(latest.current.refreshedLabel)\n } catch (error) {\n report?.(error)\n } finally {\n if (mounted) {\n setInternalRefreshing(false)\n setDistance(0)\n }\n }\n }\n\n const handleTouchStart = (e: TouchEvent) => {\n const { disabled: isDisabled, isRefreshing: busy } = latest.current\n if (isDisabled || busy || container.scrollTop > 0 || e.touches.length !== 1) return\n startX = e.touches[0].clientX ?? 0\n startY = e.touches[0].clientY ?? 0\n axis = 'pending'\n pulling = true\n setAnnouncement('')\n setIsDragging(true)\n setDistance(0)\n }\n\n const handleTouchMove = (e: TouchEvent) => {\n if (!pulling) return\n if (latest.current.disabled || latest.current.isRefreshing || container.scrollTop > 0) {\n reset()\n return\n }\n\n const dx = (e.touches[0]?.clientX ?? 0) - startX\n const dy = (e.touches[0]?.clientY ?? 0) - startY\n\n if (axis === 'pending') {\n if (Math.abs(dx) < AXIS_LOCK_SLOP && Math.abs(dy) < AXIS_LOCK_SLOP) return\n axis = dy > 0 && Math.abs(dy) > Math.abs(dx) ? 'vertical' : 'horizontal'\n // A sideways swipe or an upward scroll belongs to the content, not to us.\n if (axis === 'horizontal') {\n reset()\n return\n }\n }\n\n const next = Math.min(latest.current.maxPullDistance, Math.max(0, dy) * RESISTANCE)\n setDistance(next)\n // Needs the non-passive listener: stops the browser's own pull-to-refresh/overscroll.\n if (next > 0 && e.cancelable) e.preventDefault()\n }\n\n const handleTouchEnd = () => {\n if (!pulling) return\n pulling = false\n setIsDragging(false)\n if (distance >= latest.current.pullThreshold) void refresh()\n else setDistance(0)\n }\n\n // React registers touch listeners as passive, where preventDefault() is\n // ignored, so these are attached natively with touchmove non-passive.\n container.addEventListener('touchstart', handleTouchStart, { passive: true })\n container.addEventListener('touchmove', handleTouchMove, { passive: false })\n container.addEventListener('touchend', handleTouchEnd)\n container.addEventListener('touchcancel', reset)\n\n return () => {\n mounted = false\n container.removeEventListener('touchstart', handleTouchStart)\n container.removeEventListener('touchmove', handleTouchMove)\n container.removeEventListener('touchend', handleTouchEnd)\n container.removeEventListener('touchcancel', reset)\n }\n }, [])\n\n const progress = Math.min(1, pullDistance / pullThreshold)\n const isReady = pullDistance >= pullThreshold\n const offset = isRefreshing ? pullThreshold : pullDistance\n // Follow the finger exactly while dragging; animate only the settle afterwards.\n const motionClass = isDragging\n ? 'transition-none'\n : 'transition-transform duration-200 ease-out motion-reduce:transition-none'\n\n return (\n <div\n ref={containerRef}\n className={buildClassName('relative overflow-y-auto overscroll-none', className)}\n data-testid=\"pull-to-refresh-container\"\n >\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {isRefreshing ? refreshingLabel : announcement}\n </span>\n\n {variant === 'overlay' ? (\n <div\n className={buildClassName(\n 'pointer-events-none absolute inset-x-0 top-0 z-20 flex justify-center',\n motionClass,\n )}\n style={{\n transform: `translateY(${offset - BADGE_SIZE + BADGE_TOP_GAP}px)`,\n opacity: offset > 0 ? 1 : 0,\n }}\n aria-hidden=\"true\"\n data-testid=\"pull-to-refresh-indicator\"\n >\n <div className=\"grid size-10 place-items-center rounded-full bg-[var(--ui-primary)] text-white shadow-lg\">\n {isRefreshing\n ? (refreshingContent ?? <Loader size=\"xs\" className=\"text-white\" />)\n : (pullingContent ?? <PullArrowIcon progress={progress} className=\"size-4\" />)}\n </div>\n </div>\n ) : (\n // Absolutely positioned, so the content's own translate is the only shift.\n <div\n className=\"pointer-events-none absolute inset-x-0 top-0 z-20 flex items-center justify-center overflow-hidden\"\n style={{ height: `${offset}px`, opacity: offset > 0 ? 1 : 0 }}\n aria-hidden=\"true\"\n data-testid=\"pull-to-refresh-indicator\"\n >\n <div className=\"flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-white dark:bg-gray-800 border border-[var(--ui-border)] shadow-md text-xs font-semibold text-gray-700 dark:text-gray-200\">\n {isRefreshing\n ? (refreshingContent ?? (\n <>\n <Loader size=\"sm\" />\n <span>{refreshingLabel}</span>\n </>\n ))\n : (pullingContent ?? (\n <>\n <PullArrowIcon\n progress={progress}\n className=\"size-4 text-[var(--ui-primary)] dark:text-blue-400\"\n />\n <span>{isReady ? releaseLabel : pullLabel}</span>\n </>\n ))}\n </div>\n </div>\n )}\n\n <div\n className={buildClassName(variant === 'push' && motionClass, contentClass)}\n style={variant === 'push' ? { transform: `translateY(${offset}px)` } : undefined}\n data-testid=\"pull-to-refresh-content\"\n >\n {children}\n </div>\n </div>\n )\n}\n\nPullToRefresh.displayName = 'PullToRefresh'\n"],"names":["_jsx","_jsxs","_Fragment"],"mappings":"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"}
|
|
@@ -5,14 +5,14 @@ import { Drawer } from '../Drawer/Drawer.js';
|
|
|
5
5
|
|
|
6
6
|
const ChevronUpIcon = ({ className }) => (jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: className, "aria-hidden": "true", children: jsx("polyline", { points: "18 15 12 9 6 15" }) }));
|
|
7
7
|
const StickyActionBar = forwardRef((props, ref) => {
|
|
8
|
-
const { summaryContent, actionsContent, drawerContent, drawerTitle = 'Summary Details', position = 'fixed', containerMaxWidth = 'max-w-5xl', className, ...restProps } = props;
|
|
8
|
+
const { summaryContent, actionsContent, drawerContent, drawerTitle = 'Summary Details', position = 'fixed', containerMaxWidth = 'max-w-5xl', safeArea = true, bottomOffset, className, style, ...restProps } = props;
|
|
9
9
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
|
10
10
|
const positionClass = position === 'fixed'
|
|
11
11
|
? 'fixed bottom-0 left-0 right-0 z-30'
|
|
12
12
|
: position === 'sticky'
|
|
13
13
|
? 'sticky bottom-0 z-30'
|
|
14
14
|
: 'relative z-10';
|
|
15
|
-
return (jsxs(Fragment, { children: [jsx("div", { ref: ref, className: buildClassName(positionClass, 'border-t border-[var(--ui-border)] bg-white/90 p-3.5 backdrop-blur-md shadow-lg dark:bg-gray-900/90 transition-all', className), "data-testid": "sticky-action-bar", ...restProps, children: jsxs("div", { className: buildClassName('mx-auto flex items-center justify-between gap-3', containerMaxWidth), children: [summaryContent && (jsx("div", { className: "flex items-center gap-2", children: drawerContent ? (jsxs("button", { type: "button", onClick: () => setIsDrawerOpen(true), className: "flex items-center gap-2 text-left transition-opacity hover:opacity-80 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-primary-ring)] rounded-lg p-1 -m-1", "aria-label": "Expand summary details", "data-testid": "summary-trigger", children: [jsx("div", { children: summaryContent }), jsx(ChevronUpIcon, { className: "size-4 text-[var(--ui-primary)] dark:text-blue-400 shrink-0" })] })) : (jsx("div", { children: summaryContent })) })), actionsContent && (jsx("div", { className: "flex items-center gap-2 shrink-0 ml-auto", "data-testid": "actions-stack", children: actionsContent }))] }) }), drawerContent && (jsx(Drawer, { isOpen: isDrawerOpen, onClose: () => setIsDrawerOpen(false), align: "bottom", size: "full", title: drawerTitle, showBackButton: true, contentClass: "px-4 pb-6 max-h-[calc(100svh-6rem)] overflow-y-auto", children: drawerContent }))] }));
|
|
15
|
+
return (jsxs(Fragment, { children: [jsx("div", { ref: ref, className: buildClassName(positionClass, 'border-t border-[var(--ui-border)] bg-white/90 p-3.5 backdrop-blur-md shadow-lg dark:bg-gray-900/90 transition-all', safeArea && 'pb-[calc(0.875rem+env(safe-area-inset-bottom,0px))]', className), style: bottomOffset && position !== 'relative' ? { bottom: bottomOffset, ...style } : style, "data-testid": "sticky-action-bar", ...restProps, children: jsxs("div", { className: buildClassName('mx-auto flex items-center justify-between gap-3', containerMaxWidth), children: [summaryContent && (jsx("div", { className: "flex items-center gap-2", children: drawerContent ? (jsxs("button", { type: "button", onClick: () => setIsDrawerOpen(true), className: "flex items-center gap-2 text-left transition-opacity hover:opacity-80 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-primary-ring)] rounded-lg p-1 -m-1", "aria-label": "Expand summary details", "data-testid": "summary-trigger", children: [jsx("div", { children: summaryContent }), jsx(ChevronUpIcon, { className: "size-4 text-[var(--ui-primary)] dark:text-blue-400 shrink-0" })] })) : (jsx("div", { children: summaryContent })) })), actionsContent && (jsx("div", { className: "flex items-center gap-2 shrink-0 ml-auto", "data-testid": "actions-stack", children: actionsContent }))] }) }), drawerContent && (jsx(Drawer, { isOpen: isDrawerOpen, onClose: () => setIsDrawerOpen(false), align: "bottom", size: "full", title: drawerTitle, showBackButton: true, contentClass: "px-4 pb-6 max-h-[calc(100svh-6rem)] overflow-y-auto", children: drawerContent }))] }));
|
|
16
16
|
});
|
|
17
17
|
StickyActionBar.displayName = 'StickyActionBar';
|
|
18
18
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StickyActionBar.js","sources":["../../../../../src/components/StickyActionBar/StickyActionBar.tsx"],"sourcesContent":["import { forwardRef, useState } from 'react'\nimport { buildClassName } from '../../utils/build-classname'\nimport { Drawer } from '../Drawer'\nimport type { StickyActionBarProps } from './StickyActionBar.types'\n\nconst ChevronUpIcon = ({ className }: { className?: string }) => (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n aria-hidden=\"true\"\n >\n <polyline points=\"18 15 12 9 6 15\" />\n </svg>\n)\n\nexport const StickyActionBar = forwardRef<HTMLDivElement, StickyActionBarProps>((props, ref) => {\n const {\n summaryContent,\n actionsContent,\n drawerContent,\n drawerTitle = 'Summary Details',\n position = 'fixed',\n containerMaxWidth = 'max-w-5xl',\n className,\n ...restProps\n } = props\n\n const [isDrawerOpen, setIsDrawerOpen] = useState(false)\n\n const positionClass =\n position === 'fixed'\n ? 'fixed bottom-0 left-0 right-0 z-30'\n : position === 'sticky'\n ? 'sticky bottom-0 z-30'\n : 'relative z-10'\n\n return (\n <>\n <div\n ref={ref}\n className={buildClassName(\n positionClass,\n 'border-t border-[var(--ui-border)] bg-white/90 p-3.5 backdrop-blur-md shadow-lg dark:bg-gray-900/90 transition-all',\n className,\n )}\n data-testid=\"sticky-action-bar\"\n {...restProps}\n >\n <div\n className={buildClassName(\n 'mx-auto flex items-center justify-between gap-3',\n containerMaxWidth,\n )}\n >\n {/* Summary Content Slot */}\n {summaryContent && (\n <div className=\"flex items-center gap-2\">\n {drawerContent ? (\n <button\n type=\"button\"\n onClick={() => setIsDrawerOpen(true)}\n className=\"flex items-center gap-2 text-left transition-opacity hover:opacity-80 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-primary-ring)] rounded-lg p-1 -m-1\"\n aria-label=\"Expand summary details\"\n data-testid=\"summary-trigger\"\n >\n <div>{summaryContent}</div>\n <ChevronUpIcon className=\"size-4 text-[var(--ui-primary)] dark:text-blue-400 shrink-0\" />\n </button>\n ) : (\n <div>{summaryContent}</div>\n )}\n </div>\n )}\n\n {/* Actions Stack Slot */}\n {actionsContent && (\n <div className=\"flex items-center gap-2 shrink-0 ml-auto\" data-testid=\"actions-stack\">\n {actionsContent}\n </div>\n )}\n </div>\n </div>\n\n {/* Optional Expandable Details Drawer */}\n {drawerContent && (\n <Drawer\n isOpen={isDrawerOpen}\n onClose={() => setIsDrawerOpen(false)}\n align=\"bottom\"\n size=\"full\"\n title={drawerTitle}\n showBackButton\n contentClass=\"px-4 pb-6 max-h-[calc(100svh-6rem)] overflow-y-auto\"\n >\n {drawerContent}\n </Drawer>\n )}\n </>\n )\n})\n\nStickyActionBar.displayName = 'StickyActionBar'\n"],"names":["_jsx","_jsxs","_Fragment"],"mappings":";;;;;AAKA,MAAM,aAAa,GAAG,CAAC,EAAE,SAAS,EAA0B,MAC1DA,GAAA,CAAA,KAAA,EAAA,EACE,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,GAAG,EACf,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,SAAS,EAAE,SAAS,EAAA,aAAA,EACR,MAAM,YAElBA,GAAA,CAAA,UAAA,EAAA,EAAU,MAAM,EAAC,iBAAiB,EAAA,CAAG,EAAA,CACjC,CACP;AAEM,MAAM,eAAe,GAAG,UAAU,CAAuC,CAAC,KAAK,EAAE,GAAG,KAAI;
|
|
1
|
+
{"version":3,"file":"StickyActionBar.js","sources":["../../../../../src/components/StickyActionBar/StickyActionBar.tsx"],"sourcesContent":["import { forwardRef, useState } from 'react'\nimport { buildClassName } from '../../utils/build-classname'\nimport { Drawer } from '../Drawer'\nimport type { StickyActionBarProps } from './StickyActionBar.types'\n\nconst ChevronUpIcon = ({ className }: { className?: string }) => (\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={className}\n aria-hidden=\"true\"\n >\n <polyline points=\"18 15 12 9 6 15\" />\n </svg>\n)\n\nexport const StickyActionBar = forwardRef<HTMLDivElement, StickyActionBarProps>((props, ref) => {\n const {\n summaryContent,\n actionsContent,\n drawerContent,\n drawerTitle = 'Summary Details',\n position = 'fixed',\n containerMaxWidth = 'max-w-5xl',\n safeArea = true,\n bottomOffset,\n className,\n style,\n ...restProps\n } = props\n\n const [isDrawerOpen, setIsDrawerOpen] = useState(false)\n\n const positionClass =\n position === 'fixed'\n ? 'fixed bottom-0 left-0 right-0 z-30'\n : position === 'sticky'\n ? 'sticky bottom-0 z-30'\n : 'relative z-10'\n\n return (\n <>\n <div\n ref={ref}\n className={buildClassName(\n positionClass,\n 'border-t border-[var(--ui-border)] bg-white/90 p-3.5 backdrop-blur-md shadow-lg dark:bg-gray-900/90 transition-all',\n safeArea && 'pb-[calc(0.875rem+env(safe-area-inset-bottom,0px))]',\n className,\n )}\n style={bottomOffset && position !== 'relative' ? { bottom: bottomOffset, ...style } : style}\n data-testid=\"sticky-action-bar\"\n {...restProps}\n >\n <div\n className={buildClassName(\n 'mx-auto flex items-center justify-between gap-3',\n containerMaxWidth,\n )}\n >\n {/* Summary Content Slot */}\n {summaryContent && (\n <div className=\"flex items-center gap-2\">\n {drawerContent ? (\n <button\n type=\"button\"\n onClick={() => setIsDrawerOpen(true)}\n className=\"flex items-center gap-2 text-left transition-opacity hover:opacity-80 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-primary-ring)] rounded-lg p-1 -m-1\"\n aria-label=\"Expand summary details\"\n data-testid=\"summary-trigger\"\n >\n <div>{summaryContent}</div>\n <ChevronUpIcon className=\"size-4 text-[var(--ui-primary)] dark:text-blue-400 shrink-0\" />\n </button>\n ) : (\n <div>{summaryContent}</div>\n )}\n </div>\n )}\n\n {/* Actions Stack Slot */}\n {actionsContent && (\n <div className=\"flex items-center gap-2 shrink-0 ml-auto\" data-testid=\"actions-stack\">\n {actionsContent}\n </div>\n )}\n </div>\n </div>\n\n {/* Optional Expandable Details Drawer */}\n {drawerContent && (\n <Drawer\n isOpen={isDrawerOpen}\n onClose={() => setIsDrawerOpen(false)}\n align=\"bottom\"\n size=\"full\"\n title={drawerTitle}\n showBackButton\n contentClass=\"px-4 pb-6 max-h-[calc(100svh-6rem)] overflow-y-auto\"\n >\n {drawerContent}\n </Drawer>\n )}\n </>\n )\n})\n\nStickyActionBar.displayName = 'StickyActionBar'\n"],"names":["_jsx","_jsxs","_Fragment"],"mappings":";;;;;AAKA,MAAM,aAAa,GAAG,CAAC,EAAE,SAAS,EAA0B,MAC1DA,GAAA,CAAA,KAAA,EAAA,EACE,OAAO,EAAC,WAAW,EACnB,IAAI,EAAC,MAAM,EACX,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,GAAG,EACf,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,SAAS,EAAE,SAAS,EAAA,aAAA,EACR,MAAM,YAElBA,GAAA,CAAA,UAAA,EAAA,EAAU,MAAM,EAAC,iBAAiB,EAAA,CAAG,EAAA,CACjC,CACP;AAEM,MAAM,eAAe,GAAG,UAAU,CAAuC,CAAC,KAAK,EAAE,GAAG,KAAI;AAC7F,IAAA,MAAM,EACJ,cAAc,EACd,cAAc,EACd,aAAa,EACb,WAAW,GAAG,iBAAiB,EAC/B,QAAQ,GAAG,OAAO,EAClB,iBAAiB,GAAG,WAAW,EAC/B,QAAQ,GAAG,IAAI,EACf,YAAY,EACZ,SAAS,EACT,KAAK,EACL,GAAG,SAAS,EACb,GAAG,KAAK;IAET,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC;AAEvD,IAAA,MAAM,aAAa,GACjB,QAAQ,KAAK;AACX,UAAE;UACA,QAAQ,KAAK;AACb,cAAE;cACA,eAAe;AAEvB,IAAA,QACEC,IAAA,CAAAC,QAAA,EAAA,EAAA,QAAA,EAAA,CACEF,GAAA,CAAA,KAAA,EAAA,EACE,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,cAAc,CACvB,aAAa,EACb,oHAAoH,EACpH,QAAQ,IAAI,qDAAqD,EACjE,SAAS,CACV,EACD,KAAK,EAAE,YAAY,IAAI,QAAQ,KAAK,UAAU,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,KAAK,EAAE,GAAG,KAAK,EAAA,aAAA,EAC/E,mBAAmB,EAAA,GAC3B,SAAS,YAEbC,IAAA,CAAA,KAAA,EAAA,EACE,SAAS,EAAE,cAAc,CACvB,iDAAiD,EACjD,iBAAiB,CAClB,EAAA,QAAA,EAAA,CAGA,cAAc,KACbD,GAAA,CAAA,KAAA,EAAA,EAAK,SAAS,EAAC,yBAAyB,EAAA,QAAA,EACrC,aAAa,IACZC,IAAA,CAAA,QAAA,EAAA,EACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,MAAM,eAAe,CAAC,IAAI,CAAC,EACpC,SAAS,EAAC,8LAA8L,EAAA,YAAA,EAC7L,wBAAwB,EAAA,aAAA,EACvB,iBAAiB,aAE7BD,GAAA,CAAA,KAAA,EAAA,EAAA,QAAA,EAAM,cAAc,GAAO,EAC3BA,GAAA,CAAC,aAAa,EAAA,EAAC,SAAS,EAAC,6DAA6D,GAAG,CAAA,EAAA,CAClF,KAETA,GAAA,CAAA,KAAA,EAAA,EAAA,QAAA,EAAM,cAAc,EAAA,CAAO,CAC5B,GACG,CACP,EAGA,cAAc,KACbA,aAAK,SAAS,EAAC,0CAA0C,EAAA,aAAA,EAAa,eAAe,YAClF,cAAc,EAAA,CACX,CACP,CAAA,EAAA,CACG,EAAA,CACF,EAGL,aAAa,KACZA,GAAA,CAAC,MAAM,EAAA,EACL,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,MAAM,eAAe,CAAC,KAAK,CAAC,EACrC,KAAK,EAAC,QAAQ,EACd,IAAI,EAAC,MAAM,EACX,KAAK,EAAE,WAAW,EAClB,cAAc,QACd,YAAY,EAAC,qDAAqD,EAAA,QAAA,EAEjE,aAAa,GACP,CACV,CAAA,EAAA,CACA;AAEP,CAAC;AAED,eAAe,CAAC,WAAW,GAAG,iBAAiB;;;;"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
2
|
+
|
|
3
|
+
const BREAKPOINT_QUERIES = {
|
|
4
|
+
medium: '(min-width: 768px)',
|
|
5
|
+
expanded: '(min-width: 1024px)',
|
|
6
|
+
};
|
|
7
|
+
// jsdom and some embedded webviews have no matchMedia; treat every query as
|
|
8
|
+
// unmatched there (the compact layout) rather than throwing.
|
|
9
|
+
const hasMatchMedia = () => typeof window !== 'undefined' && typeof window.matchMedia === 'function';
|
|
10
|
+
/** Live result of a CSS media query. `false` during server rendering or without `matchMedia`. */
|
|
11
|
+
const useMediaQuery = (query) => {
|
|
12
|
+
const subscribe = useCallback((onChange) => {
|
|
13
|
+
if (!hasMatchMedia())
|
|
14
|
+
return () => { };
|
|
15
|
+
const list = window.matchMedia(query);
|
|
16
|
+
list.addEventListener('change', onChange);
|
|
17
|
+
return () => list.removeEventListener('change', onChange);
|
|
18
|
+
}, [query]);
|
|
19
|
+
return useSyncExternalStore(subscribe, () => hasMatchMedia() && window.matchMedia(query).matches, () => false);
|
|
20
|
+
};
|
|
21
|
+
/** Current layout range; `compact` during server rendering. */
|
|
22
|
+
const useBreakpoint = () => {
|
|
23
|
+
const isMedium = useMediaQuery(BREAKPOINT_QUERIES.medium);
|
|
24
|
+
const isExpanded = useMediaQuery(BREAKPOINT_QUERIES.expanded);
|
|
25
|
+
if (isExpanded)
|
|
26
|
+
return 'expanded';
|
|
27
|
+
return isMedium ? 'medium' : 'compact';
|
|
28
|
+
};
|
|
29
|
+
/** Resolves a `ResponsiveValue` for the current viewport. */
|
|
30
|
+
const useResponsiveValue = (value) => {
|
|
31
|
+
var _a, _b, _c;
|
|
32
|
+
const breakpoint = useBreakpoint();
|
|
33
|
+
if (typeof value !== 'object' || value === null || !('base' in value))
|
|
34
|
+
return value;
|
|
35
|
+
if (breakpoint === 'expanded')
|
|
36
|
+
return (_b = (_a = value.lg) !== null && _a !== void 0 ? _a : value.md) !== null && _b !== void 0 ? _b : value.base;
|
|
37
|
+
if (breakpoint === 'medium')
|
|
38
|
+
return (_c = value.md) !== null && _c !== void 0 ? _c : value.base;
|
|
39
|
+
return value.base;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export { BREAKPOINT_QUERIES, useBreakpoint, useMediaQuery, useResponsiveValue };
|
|
43
|
+
//# sourceMappingURL=use-breakpoint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-breakpoint.js","sources":["../../../../src/hooks/use-breakpoint.tsx"],"sourcesContent":["import { useCallback, useSyncExternalStore } from 'react'\n\n/**\n * Layout ranges matching Tailwind's `md` (768px) and `lg` (1024px) tokens, so a\n * JS decision and an `md:`/`lg:` class always agree:\n * - `compact`: phones\n * - `medium`: tablets and narrow windows\n * - `expanded`: desktop\n *\n * Width only. Unlike `useIsMobile`, touch capability is never considered, so a\n * phone in landscape and a narrow desktop window get the layout for their size.\n */\nexport type Breakpoint = 'compact' | 'medium' | 'expanded'\n\nexport const BREAKPOINT_QUERIES = {\n medium: '(min-width: 768px)',\n expanded: '(min-width: 1024px)',\n} as const\n\n// jsdom and some embedded webviews have no matchMedia; treat every query as\n// unmatched there (the compact layout) rather than throwing.\nconst hasMatchMedia = () => typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n\n/** Live result of a CSS media query. `false` during server rendering or without `matchMedia`. */\nexport const useMediaQuery = (query: string): boolean => {\n const subscribe = useCallback(\n (onChange: () => void) => {\n if (!hasMatchMedia()) return () => {}\n const list = window.matchMedia(query)\n list.addEventListener('change', onChange)\n return () => list.removeEventListener('change', onChange)\n },\n [query],\n )\n\n return useSyncExternalStore(\n subscribe,\n () => hasMatchMedia() && window.matchMedia(query).matches,\n () => false,\n )\n}\n\n/** Current layout range; `compact` during server rendering. */\nexport const useBreakpoint = (): Breakpoint => {\n const isMedium = useMediaQuery(BREAKPOINT_QUERIES.medium)\n const isExpanded = useMediaQuery(BREAKPOINT_QUERIES.expanded)\n if (isExpanded) return 'expanded'\n return isMedium ? 'medium' : 'compact'\n}\n\n/** A value per breakpoint; missing ranges fall back to the next smaller one. */\nexport type ResponsiveValue<T> = T | { base: T; md?: T; lg?: T }\n\n/** Resolves a `ResponsiveValue` for the current viewport. */\nexport const useResponsiveValue = <T,>(value: ResponsiveValue<T>): T => {\n const breakpoint = useBreakpoint()\n if (typeof value !== 'object' || value === null || !('base' in value)) return value as T\n if (breakpoint === 'expanded') return value.lg ?? value.md ?? value.base\n if (breakpoint === 'medium') return value.md ?? value.base\n return value.base\n}\n"],"names":[],"mappings":";;AAcO,MAAM,kBAAkB,GAAG;AAChC,IAAA,MAAM,EAAE,oBAAoB;AAC5B,IAAA,QAAQ,EAAE,qBAAqB;;AAGjC;AACA;AACA,MAAM,aAAa,GAAG,MAAM,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU;AAEpG;AACO,MAAM,aAAa,GAAG,CAAC,KAAa,KAAa;AACtD,IAAA,MAAM,SAAS,GAAG,WAAW,CAC3B,CAAC,QAAoB,KAAI;QACvB,IAAI,CAAC,aAAa,EAAE;AAAE,YAAA,OAAO,MAAK,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC;AACrC,QAAA,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC;QACzC,OAAO,MAAM,IAAI,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC;AAC3D,IAAA,CAAC,EACD,CAAC,KAAK,CAAC,CACR;IAED,OAAO,oBAAoB,CACzB,SAAS,EACT,MAAM,aAAa,EAAE,IAAI,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,OAAO,EACzD,MAAM,KAAK,CACZ;AACH;AAEA;AACO,MAAM,aAAa,GAAG,MAAiB;IAC5C,MAAM,QAAQ,GAAG,aAAa,CAAC,kBAAkB,CAAC,MAAM,CAAC;IACzD,MAAM,UAAU,GAAG,aAAa,CAAC,kBAAkB,CAAC,QAAQ,CAAC;AAC7D,IAAA,IAAI,UAAU;AAAE,QAAA,OAAO,UAAU;IACjC,OAAO,QAAQ,GAAG,QAAQ,GAAG,SAAS;AACxC;AAKA;AACO,MAAM,kBAAkB,GAAG,CAAK,KAAyB,KAAO;;AACrE,IAAA,MAAM,UAAU,GAAG,aAAa,EAAE;AAClC,IAAA,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,EAAE,MAAM,IAAI,KAAK,CAAC;AAAE,QAAA,OAAO,KAAU;IACxF,IAAI,UAAU,KAAK,UAAU;AAAE,QAAA,OAAO,CAAA,EAAA,GAAA,CAAA,EAAA,GAAA,KAAK,CAAC,EAAE,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,KAAK,CAAC,EAAE,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,KAAK,CAAC,IAAI;IACxE,IAAI,UAAU,KAAK,QAAQ;QAAE,OAAO,CAAA,EAAA,GAAA,KAAK,CAAC,EAAE,mCAAI,KAAK,CAAC,IAAI;IAC1D,OAAO,KAAK,CAAC,IAAI;AACnB;;;;"}
|
package/dist/es/index.js
CHANGED
|
@@ -56,6 +56,7 @@ export { Textarea } from './components/Textarea/Textarea.js';
|
|
|
56
56
|
export { ToastProvider, useToast } from './components/Toast/ToastProvider.js';
|
|
57
57
|
export { Toast } from './components/Toast/Toast.js';
|
|
58
58
|
export { VirtualizedDataTable } from './components/VirtualizedDataTable/VirtualizedDataTable.js';
|
|
59
|
+
export { BREAKPOINT_QUERIES, useBreakpoint, useMediaQuery, useResponsiveValue } from './hooks/use-breakpoint.js';
|
|
59
60
|
export { useDebounce, useDebounceFn } from './hooks/use-debounce.js';
|
|
60
61
|
export { useIsMobile } from './hooks/use-is-mobile.js';
|
|
61
62
|
export { useIsTablet } from './hooks/use-is-tablet.js';
|
package/dist/es/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|