@mohasinac/appkit 3.5.3 → 3.5.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.
- package/dist/features/seller/components/CategoryInlineSelect.d.ts +1 -1
- package/dist/features/seller/components/CategoryInlineSelect.js +1 -1
- package/dist/react/hooks/useBulkSelection.js +12 -3
- package/dist/react/hooks/useGesture.js +48 -4
- package/dist/react/hooks/useLongPress.d.ts +1 -0
- package/dist/react/hooks/useLongPress.js +5 -0
- package/dist/react/hooks/usePullToRefresh.js +12 -1
- package/dist/react/hooks/useRealtimeEvent.js +18 -1
- package/dist/react/hooks/useSwipe.js +14 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/BaseListingCard.d.ts +2 -1
- package/dist/ui/components/BaseListingCard.js +4 -5
- package/dist/ui/components/Button.js +52 -22
- package/dist/ui/components/DateInput.js +1 -0
- package/dist/ui/components/DynamicBgDiv.js +8 -6
- package/dist/ui/components/FormField.js +1 -1
- package/dist/ui/components/HorizontalScroller.js +17 -6
- package/dist/ui/components/Iframe.d.ts +8 -1
- package/dist/ui/components/Layout.d.ts +4 -4
- package/dist/ui/components/Layout.js +8 -8
- package/dist/ui/components/Motion.js +4 -1
- package/dist/ui/components/OtpInput.js +1 -1
- package/dist/ui/components/PaginatedSelect.js +29 -3
- package/dist/ui/components/Pagination.js +10 -5
- package/dist/ui/components/RichTextEditor.js +64 -3
- package/dist/ui/components/Select.js +1 -0
- package/dist/ui/components/Semantic.d.ts +10 -10
- package/dist/ui/components/Semantic.js +25 -25
- package/dist/ui/components/SideDrawer.js +13 -11
- package/dist/ui/components/SideModal.js +9 -7
- package/dist/ui/components/SlottedListingView.d.ts +13 -2
- package/dist/ui/components/StickyToolbar.js +9 -5
- package/dist/ui/components/TagInput.js +15 -2
- package/dist/ui/components/Textarea.js +1 -0
- package/dist/ui/components/Toggle.js +8 -1
- package/dist/ui/components/UnsavedChangesModal.js +12 -1
- package/dist/ui/forms/ColorPickerField.js +7 -0
- package/dist/ui/forms/FieldCheckbox.js +1 -1
- package/dist/utils/id-generators.d.ts +7 -4
- package/dist/utils/id-generators.js +53 -31
- package/dist/utils/number.formatter.js +19 -6
- package/dist/utils/string.formatter.js +4 -1
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ export interface CategoryInlineSelectProps {
|
|
|
4
4
|
onChange: (v: string) => void;
|
|
5
5
|
placeholder?: string;
|
|
6
6
|
disabled?: boolean;
|
|
7
|
-
/** Show the "+ Create new category" option. Defaults to
|
|
7
|
+
/** Show the "+ Create new category" option. Defaults to true — sellers can create categories inline. */
|
|
8
8
|
allowCreate?: boolean;
|
|
9
9
|
}
|
|
10
10
|
export declare function CategoryInlineSelect({ value, onChange, placeholder, disabled, allowCreate, }: CategoryInlineSelectProps): React.JSX.Element;
|
|
@@ -35,7 +35,7 @@ async function loadPublicCategoryOptions(query, _page) {
|
|
|
35
35
|
const items = q ? all.filter((c) => c.label.toLowerCase().includes(q)) : all;
|
|
36
36
|
return { items, hasMore: false, nextPage: 2 };
|
|
37
37
|
}
|
|
38
|
-
export function CategoryInlineSelect({ value, onChange, placeholder = "Search categories…", disabled, allowCreate =
|
|
38
|
+
export function CategoryInlineSelect({ value, onChange, placeholder = "Search categories…", disabled, allowCreate = true, }) {
|
|
39
39
|
const loadOptions = allowCreate ? loadAdminCategoryOptions : loadPublicCategoryOptions;
|
|
40
40
|
return (_jsx(PaginatedSelect, { value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadOptions, placeholder: placeholder, disabled: disabled, createLabel: "category", drawerTitle: "Create Category", renderCreateForm: allowCreate
|
|
41
41
|
? ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel }))
|
|
@@ -19,7 +19,12 @@ export function useBulkSelection({ items, keyExtractor, maxSelection = BULK_MAX_
|
|
|
19
19
|
}, [maxSelection]);
|
|
20
20
|
const toggleAll = useCallback(() => {
|
|
21
21
|
setSelectedSet((prev) => {
|
|
22
|
-
|
|
22
|
+
// Compare actual membership, not just set size — `prev` can hold IDs
|
|
23
|
+
// from a page that's no longer rendered (e.g. after paginating), in
|
|
24
|
+
// which case the sizes coincidentally matching would wrongly report
|
|
25
|
+
// "all selected" and CLEAR the selection instead of selecting the
|
|
26
|
+
// currently-visible items.
|
|
27
|
+
const allSelected = allIds.length > 0 && allIds.every((id) => prev.has(id));
|
|
23
28
|
if (allSelected)
|
|
24
29
|
return new Set();
|
|
25
30
|
return new Set(allIds.slice(0, maxSelection));
|
|
@@ -33,8 +38,12 @@ export function useBulkSelection({ items, keyExtractor, maxSelection = BULK_MAX_
|
|
|
33
38
|
const selectedIds = useMemo(() => Array.from(selectedSet), [selectedSet]);
|
|
34
39
|
const selectedCount = selectedSet.size;
|
|
35
40
|
const isSelecting = selectedCount > 0;
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
// Membership-based, not cardinality-based — `selectedSet` can hold IDs
|
|
42
|
+
// from items that are no longer in `allIds` (e.g. the previous page),
|
|
43
|
+
// so comparing sizes alone can report "all selected" when none of the
|
|
44
|
+
// *currently rendered* rows are actually selected.
|
|
45
|
+
const isAllSelected = allIds.length > 0 && allIds.every((id) => selectedSet.has(id));
|
|
46
|
+
const isIndeterminate = !isAllSelected && allIds.some((id) => selectedSet.has(id));
|
|
38
47
|
return {
|
|
39
48
|
selectedIds,
|
|
40
49
|
selectedIdSet: selectedSet,
|
|
@@ -28,6 +28,12 @@ export function useGesture(ref, options = {}) {
|
|
|
28
28
|
const lastTapRef = useRef(0);
|
|
29
29
|
const initialPinchDistanceRef = useRef(0);
|
|
30
30
|
const initialRotationRef = useRef(0);
|
|
31
|
+
// Real devices lift two fingers in separate touchend events, so by the
|
|
32
|
+
// final touchend `e.changedTouches` only ever has one entry — tracking
|
|
33
|
+
// the last-known two-finger positions here lets us compute the true
|
|
34
|
+
// final pinch distance / rotation angle instead of pairing a touch with
|
|
35
|
+
// itself (which always yields 0).
|
|
36
|
+
const lastTwoTouchesRef = useRef(null);
|
|
31
37
|
// Store callbacks in refs to avoid event listener churn
|
|
32
38
|
const onTapRef = useRef(onTap);
|
|
33
39
|
const onDoubleTapRef = useRef(onDoubleTap);
|
|
@@ -67,6 +73,14 @@ export function useGesture(ref, options = {}) {
|
|
|
67
73
|
const dy = touch1.clientY - touch2.clientY;
|
|
68
74
|
return Math.atan2(dy, dx) * (180 / Math.PI);
|
|
69
75
|
};
|
|
76
|
+
const captureTwoTouches = (t1, t2) => {
|
|
77
|
+
lastTwoTouchesRef.current = {
|
|
78
|
+
x1: t1.clientX,
|
|
79
|
+
y1: t1.clientY,
|
|
80
|
+
x2: t2.clientX,
|
|
81
|
+
y2: t2.clientY,
|
|
82
|
+
};
|
|
83
|
+
};
|
|
70
84
|
const handleTouchStart = (e) => {
|
|
71
85
|
if (preventDefault)
|
|
72
86
|
e.preventDefault();
|
|
@@ -79,6 +93,7 @@ export function useGesture(ref, options = {}) {
|
|
|
79
93
|
if (e.touches.length === 2) {
|
|
80
94
|
initialPinchDistanceRef.current = getDistance(e.touches[0], e.touches[1]);
|
|
81
95
|
initialRotationRef.current = getAngle(e.touches[0], e.touches[1]);
|
|
96
|
+
captureTwoTouches(e.touches[0], e.touches[1]);
|
|
82
97
|
}
|
|
83
98
|
};
|
|
84
99
|
const handleTouchMove = (e) => {
|
|
@@ -91,6 +106,7 @@ export function useGesture(ref, options = {}) {
|
|
|
91
106
|
onRotatingRef.current)) {
|
|
92
107
|
if (preventDefault)
|
|
93
108
|
e.preventDefault();
|
|
109
|
+
captureTwoTouches(e.touches[0], e.touches[1]);
|
|
94
110
|
const currentDistance = getDistance(e.touches[0], e.touches[1]);
|
|
95
111
|
const currentAngle = getAngle(e.touches[0], e.touches[1]);
|
|
96
112
|
if (initialPinchDistanceRef.current > 0) {
|
|
@@ -122,16 +138,40 @@ export function useGesture(ref, options = {}) {
|
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
140
|
if (e.touches.length === 0 && initialPinchDistanceRef.current > 0) {
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
// `e.changedTouches` only has the LAST-lifted finger at this point
|
|
142
|
+
// (the other one already ended in a prior touchend event), so pair
|
|
143
|
+
// it against the last-known two-finger positions instead of
|
|
144
|
+
// self-pairing it (which always yields a distance of 0).
|
|
145
|
+
const last = lastTwoTouchesRef.current;
|
|
146
|
+
if (last) {
|
|
147
|
+
const currentDistance = getDistance({ clientX: last.x1, clientY: last.y1 }, { clientX: last.x2, clientY: last.y2 });
|
|
148
|
+
onPinchRef.current?.(currentDistance / initialPinchDistanceRef.current, currentDistance);
|
|
149
|
+
}
|
|
127
150
|
initialPinchDistanceRef.current = 0;
|
|
128
151
|
}
|
|
129
152
|
if (e.touches.length === 0 && initialRotationRef.current !== 0) {
|
|
130
|
-
const
|
|
131
|
-
|
|
153
|
+
const last = lastTwoTouchesRef.current;
|
|
154
|
+
if (last) {
|
|
155
|
+
const currentAngle = getAngle({ clientX: last.x1, clientY: last.y1 }, { clientX: last.x2, clientY: last.y2 });
|
|
156
|
+
onRotateRef.current?.(currentAngle - initialRotationRef.current);
|
|
157
|
+
}
|
|
132
158
|
initialRotationRef.current = 0;
|
|
133
159
|
}
|
|
160
|
+
if (e.touches.length === 0) {
|
|
161
|
+
lastTwoTouchesRef.current = null;
|
|
162
|
+
}
|
|
163
|
+
touchStartRef.current = null;
|
|
164
|
+
};
|
|
165
|
+
const handleTouchCancel = () => {
|
|
166
|
+
// OS/browser gesture takeover interrupted the touch sequence — reset
|
|
167
|
+
// all tracked state without firing any callback, matching the "quick
|
|
168
|
+
// taps don't fire" contract. Without this, stale pinch/rotation refs
|
|
169
|
+
// survive into the next unrelated tap and spuriously fire onPinch /
|
|
170
|
+
// onRotate for it.
|
|
134
171
|
touchStartRef.current = null;
|
|
172
|
+
initialPinchDistanceRef.current = 0;
|
|
173
|
+
initialRotationRef.current = 0;
|
|
174
|
+
lastTwoTouchesRef.current = null;
|
|
135
175
|
};
|
|
136
176
|
const handleMouseDown = (e) => {
|
|
137
177
|
if (preventDefault)
|
|
@@ -168,12 +208,16 @@ export function useGesture(ref, options = {}) {
|
|
|
168
208
|
element.addEventListener("touchend", handleTouchEnd, {
|
|
169
209
|
passive: !preventDefault,
|
|
170
210
|
});
|
|
211
|
+
element.addEventListener("touchcancel", handleTouchCancel, {
|
|
212
|
+
passive: true,
|
|
213
|
+
});
|
|
171
214
|
element.addEventListener("mousedown", handleMouseDown);
|
|
172
215
|
element.addEventListener("mouseup", handleMouseUp);
|
|
173
216
|
return () => {
|
|
174
217
|
element.removeEventListener("touchstart", handleTouchStart);
|
|
175
218
|
element.removeEventListener("touchmove", handleTouchMove);
|
|
176
219
|
element.removeEventListener("touchend", handleTouchEnd);
|
|
220
|
+
element.removeEventListener("touchcancel", handleTouchCancel);
|
|
177
221
|
element.removeEventListener("mousedown", handleMouseDown);
|
|
178
222
|
element.removeEventListener("mouseup", handleMouseUp);
|
|
179
223
|
};
|
|
@@ -32,5 +32,10 @@ export function useLongPress(callback, ms = 500) {
|
|
|
32
32
|
onMouseLeave: cancel,
|
|
33
33
|
onTouchStart: start,
|
|
34
34
|
onTouchEnd: cancel,
|
|
35
|
+
// Browsers fire touchcancel (not touchend) when a touch is reinterpreted
|
|
36
|
+
// as a scroll — common for cards in a scrollable list. Without this the
|
|
37
|
+
// pending timer survives and can fire the callback after what was really
|
|
38
|
+
// just a scroll, contradicting the "quick taps do NOT fire" contract.
|
|
39
|
+
onTouchCancel: cancel,
|
|
35
40
|
};
|
|
36
41
|
}
|
|
@@ -65,6 +65,15 @@ export function usePullToRefresh(onRefresh, options = {}) {
|
|
|
65
65
|
await onRefreshRef.current();
|
|
66
66
|
}
|
|
67
67
|
}, []);
|
|
68
|
+
const handleTouchCancel = useCallback(() => {
|
|
69
|
+
// Interrupted pull (OS/browser gesture takeover) — reset without
|
|
70
|
+
// triggering onRefresh. Without this, isPulling/progress stay frozen
|
|
71
|
+
// mid-animation since only touchend resets them.
|
|
72
|
+
setIsPulling(false);
|
|
73
|
+
setProgress(0);
|
|
74
|
+
startYRef.current = null;
|
|
75
|
+
thresholdReachedRef.current = false;
|
|
76
|
+
}, []);
|
|
68
77
|
useEffect(() => {
|
|
69
78
|
const el = containerRef.current;
|
|
70
79
|
if (!el)
|
|
@@ -72,11 +81,13 @@ export function usePullToRefresh(onRefresh, options = {}) {
|
|
|
72
81
|
el.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
73
82
|
el.addEventListener("touchmove", handleTouchMove, { passive: true });
|
|
74
83
|
el.addEventListener("touchend", handleTouchEnd);
|
|
84
|
+
el.addEventListener("touchcancel", handleTouchCancel, { passive: true });
|
|
75
85
|
return () => {
|
|
76
86
|
el.removeEventListener("touchstart", handleTouchStart);
|
|
77
87
|
el.removeEventListener("touchmove", handleTouchMove);
|
|
78
88
|
el.removeEventListener("touchend", handleTouchEnd);
|
|
89
|
+
el.removeEventListener("touchcancel", handleTouchCancel);
|
|
79
90
|
};
|
|
80
|
-
}, [handleTouchStart, handleTouchMove, handleTouchEnd]);
|
|
91
|
+
}, [handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel]);
|
|
81
92
|
return { containerRef, isPulling, progress };
|
|
82
93
|
}
|
|
@@ -32,6 +32,13 @@ const DEFAULT_MESSAGES = {
|
|
|
32
32
|
};
|
|
33
33
|
export function useRealtimeEvent(config) {
|
|
34
34
|
const configRef = useRef(config);
|
|
35
|
+
// Re-sync every render — callers commonly pass a fresh config object
|
|
36
|
+
// (new extractData/messages/onLogError) each render, and subscribe()/
|
|
37
|
+
// cleanup() only ever read configRef.current, so without this the very
|
|
38
|
+
// first render's config would be captured forever.
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
configRef.current = config;
|
|
41
|
+
});
|
|
35
42
|
const [status, setStatus] = useState(RealtimeEventStatus.IDLE);
|
|
36
43
|
const [error, setError] = useState(null);
|
|
37
44
|
const [data, setData] = useState(null);
|
|
@@ -71,13 +78,23 @@ export function useRealtimeEvent(config) {
|
|
|
71
78
|
catch (authErr) {
|
|
72
79
|
void normalizeError(authErr);
|
|
73
80
|
onLogError?.(`useRealtimeEvent[${type}]: custom token sign-in failed`, authErr);
|
|
81
|
+
// A newer subscribe() call for a different eventId may have already
|
|
82
|
+
// superseded this one (and possibly already fully subscribed) while
|
|
83
|
+
// this sign-in was still in flight. Do NOT call cleanup() here —
|
|
84
|
+
// cleanup() tears down whatever is *currently* registered in the
|
|
85
|
+
// shared refs, which by now may be the newer call's live
|
|
86
|
+
// subscription, not anything this stale call ever set up itself.
|
|
87
|
+
if (eventIdRef.current !== eventId) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
74
90
|
setError(msg.tokenFailure);
|
|
75
91
|
setStatus(RealtimeEventStatus.FAILED);
|
|
76
92
|
cleanup();
|
|
77
93
|
return;
|
|
78
94
|
}
|
|
79
95
|
if (eventIdRef.current !== eventId) {
|
|
80
|
-
|
|
96
|
+
// Same reasoning as the catch branch above — don't tear down a
|
|
97
|
+
// newer call's already-live subscription via the shared refs.
|
|
81
98
|
return;
|
|
82
99
|
}
|
|
83
100
|
setStatus(RealtimeEventStatus.PENDING);
|
|
@@ -118,6 +118,16 @@ export function useSwipe(ref, options = {}) {
|
|
|
118
118
|
isSwiping.current = false;
|
|
119
119
|
onSwipeEndRef.current?.();
|
|
120
120
|
};
|
|
121
|
+
const handleTouchCancel = () => {
|
|
122
|
+
// OS/browser gesture takeover interrupted the touch — without this,
|
|
123
|
+
// isSwiping/touchStartRef stay stuck and onSwipeEnd (which callers
|
|
124
|
+
// rely on to clear a "dragging" UI flag) never fires.
|
|
125
|
+
if (!isSwiping.current)
|
|
126
|
+
return;
|
|
127
|
+
touchStartRef.current = null;
|
|
128
|
+
isSwiping.current = false;
|
|
129
|
+
onSwipeEndRef.current?.();
|
|
130
|
+
};
|
|
121
131
|
const handleMouseDown = (e) => {
|
|
122
132
|
if (preventDefault)
|
|
123
133
|
e.preventDefault();
|
|
@@ -149,6 +159,9 @@ export function useSwipe(ref, options = {}) {
|
|
|
149
159
|
element.addEventListener("touchend", handleTouchEnd, {
|
|
150
160
|
passive: !preventDefault,
|
|
151
161
|
});
|
|
162
|
+
element.addEventListener("touchcancel", handleTouchCancel, {
|
|
163
|
+
passive: true,
|
|
164
|
+
});
|
|
152
165
|
element.addEventListener("mousedown", handleMouseDown);
|
|
153
166
|
document.addEventListener("mousemove", handleMouseMove);
|
|
154
167
|
document.addEventListener("mouseup", handleMouseUp);
|
|
@@ -156,6 +169,7 @@ export function useSwipe(ref, options = {}) {
|
|
|
156
169
|
element.removeEventListener("touchstart", handleTouchStart);
|
|
157
170
|
element.removeEventListener("touchmove", handleTouchMove);
|
|
158
171
|
element.removeEventListener("touchend", handleTouchEnd);
|
|
172
|
+
element.removeEventListener("touchcancel", handleTouchCancel);
|
|
159
173
|
element.removeEventListener("mousedown", handleMouseDown);
|
|
160
174
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
161
175
|
document.removeEventListener("mouseup", handleMouseUp);
|