@almadar/ui 2.13.1 → 2.13.3

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.
@@ -1,385 +1,171 @@
1
1
  import { useEventBus } from './chunk-YXZM3WCF.js';
2
2
  import { en_default } from './chunk-2XXSUIOK.js';
3
- import React2, { createContext, useState, useCallback, useEffect, useContext, useMemo, useRef } from 'react';
3
+ import React2, { createContext, useRef, useEffect, useCallback, useContext, useState, useMemo } from 'react';
4
4
  import { jsx } from 'react/jsx-runtime';
5
5
 
6
- var SelectionContext = createContext(null);
7
- var defaultCompareEntities = (a, b) => {
8
- if (a === b) return true;
9
- if (!a || !b) return false;
10
- if (typeof a === "object" && typeof b === "object") {
11
- const aId = a.id;
12
- const bId = b.id;
13
- return aId !== void 0 && aId === bId;
14
- }
15
- return false;
16
- };
17
- function SelectionProvider({
18
- children,
19
- debug = false,
20
- compareEntities = defaultCompareEntities
21
- }) {
22
- const eventBus = useEventBus();
23
- const [selected, setSelectedState] = useState(null);
24
- const setSelected = useCallback(
25
- (entity) => {
26
- setSelectedState(entity);
27
- if (debug) {
28
- console.log("[SelectionProvider] Selection set:", entity);
29
- }
30
- },
31
- [debug]
32
- );
33
- const clearSelection = useCallback(() => {
34
- setSelectedState(null);
35
- if (debug) {
36
- console.log("[SelectionProvider] Selection cleared");
37
- }
38
- }, [debug]);
39
- const isSelected = useCallback(
40
- (entity) => {
41
- return compareEntities(selected, entity);
42
- },
43
- [selected, compareEntities]
44
- );
6
+ function useInfiniteScroll(onLoadMore, options = {}) {
7
+ const { rootMargin = "200px", hasMore = true, isLoading = false } = options;
8
+ const observerRef = useRef(null);
9
+ const callbackRef = useRef(onLoadMore);
10
+ callbackRef.current = onLoadMore;
11
+ const hasMoreRef = useRef(hasMore);
12
+ hasMoreRef.current = hasMore;
13
+ const isLoadingRef = useRef(isLoading);
14
+ isLoadingRef.current = isLoading;
45
15
  useEffect(() => {
46
- const handleSelect = (event) => {
47
- const row = event.payload?.row;
48
- if (row) {
49
- setSelected(row);
50
- if (debug) {
51
- console.log(`[SelectionProvider] ${event.type} received:`, row);
52
- }
53
- }
54
- };
55
- const handleDeselect = (event) => {
56
- clearSelection();
57
- if (debug) {
58
- console.log(`[SelectionProvider] ${event.type} received - clearing selection`);
59
- }
60
- };
61
- const unsubView = eventBus.on("UI:VIEW", handleSelect);
62
- const unsubSelect = eventBus.on("UI:SELECT", handleSelect);
63
- const unsubClose = eventBus.on("UI:CLOSE", handleDeselect);
64
- const unsubDeselect = eventBus.on("UI:DESELECT", handleDeselect);
65
- const unsubCancel = eventBus.on("UI:CANCEL", handleDeselect);
66
16
  return () => {
67
- unsubView();
68
- unsubSelect();
69
- unsubClose();
70
- unsubDeselect();
71
- unsubCancel();
17
+ observerRef.current?.disconnect();
72
18
  };
73
- }, [eventBus, setSelected, clearSelection, debug]);
74
- const contextValue = {
75
- selected,
76
- setSelected,
77
- clearSelection,
78
- isSelected
19
+ }, []);
20
+ const sentinelRef = useCallback((node) => {
21
+ observerRef.current?.disconnect();
22
+ if (!node) return;
23
+ observerRef.current = new IntersectionObserver(
24
+ (entries) => {
25
+ const entry = entries[0];
26
+ if (entry.isIntersecting && hasMoreRef.current && !isLoadingRef.current) {
27
+ callbackRef.current();
28
+ }
29
+ },
30
+ { rootMargin }
31
+ );
32
+ observerRef.current.observe(node);
33
+ }, [rootMargin]);
34
+ return { sentinelRef };
35
+ }
36
+ var { $meta: _meta, ...coreMessages } = en_default;
37
+ var coreLocale = coreMessages;
38
+ var I18nContext = createContext({
39
+ locale: "en",
40
+ direction: "ltr",
41
+ t: (key) => coreLocale[key] ?? key
42
+ // core locale fallback
43
+ });
44
+ I18nContext.displayName = "I18nContext";
45
+ var I18nProvider = I18nContext.Provider;
46
+ function useTranslate() {
47
+ return useContext(I18nContext);
48
+ }
49
+ function createTranslate(messages) {
50
+ return (key, params) => {
51
+ let msg = messages[key] ?? coreLocale[key] ?? key;
52
+ if (params) {
53
+ for (const [k, v] of Object.entries(params)) {
54
+ msg = msg.split(`{{${k}}}`).join(String(v));
55
+ }
56
+ }
57
+ return msg;
79
58
  };
80
- return /* @__PURE__ */ jsx(SelectionContext.Provider, { value: contextValue, children });
81
59
  }
82
- function useSelection() {
83
- const context = useContext(SelectionContext);
84
- if (!context) {
85
- throw new Error("useSelection must be used within a SelectionProvider");
60
+ var queryStores = /* @__PURE__ */ new Map();
61
+ function getOrCreateStore(query) {
62
+ if (!queryStores.has(query)) {
63
+ queryStores.set(query, {
64
+ search: "",
65
+ filters: {},
66
+ sortField: void 0,
67
+ sortDirection: void 0,
68
+ listeners: /* @__PURE__ */ new Set()
69
+ });
86
70
  }
87
- return context;
88
- }
89
- function useSelectionOptional() {
90
- const context = useContext(SelectionContext);
91
- return context;
71
+ return queryStores.get(query);
92
72
  }
93
- var EntityDataContext = createContext(null);
94
- function EntityDataProvider({
95
- adapter,
96
- children
97
- }) {
98
- return React2.createElement(
99
- EntityDataContext.Provider,
100
- { value: adapter },
101
- children
102
- );
73
+ function useQuerySingleton(query) {
74
+ const [, forceUpdate] = useState({});
75
+ if (!query) {
76
+ return null;
77
+ }
78
+ const store = useMemo(() => getOrCreateStore(query), [query]);
79
+ useMemo(() => {
80
+ const listener = () => forceUpdate({});
81
+ store.listeners.add(listener);
82
+ return () => {
83
+ store.listeners.delete(listener);
84
+ };
85
+ }, [store]);
86
+ const notifyListeners = useCallback(() => {
87
+ store.listeners.forEach((listener) => listener());
88
+ }, [store]);
89
+ const setSearch = useCallback((value) => {
90
+ store.search = value;
91
+ notifyListeners();
92
+ }, [store, notifyListeners]);
93
+ const setFilter = useCallback((key, value) => {
94
+ store.filters = { ...store.filters, [key]: value };
95
+ notifyListeners();
96
+ }, [store, notifyListeners]);
97
+ const clearFilters = useCallback(() => {
98
+ store.filters = {};
99
+ store.search = "";
100
+ notifyListeners();
101
+ }, [store, notifyListeners]);
102
+ const setSort = useCallback((field, direction) => {
103
+ store.sortField = field;
104
+ store.sortDirection = direction;
105
+ notifyListeners();
106
+ }, [store, notifyListeners]);
107
+ return {
108
+ search: store.search,
109
+ setSearch,
110
+ filters: store.filters,
111
+ setFilter,
112
+ clearFilters,
113
+ sortField: store.sortField,
114
+ sortDirection: store.sortDirection,
115
+ setSort
116
+ };
103
117
  }
104
- function useEntityDataAdapter() {
105
- return useContext(EntityDataContext);
118
+ function parseQueryBinding(binding) {
119
+ const cleaned = binding.startsWith("@") ? binding.slice(1) : binding;
120
+ const parts = cleaned.split(".");
121
+ return {
122
+ query: parts[0],
123
+ field: parts.length > 1 ? parts.slice(1).join(".") : void 0
124
+ };
106
125
  }
107
- var entityDataKeys = {
108
- all: ["entities"],
109
- lists: () => [...entityDataKeys.all, "list"],
110
- list: (entity, filters) => [...entityDataKeys.lists(), entity, filters],
111
- details: () => [...entityDataKeys.all, "detail"],
112
- detail: (entity, id) => [...entityDataKeys.details(), entity, id]
113
- };
114
- function useEntityList(entity, options = {}) {
115
- const { skip = false } = options;
116
- const adapter = useContext(EntityDataContext);
117
- const adapterData = useMemo(() => {
118
- if (!adapter || !entity || skip) return [];
119
- return adapter.getData(entity);
120
- }, [adapter, entity, skip, adapter?.isLoading]);
121
- const [stubData, setStubData] = useState([]);
122
- const [stubLoading, setStubLoading] = useState(!skip && !!entity && !adapter);
123
- const [stubError, setStubError] = useState(null);
124
- useEffect(() => {
125
- if (adapter || skip || !entity) {
126
- setStubLoading(false);
127
- return;
126
+ function useLongPress(onLongPress, options = {}) {
127
+ const { duration = 500, moveThreshold = 10 } = options;
128
+ const timerRef = useRef(null);
129
+ const startPos = useRef({ x: 0, y: 0 });
130
+ const isPressedRef = useRef(false);
131
+ const firedRef = useRef(false);
132
+ const cancel = useCallback(() => {
133
+ if (timerRef.current) {
134
+ clearTimeout(timerRef.current);
135
+ timerRef.current = null;
128
136
  }
129
- setStubLoading(true);
130
- const t = setTimeout(() => {
131
- setStubData([]);
132
- setStubLoading(false);
133
- }, 100);
134
- return () => clearTimeout(t);
135
- }, [entity, skip, adapter]);
136
- if (adapter) {
137
- return {
138
- data: adapterData,
139
- isLoading: adapter.isLoading,
140
- error: adapter.error ? new Error(adapter.error) : null,
141
- refetch: () => {
142
- }
143
- };
144
- }
145
- return { data: stubData, isLoading: stubLoading, error: stubError, refetch: () => {
146
- } };
147
- }
148
- function useEntity(entity, id) {
149
- const adapter = useContext(EntityDataContext);
150
- const adapterData = useMemo(() => {
151
- if (!adapter || !entity || !id) return null;
152
- return adapter.getById(entity, id) ?? null;
153
- }, [adapter, entity, id, adapter?.isLoading]);
154
- const [stubData, setStubData] = useState(null);
155
- const [stubLoading, setStubLoading] = useState(!!entity && !!id && !adapter);
156
- const [stubError, setStubError] = useState(null);
157
- useEffect(() => {
158
- if (adapter || !entity || !id) {
159
- setStubLoading(false);
160
- return;
161
- }
162
- setStubLoading(true);
163
- const t = setTimeout(() => {
164
- setStubData(null);
165
- setStubLoading(false);
166
- }, 100);
167
- return () => clearTimeout(t);
168
- }, [entity, id, adapter]);
169
- if (adapter) {
170
- return {
171
- data: adapterData,
172
- isLoading: adapter.isLoading,
173
- error: adapter.error ? new Error(adapter.error) : null
174
- };
175
- }
176
- return { data: stubData, isLoading: stubLoading, error: stubError };
177
- }
178
- function useEntityDetail(entity, id) {
179
- const result = useEntity(entity, id);
180
- return { ...result, refetch: () => {
181
- } };
182
- }
183
- var suspenseCache = /* @__PURE__ */ new Map();
184
- function getSuspenseCacheKey(entity, type, id) {
185
- return id ? `${type}:${entity}:${id}` : `${type}:${entity}`;
186
- }
187
- function useEntityListSuspense(entity) {
188
- const adapter = useContext(EntityDataContext);
189
- if (adapter) {
190
- if (adapter.isLoading) {
191
- const cacheKey2 = getSuspenseCacheKey(entity, "list");
192
- let entry2 = suspenseCache.get(cacheKey2);
193
- if (!entry2 || entry2.status === "resolved") {
194
- let resolve;
195
- const promise = new Promise((r) => {
196
- resolve = r;
197
- });
198
- entry2 = { promise, status: "pending" };
199
- suspenseCache.set(cacheKey2, entry2);
200
- const check = setInterval(() => {
201
- if (!adapter.isLoading) {
202
- clearInterval(check);
203
- entry2.status = "resolved";
204
- resolve();
205
- }
206
- }, 50);
207
- }
208
- if (entry2.status === "pending") {
209
- throw entry2.promise;
210
- }
211
- }
212
- if (adapter.error) {
213
- throw new Error(adapter.error);
214
- }
215
- return {
216
- data: adapter.getData(entity),
217
- refetch: () => {
218
- }
219
- };
220
- }
221
- const cacheKey = getSuspenseCacheKey(entity, "list");
222
- let entry = suspenseCache.get(cacheKey);
223
- if (!entry) {
224
- let resolve;
225
- const promise = new Promise((r) => {
226
- resolve = r;
227
- setTimeout(() => {
228
- entry.status = "resolved";
229
- resolve();
230
- }, 100);
231
- });
232
- entry = { promise, status: "pending" };
233
- suspenseCache.set(cacheKey, entry);
234
- }
235
- if (entry.status === "pending") {
236
- throw entry.promise;
237
- }
238
- return { data: [], refetch: () => {
239
- } };
240
- }
241
- function useEntitySuspense(entity, id) {
242
- const adapter = useContext(EntityDataContext);
243
- if (adapter) {
244
- if (adapter.isLoading) {
245
- const cacheKey2 = getSuspenseCacheKey(entity, "detail", id);
246
- let entry2 = suspenseCache.get(cacheKey2);
247
- if (!entry2 || entry2.status === "resolved") {
248
- let resolve;
249
- const promise = new Promise((r) => {
250
- resolve = r;
251
- });
252
- entry2 = { promise, status: "pending" };
253
- suspenseCache.set(cacheKey2, entry2);
254
- const check = setInterval(() => {
255
- if (!adapter.isLoading) {
256
- clearInterval(check);
257
- entry2.status = "resolved";
258
- resolve();
259
- }
260
- }, 50);
261
- }
262
- if (entry2.status === "pending") {
263
- throw entry2.promise;
264
- }
265
- }
266
- if (adapter.error) {
267
- throw new Error(adapter.error);
137
+ isPressedRef.current = false;
138
+ }, []);
139
+ const onPointerDown = useCallback((e) => {
140
+ firedRef.current = false;
141
+ startPos.current = { x: e.clientX, y: e.clientY };
142
+ isPressedRef.current = true;
143
+ timerRef.current = setTimeout(() => {
144
+ firedRef.current = true;
145
+ isPressedRef.current = false;
146
+ onLongPress();
147
+ }, duration);
148
+ }, [duration, onLongPress]);
149
+ const onPointerMove = useCallback((e) => {
150
+ if (!isPressedRef.current) return;
151
+ const dx = e.clientX - startPos.current.x;
152
+ const dy = e.clientY - startPos.current.y;
153
+ if (Math.sqrt(dx * dx + dy * dy) > moveThreshold) {
154
+ cancel();
268
155
  }
269
- return {
270
- data: adapter.getById(entity, id) ?? null,
271
- refetch: () => {
272
- }
273
- };
274
- }
275
- const cacheKey = getSuspenseCacheKey(entity, "detail", id);
276
- let entry = suspenseCache.get(cacheKey);
277
- if (!entry) {
278
- let resolve;
279
- const promise = new Promise((r) => {
280
- resolve = r;
281
- setTimeout(() => {
282
- entry.status = "resolved";
283
- resolve();
284
- }, 100);
285
- });
286
- entry = { promise, status: "pending" };
287
- suspenseCache.set(cacheKey, entry);
288
- }
289
- if (entry.status === "pending") {
290
- throw entry.promise;
291
- }
292
- return { data: null, refetch: () => {
293
- } };
294
- }
295
- var queryStores = /* @__PURE__ */ new Map();
296
- function getOrCreateStore(query) {
297
- if (!queryStores.has(query)) {
298
- queryStores.set(query, {
299
- search: "",
300
- filters: {},
301
- sortField: void 0,
302
- sortDirection: void 0,
303
- listeners: /* @__PURE__ */ new Set()
304
- });
305
- }
306
- return queryStores.get(query);
307
- }
308
- function useQuerySingleton(query) {
309
- const [, forceUpdate] = useState({});
310
- if (!query) {
311
- return null;
312
- }
313
- const store = useMemo(() => getOrCreateStore(query), [query]);
314
- useMemo(() => {
315
- const listener = () => forceUpdate({});
316
- store.listeners.add(listener);
317
- return () => {
318
- store.listeners.delete(listener);
319
- };
320
- }, [store]);
321
- const notifyListeners = useCallback(() => {
322
- store.listeners.forEach((listener) => listener());
323
- }, [store]);
324
- const setSearch = useCallback((value) => {
325
- store.search = value;
326
- notifyListeners();
327
- }, [store, notifyListeners]);
328
- const setFilter = useCallback((key, value) => {
329
- store.filters = { ...store.filters, [key]: value };
330
- notifyListeners();
331
- }, [store, notifyListeners]);
332
- const clearFilters = useCallback(() => {
333
- store.filters = {};
334
- store.search = "";
335
- notifyListeners();
336
- }, [store, notifyListeners]);
337
- const setSort = useCallback((field, direction) => {
338
- store.sortField = field;
339
- store.sortDirection = direction;
340
- notifyListeners();
341
- }, [store, notifyListeners]);
342
- return {
343
- search: store.search,
344
- setSearch,
345
- filters: store.filters,
346
- setFilter,
347
- clearFilters,
348
- sortField: store.sortField,
349
- sortDirection: store.sortDirection,
350
- setSort
351
- };
352
- }
353
- function parseQueryBinding(binding) {
354
- const cleaned = binding.startsWith("@") ? binding.slice(1) : binding;
355
- const parts = cleaned.split(".");
156
+ }, [moveThreshold, cancel]);
157
+ const onPointerUp = useCallback(() => {
158
+ cancel();
159
+ }, [cancel]);
160
+ const onPointerCancel = useCallback(() => {
161
+ cancel();
162
+ }, [cancel]);
356
163
  return {
357
- query: parts[0],
358
- field: parts.length > 1 ? parts.slice(1).join(".") : void 0
359
- };
360
- }
361
- var { $meta: _meta, ...coreMessages } = en_default;
362
- var coreLocale = coreMessages;
363
- var I18nContext = createContext({
364
- locale: "en",
365
- direction: "ltr",
366
- t: (key) => coreLocale[key] ?? key
367
- // core locale fallback
368
- });
369
- I18nContext.displayName = "I18nContext";
370
- var I18nProvider = I18nContext.Provider;
371
- function useTranslate() {
372
- return useContext(I18nContext);
373
- }
374
- function createTranslate(messages) {
375
- return (key, params) => {
376
- let msg = messages[key] ?? coreLocale[key] ?? key;
377
- if (params) {
378
- for (const [k, v] of Object.entries(params)) {
379
- msg = msg.split(`{{${k}}}`).join(String(v));
380
- }
381
- }
382
- return msg;
164
+ onPointerDown,
165
+ onPointerMove,
166
+ onPointerUp,
167
+ onPointerCancel,
168
+ isPressed: isPressedRef.current
383
169
  };
384
170
  }
385
171
  function useSwipeGesture(callbacks, options = {}) {
@@ -444,51 +230,6 @@ function useSwipeGesture(callbacks, options = {}) {
444
230
  isSwiping: isSwipingRef.current
445
231
  };
446
232
  }
447
- function useLongPress(onLongPress, options = {}) {
448
- const { duration = 500, moveThreshold = 10 } = options;
449
- const timerRef = useRef(null);
450
- const startPos = useRef({ x: 0, y: 0 });
451
- const isPressedRef = useRef(false);
452
- const firedRef = useRef(false);
453
- const cancel = useCallback(() => {
454
- if (timerRef.current) {
455
- clearTimeout(timerRef.current);
456
- timerRef.current = null;
457
- }
458
- isPressedRef.current = false;
459
- }, []);
460
- const onPointerDown = useCallback((e) => {
461
- firedRef.current = false;
462
- startPos.current = { x: e.clientX, y: e.clientY };
463
- isPressedRef.current = true;
464
- timerRef.current = setTimeout(() => {
465
- firedRef.current = true;
466
- isPressedRef.current = false;
467
- onLongPress();
468
- }, duration);
469
- }, [duration, onLongPress]);
470
- const onPointerMove = useCallback((e) => {
471
- if (!isPressedRef.current) return;
472
- const dx = e.clientX - startPos.current.x;
473
- const dy = e.clientY - startPos.current.y;
474
- if (Math.sqrt(dx * dx + dy * dy) > moveThreshold) {
475
- cancel();
476
- }
477
- }, [moveThreshold, cancel]);
478
- const onPointerUp = useCallback(() => {
479
- cancel();
480
- }, [cancel]);
481
- const onPointerCancel = useCallback(() => {
482
- cancel();
483
- }, [cancel]);
484
- return {
485
- onPointerDown,
486
- onPointerMove,
487
- onPointerUp,
488
- onPointerCancel,
489
- isPressed: isPressedRef.current
490
- };
491
- }
492
233
  function useDragReorder(initialItems, onReorder) {
493
234
  const [items, setItems] = useState(initialItems);
494
235
  const [dragIndex, setDragIndex] = useState(-1);
@@ -547,105 +288,364 @@ function useDragReorder(initialItems, onReorder) {
547
288
  transition: isDragging ? "transform 150ms ease" : void 0,
548
289
  transform: isDragging && dragOverIndex >= 0 ? index === dragIndex ? "scale(1.02)" : index > dragIndex && index <= dragOverIndex ? "translateY(-100%)" : index < dragIndex && index >= dragOverIndex ? "translateY(100%)" : void 0 : void 0
549
290
  }
550
- }), [handleDragMove, handleDragEnd, dragIndex, dragOverIndex, isDragging]);
551
- return {
552
- items,
553
- dragIndex,
554
- dragOverIndex,
555
- isDragging,
556
- getDragHandleProps,
557
- getItemProps
558
- };
291
+ }), [handleDragMove, handleDragEnd, dragIndex, dragOverIndex, isDragging]);
292
+ return {
293
+ items,
294
+ dragIndex,
295
+ dragOverIndex,
296
+ isDragging,
297
+ getDragHandleProps,
298
+ getItemProps
299
+ };
300
+ }
301
+ function usePullToRefresh(onRefresh, options = {}) {
302
+ const { threshold = 60, maxPull = 120 } = options;
303
+ const [pullDistance, setPullDistance] = useState(0);
304
+ const [isPulling, setIsPulling] = useState(false);
305
+ const [isRefreshing, setIsRefreshing] = useState(false);
306
+ const startY = useRef(0);
307
+ const scrollTopRef = useRef(0);
308
+ const onTouchStart = useCallback((e) => {
309
+ const container = e.currentTarget;
310
+ scrollTopRef.current = container.scrollTop;
311
+ if (scrollTopRef.current <= 0) {
312
+ startY.current = e.touches[0].clientY;
313
+ setIsPulling(true);
314
+ }
315
+ }, []);
316
+ const onTouchMove = useCallback((e) => {
317
+ if (!isPulling || isRefreshing) return;
318
+ const container = e.currentTarget;
319
+ if (container.scrollTop > 0) {
320
+ setPullDistance(0);
321
+ return;
322
+ }
323
+ const dy = e.touches[0].clientY - startY.current;
324
+ if (dy > 0) {
325
+ const distance = Math.min(dy * 0.5, maxPull);
326
+ setPullDistance(distance);
327
+ }
328
+ }, [isPulling, isRefreshing, maxPull]);
329
+ const onTouchEnd = useCallback(() => {
330
+ if (!isPulling) return;
331
+ setIsPulling(false);
332
+ if (pullDistance >= threshold && !isRefreshing) {
333
+ setIsRefreshing(true);
334
+ setPullDistance(threshold);
335
+ onRefresh();
336
+ } else {
337
+ setPullDistance(0);
338
+ }
339
+ }, [isPulling, pullDistance, threshold, isRefreshing, onRefresh]);
340
+ const endRefresh = useCallback(() => {
341
+ setIsRefreshing(false);
342
+ setPullDistance(0);
343
+ }, []);
344
+ const containerProps = {
345
+ onTouchStart,
346
+ onTouchMove,
347
+ onTouchEnd,
348
+ style: {
349
+ transform: pullDistance > 0 ? `translateY(${pullDistance}px)` : void 0,
350
+ transition: isPulling ? "none" : "transform 300ms ease-out"
351
+ }
352
+ };
353
+ return {
354
+ pullDistance,
355
+ isPulling,
356
+ isRefreshing,
357
+ containerProps,
358
+ endRefresh
359
+ };
360
+ }
361
+ var SelectionContext = createContext(null);
362
+ var defaultCompareEntities = (a, b) => {
363
+ if (a === b) return true;
364
+ if (!a || !b) return false;
365
+ if (typeof a === "object" && typeof b === "object") {
366
+ const aId = a.id;
367
+ const bId = b.id;
368
+ return aId !== void 0 && aId === bId;
369
+ }
370
+ return false;
371
+ };
372
+ function SelectionProvider({
373
+ children,
374
+ debug = false,
375
+ compareEntities = defaultCompareEntities
376
+ }) {
377
+ const eventBus = useEventBus();
378
+ const [selected, setSelectedState] = useState(null);
379
+ const setSelected = useCallback(
380
+ (entity) => {
381
+ setSelectedState(entity);
382
+ if (debug) {
383
+ console.log("[SelectionProvider] Selection set:", entity);
384
+ }
385
+ },
386
+ [debug]
387
+ );
388
+ const clearSelection = useCallback(() => {
389
+ setSelectedState(null);
390
+ if (debug) {
391
+ console.log("[SelectionProvider] Selection cleared");
392
+ }
393
+ }, [debug]);
394
+ const isSelected = useCallback(
395
+ (entity) => {
396
+ return compareEntities(selected, entity);
397
+ },
398
+ [selected, compareEntities]
399
+ );
400
+ useEffect(() => {
401
+ const handleSelect = (event) => {
402
+ const row = event.payload?.row;
403
+ if (row) {
404
+ setSelected(row);
405
+ if (debug) {
406
+ console.log(`[SelectionProvider] ${event.type} received:`, row);
407
+ }
408
+ }
409
+ };
410
+ const handleDeselect = (event) => {
411
+ clearSelection();
412
+ if (debug) {
413
+ console.log(`[SelectionProvider] ${event.type} received - clearing selection`);
414
+ }
415
+ };
416
+ const unsubView = eventBus.on("UI:VIEW", handleSelect);
417
+ const unsubSelect = eventBus.on("UI:SELECT", handleSelect);
418
+ const unsubClose = eventBus.on("UI:CLOSE", handleDeselect);
419
+ const unsubDeselect = eventBus.on("UI:DESELECT", handleDeselect);
420
+ const unsubCancel = eventBus.on("UI:CANCEL", handleDeselect);
421
+ return () => {
422
+ unsubView();
423
+ unsubSelect();
424
+ unsubClose();
425
+ unsubDeselect();
426
+ unsubCancel();
427
+ };
428
+ }, [eventBus, setSelected, clearSelection, debug]);
429
+ const contextValue = {
430
+ selected,
431
+ setSelected,
432
+ clearSelection,
433
+ isSelected
434
+ };
435
+ return /* @__PURE__ */ jsx(SelectionContext.Provider, { value: contextValue, children });
436
+ }
437
+ function useSelection() {
438
+ const context = useContext(SelectionContext);
439
+ if (!context) {
440
+ throw new Error("useSelection must be used within a SelectionProvider");
441
+ }
442
+ return context;
443
+ }
444
+ function useSelectionOptional() {
445
+ const context = useContext(SelectionContext);
446
+ return context;
447
+ }
448
+ var EntityDataContext = createContext(null);
449
+ function EntityDataProvider({
450
+ adapter,
451
+ children
452
+ }) {
453
+ return React2.createElement(
454
+ EntityDataContext.Provider,
455
+ { value: adapter },
456
+ children
457
+ );
458
+ }
459
+ function useEntityDataAdapter() {
460
+ return useContext(EntityDataContext);
461
+ }
462
+ var entityDataKeys = {
463
+ all: ["entities"],
464
+ lists: () => [...entityDataKeys.all, "list"],
465
+ list: (entity, filters) => [...entityDataKeys.lists(), entity, filters],
466
+ details: () => [...entityDataKeys.all, "detail"],
467
+ detail: (entity, id) => [...entityDataKeys.details(), entity, id]
468
+ };
469
+ function useEntityList(entity, options = {}) {
470
+ const { skip = false } = options;
471
+ const adapter = useContext(EntityDataContext);
472
+ const adapterData = useMemo(() => {
473
+ if (!adapter || !entity || skip) return [];
474
+ return adapter.getData(entity);
475
+ }, [adapter, entity, skip, adapter?.isLoading]);
476
+ const [stubData, setStubData] = useState([]);
477
+ const [stubLoading, setStubLoading] = useState(!skip && !!entity && !adapter);
478
+ const [stubError, setStubError] = useState(null);
479
+ useEffect(() => {
480
+ if (adapter || skip || !entity) {
481
+ setStubLoading(false);
482
+ return;
483
+ }
484
+ setStubLoading(true);
485
+ const t = setTimeout(() => {
486
+ setStubData([]);
487
+ setStubLoading(false);
488
+ }, 100);
489
+ return () => clearTimeout(t);
490
+ }, [entity, skip, adapter]);
491
+ if (adapter) {
492
+ return {
493
+ data: adapterData,
494
+ isLoading: adapter.isLoading,
495
+ error: adapter.error ? new Error(adapter.error) : null,
496
+ refetch: () => {
497
+ }
498
+ };
499
+ }
500
+ return { data: stubData, isLoading: stubLoading, error: stubError, refetch: () => {
501
+ } };
559
502
  }
560
- function useInfiniteScroll(onLoadMore, options = {}) {
561
- const { rootMargin = "200px", hasMore = true, isLoading = false } = options;
562
- const observerRef = useRef(null);
563
- const callbackRef = useRef(onLoadMore);
564
- callbackRef.current = onLoadMore;
565
- const hasMoreRef = useRef(hasMore);
566
- hasMoreRef.current = hasMore;
567
- const isLoadingRef = useRef(isLoading);
568
- isLoadingRef.current = isLoading;
503
+ function useEntity(entity, id) {
504
+ const adapter = useContext(EntityDataContext);
505
+ const adapterData = useMemo(() => {
506
+ if (!adapter || !entity || !id) return null;
507
+ return adapter.getById(entity, id) ?? null;
508
+ }, [adapter, entity, id, adapter?.isLoading]);
509
+ const [stubData, setStubData] = useState(null);
510
+ const [stubLoading, setStubLoading] = useState(!!entity && !!id && !adapter);
511
+ const [stubError, setStubError] = useState(null);
569
512
  useEffect(() => {
570
- return () => {
571
- observerRef.current?.disconnect();
513
+ if (adapter || !entity || !id) {
514
+ setStubLoading(false);
515
+ return;
516
+ }
517
+ setStubLoading(true);
518
+ const t = setTimeout(() => {
519
+ setStubData(null);
520
+ setStubLoading(false);
521
+ }, 100);
522
+ return () => clearTimeout(t);
523
+ }, [entity, id, adapter]);
524
+ if (adapter) {
525
+ return {
526
+ data: adapterData,
527
+ isLoading: adapter.isLoading,
528
+ error: adapter.error ? new Error(adapter.error) : null
572
529
  };
573
- }, []);
574
- const sentinelRef = useCallback((node) => {
575
- observerRef.current?.disconnect();
576
- if (!node) return;
577
- observerRef.current = new IntersectionObserver(
578
- (entries) => {
579
- const entry = entries[0];
580
- if (entry.isIntersecting && hasMoreRef.current && !isLoadingRef.current) {
581
- callbackRef.current();
582
- }
583
- },
584
- { rootMargin }
585
- );
586
- observerRef.current.observe(node);
587
- }, [rootMargin]);
588
- return { sentinelRef };
530
+ }
531
+ return { data: stubData, isLoading: stubLoading, error: stubError };
589
532
  }
590
- function usePullToRefresh(onRefresh, options = {}) {
591
- const { threshold = 60, maxPull = 120 } = options;
592
- const [pullDistance, setPullDistance] = useState(0);
593
- const [isPulling, setIsPulling] = useState(false);
594
- const [isRefreshing, setIsRefreshing] = useState(false);
595
- const startY = useRef(0);
596
- const scrollTopRef = useRef(0);
597
- const onTouchStart = useCallback((e) => {
598
- const container = e.currentTarget;
599
- scrollTopRef.current = container.scrollTop;
600
- if (scrollTopRef.current <= 0) {
601
- startY.current = e.touches[0].clientY;
602
- setIsPulling(true);
603
- }
604
- }, []);
605
- const onTouchMove = useCallback((e) => {
606
- if (!isPulling || isRefreshing) return;
607
- const container = e.currentTarget;
608
- if (container.scrollTop > 0) {
609
- setPullDistance(0);
610
- return;
533
+ function useEntityDetail(entity, id) {
534
+ const result = useEntity(entity, id);
535
+ return { ...result, refetch: () => {
536
+ } };
537
+ }
538
+ var suspenseCache = /* @__PURE__ */ new Map();
539
+ function getSuspenseCacheKey(entity, type, id) {
540
+ return id ? `${type}:${entity}:${id}` : `${type}:${entity}`;
541
+ }
542
+ function useEntityListSuspense(entity) {
543
+ const adapter = useContext(EntityDataContext);
544
+ if (adapter) {
545
+ if (adapter.isLoading) {
546
+ const cacheKey2 = getSuspenseCacheKey(entity, "list");
547
+ let entry2 = suspenseCache.get(cacheKey2);
548
+ if (!entry2 || entry2.status === "resolved") {
549
+ let resolve;
550
+ const promise = new Promise((r) => {
551
+ resolve = r;
552
+ });
553
+ entry2 = { promise, status: "pending" };
554
+ suspenseCache.set(cacheKey2, entry2);
555
+ const check = setInterval(() => {
556
+ if (!adapter.isLoading) {
557
+ clearInterval(check);
558
+ entry2.status = "resolved";
559
+ resolve();
560
+ }
561
+ }, 50);
562
+ }
563
+ if (entry2.status === "pending") {
564
+ throw entry2.promise;
565
+ }
611
566
  }
612
- const dy = e.touches[0].clientY - startY.current;
613
- if (dy > 0) {
614
- const distance = Math.min(dy * 0.5, maxPull);
615
- setPullDistance(distance);
567
+ if (adapter.error) {
568
+ throw new Error(adapter.error);
616
569
  }
617
- }, [isPulling, isRefreshing, maxPull]);
618
- const onTouchEnd = useCallback(() => {
619
- if (!isPulling) return;
620
- setIsPulling(false);
621
- if (pullDistance >= threshold && !isRefreshing) {
622
- setIsRefreshing(true);
623
- setPullDistance(threshold);
624
- onRefresh();
625
- } else {
626
- setPullDistance(0);
570
+ return {
571
+ data: adapter.getData(entity),
572
+ refetch: () => {
573
+ }
574
+ };
575
+ }
576
+ const cacheKey = getSuspenseCacheKey(entity, "list");
577
+ let entry = suspenseCache.get(cacheKey);
578
+ if (!entry) {
579
+ let resolve;
580
+ const promise = new Promise((r) => {
581
+ resolve = r;
582
+ setTimeout(() => {
583
+ entry.status = "resolved";
584
+ resolve();
585
+ }, 100);
586
+ });
587
+ entry = { promise, status: "pending" };
588
+ suspenseCache.set(cacheKey, entry);
589
+ }
590
+ if (entry.status === "pending") {
591
+ throw entry.promise;
592
+ }
593
+ return { data: [], refetch: () => {
594
+ } };
595
+ }
596
+ function useEntitySuspense(entity, id) {
597
+ const adapter = useContext(EntityDataContext);
598
+ if (adapter) {
599
+ if (adapter.isLoading) {
600
+ const cacheKey2 = getSuspenseCacheKey(entity, "detail", id);
601
+ let entry2 = suspenseCache.get(cacheKey2);
602
+ if (!entry2 || entry2.status === "resolved") {
603
+ let resolve;
604
+ const promise = new Promise((r) => {
605
+ resolve = r;
606
+ });
607
+ entry2 = { promise, status: "pending" };
608
+ suspenseCache.set(cacheKey2, entry2);
609
+ const check = setInterval(() => {
610
+ if (!adapter.isLoading) {
611
+ clearInterval(check);
612
+ entry2.status = "resolved";
613
+ resolve();
614
+ }
615
+ }, 50);
616
+ }
617
+ if (entry2.status === "pending") {
618
+ throw entry2.promise;
619
+ }
627
620
  }
628
- }, [isPulling, pullDistance, threshold, isRefreshing, onRefresh]);
629
- const endRefresh = useCallback(() => {
630
- setIsRefreshing(false);
631
- setPullDistance(0);
632
- }, []);
633
- const containerProps = {
634
- onTouchStart,
635
- onTouchMove,
636
- onTouchEnd,
637
- style: {
638
- transform: pullDistance > 0 ? `translateY(${pullDistance}px)` : void 0,
639
- transition: isPulling ? "none" : "transform 300ms ease-out"
621
+ if (adapter.error) {
622
+ throw new Error(adapter.error);
640
623
  }
641
- };
642
- return {
643
- pullDistance,
644
- isPulling,
645
- isRefreshing,
646
- containerProps,
647
- endRefresh
648
- };
624
+ return {
625
+ data: adapter.getById(entity, id) ?? null,
626
+ refetch: () => {
627
+ }
628
+ };
629
+ }
630
+ const cacheKey = getSuspenseCacheKey(entity, "detail", id);
631
+ let entry = suspenseCache.get(cacheKey);
632
+ if (!entry) {
633
+ let resolve;
634
+ const promise = new Promise((r) => {
635
+ resolve = r;
636
+ setTimeout(() => {
637
+ entry.status = "resolved";
638
+ resolve();
639
+ }, 100);
640
+ });
641
+ entry = { promise, status: "pending" };
642
+ suspenseCache.set(cacheKey, entry);
643
+ }
644
+ if (entry.status === "pending") {
645
+ throw entry.promise;
646
+ }
647
+ return { data: null, refetch: () => {
648
+ } };
649
649
  }
650
650
 
651
651
  export { EntityDataProvider, I18nProvider, SelectionContext, SelectionProvider, createTranslate, entityDataKeys, parseQueryBinding, useDragReorder, useEntity, useEntityDataAdapter, useEntityDetail, useEntityList, useEntityListSuspense, useEntitySuspense, useInfiniteScroll, useLongPress, usePullToRefresh, useQuerySingleton, useSelection, useSelectionOptional, useSwipeGesture, useTranslate };