@webority-technologies/mobile-ui 0.0.1 → 0.0.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.
Files changed (134) hide show
  1. package/README.md +2 -1
  2. package/lib/commonjs/components/AuthedImage/AuthedImage.js +225 -0
  3. package/lib/commonjs/components/AuthedImage/authedImageCache.js +253 -0
  4. package/lib/commonjs/components/AuthedImage/index.js +45 -0
  5. package/lib/commonjs/components/BottomSheet/BottomSheet.js +10 -4
  6. package/lib/commonjs/components/Drawer/Drawer.js +6 -3
  7. package/lib/commonjs/components/ErrorBoundary/ErrorBoundary.js +136 -0
  8. package/lib/commonjs/components/ErrorBoundary/index.js +27 -0
  9. package/lib/commonjs/components/ErrorBoundary/useErrorHandler.js +24 -0
  10. package/lib/commonjs/components/ErrorBoundary/withErrorBoundary.js +28 -0
  11. package/lib/commonjs/components/FilePicker/FilePicker.js +1 -1
  12. package/lib/commonjs/components/FloatingActionButton/FloatingActionButton.js +14 -1
  13. package/lib/commonjs/components/ImageGallery/ImageGallery.js +13 -0
  14. package/lib/commonjs/components/InlineSelect/InlineSelect.js +1 -1
  15. package/lib/commonjs/components/Input/Input.js +6 -0
  16. package/lib/commonjs/components/KeyboardAwareScrollView/KeyboardAwareScrollView.js +62 -13
  17. package/lib/commonjs/components/LoaderOverlay/LoaderOverlay.js +203 -0
  18. package/lib/commonjs/components/LoaderOverlay/index.js +25 -0
  19. package/lib/commonjs/components/Markdown/Markdown.js +274 -0
  20. package/lib/commonjs/components/Markdown/index.js +13 -0
  21. package/lib/commonjs/components/Markdown/parse.js +229 -0
  22. package/lib/commonjs/components/OTPInput/OTPInput.js +5 -0
  23. package/lib/commonjs/components/OptionSheet/OptionSheet.js +3 -3
  24. package/lib/commonjs/components/PhoneNumberInput/PhoneNumberInput.js +1 -1
  25. package/lib/commonjs/components/Radio/Radio.js +9 -0
  26. package/lib/commonjs/components/Rating/Rating.js +5 -0
  27. package/lib/commonjs/components/RichTextEditor/RichTextEditor.js +311 -0
  28. package/lib/commonjs/components/RichTextEditor/index.js +20 -0
  29. package/lib/commonjs/components/RichTextEditor/markdownActions.js +213 -0
  30. package/lib/commonjs/components/SearchBar/SearchBar.js +7 -0
  31. package/lib/commonjs/components/Select/Select.js +1 -1
  32. package/lib/commonjs/components/Skeleton/Skeleton.js +17 -36
  33. package/lib/commonjs/components/Slider/Slider.js +21 -5
  34. package/lib/commonjs/components/Swipeable/Swipeable.js +5 -2
  35. package/lib/commonjs/components/TimePicker/TimePicker.js +11 -0
  36. package/lib/commonjs/components/index.js +230 -141
  37. package/lib/commonjs/form/FormContext.js +13 -1
  38. package/lib/commonjs/form/useField.js +13 -1
  39. package/lib/commonjs/theme/Gradient.js +44 -41
  40. package/lib/commonjs/theme/index.js +24 -2
  41. package/lib/commonjs/utils/hapticUtils.js +38 -18
  42. package/lib/module/components/AuthedImage/AuthedImage.js +221 -0
  43. package/lib/module/components/AuthedImage/authedImageCache.js +239 -0
  44. package/lib/module/components/AuthedImage/index.js +5 -0
  45. package/lib/module/components/BottomSheet/BottomSheet.js +10 -4
  46. package/lib/module/components/Drawer/Drawer.js +6 -3
  47. package/lib/module/components/ErrorBoundary/ErrorBoundary.js +130 -0
  48. package/lib/module/components/ErrorBoundary/index.js +6 -0
  49. package/lib/module/components/ErrorBoundary/useErrorHandler.js +20 -0
  50. package/lib/module/components/ErrorBoundary/withErrorBoundary.js +23 -0
  51. package/lib/module/components/FilePicker/FilePicker.js +1 -1
  52. package/lib/module/components/FloatingActionButton/FloatingActionButton.js +14 -1
  53. package/lib/module/components/ImageGallery/ImageGallery.js +13 -0
  54. package/lib/module/components/InlineSelect/InlineSelect.js +1 -1
  55. package/lib/module/components/Input/Input.js +6 -0
  56. package/lib/module/components/KeyboardAwareScrollView/KeyboardAwareScrollView.js +63 -14
  57. package/lib/module/components/LoaderOverlay/LoaderOverlay.js +196 -0
  58. package/lib/module/components/LoaderOverlay/index.js +4 -0
  59. package/lib/module/components/Markdown/Markdown.js +268 -0
  60. package/lib/module/components/Markdown/index.js +4 -0
  61. package/lib/module/components/Markdown/parse.js +223 -0
  62. package/lib/module/components/OTPInput/OTPInput.js +5 -0
  63. package/lib/module/components/OptionSheet/OptionSheet.js +3 -3
  64. package/lib/module/components/PhoneNumberInput/PhoneNumberInput.js +1 -1
  65. package/lib/module/components/Radio/Radio.js +9 -0
  66. package/lib/module/components/Rating/Rating.js +5 -0
  67. package/lib/module/components/RichTextEditor/RichTextEditor.js +306 -0
  68. package/lib/module/components/RichTextEditor/index.js +5 -0
  69. package/lib/module/components/RichTextEditor/markdownActions.js +207 -0
  70. package/lib/module/components/SearchBar/SearchBar.js +7 -0
  71. package/lib/module/components/Select/Select.js +1 -1
  72. package/lib/module/components/Skeleton/Skeleton.js +17 -36
  73. package/lib/module/components/Slider/Slider.js +21 -5
  74. package/lib/module/components/Swipeable/Swipeable.js +5 -2
  75. package/lib/module/components/TimePicker/TimePicker.js +11 -0
  76. package/lib/module/components/index.js +5 -0
  77. package/lib/module/form/FormContext.js +13 -1
  78. package/lib/module/form/useField.js +13 -1
  79. package/lib/module/theme/Gradient.js +41 -38
  80. package/lib/module/theme/index.js +17 -1
  81. package/lib/module/utils/hapticUtils.js +37 -18
  82. package/lib/typescript/commonjs/components/AuthedImage/AuthedImage.d.ts +22 -0
  83. package/lib/typescript/commonjs/components/AuthedImage/authedImageCache.d.ts +48 -0
  84. package/lib/typescript/commonjs/components/AuthedImage/index.d.ts +5 -0
  85. package/lib/typescript/commonjs/components/Button/Button.d.ts +1 -3
  86. package/lib/typescript/commonjs/components/Card/Card.d.ts +1 -2
  87. package/lib/typescript/commonjs/components/ErrorBoundary/ErrorBoundary.d.ts +38 -0
  88. package/lib/typescript/commonjs/components/ErrorBoundary/index.d.ts +5 -0
  89. package/lib/typescript/commonjs/components/ErrorBoundary/useErrorHandler.d.ts +9 -0
  90. package/lib/typescript/commonjs/components/ErrorBoundary/withErrorBoundary.d.ts +9 -0
  91. package/lib/typescript/commonjs/components/KeyboardAwareScrollView/KeyboardAwareScrollView.d.ts +15 -0
  92. package/lib/typescript/commonjs/components/LoaderOverlay/LoaderOverlay.d.ts +25 -0
  93. package/lib/typescript/commonjs/components/LoaderOverlay/index.d.ts +3 -0
  94. package/lib/typescript/commonjs/components/Markdown/Markdown.d.ts +29 -0
  95. package/lib/typescript/commonjs/components/Markdown/index.d.ts +3 -0
  96. package/lib/typescript/commonjs/components/Markdown/parse.d.ts +58 -0
  97. package/lib/typescript/commonjs/components/OptionSheet/OptionSheet.d.ts +8 -1
  98. package/lib/typescript/commonjs/components/RichTextEditor/RichTextEditor.d.ts +48 -0
  99. package/lib/typescript/commonjs/components/RichTextEditor/index.d.ts +5 -0
  100. package/lib/typescript/commonjs/components/RichTextEditor/markdownActions.d.ts +35 -0
  101. package/lib/typescript/commonjs/components/Skeleton/Skeleton.d.ts +1 -1
  102. package/lib/typescript/commonjs/components/index.d.ts +10 -0
  103. package/lib/typescript/commonjs/form/FormContext.d.ts +13 -3
  104. package/lib/typescript/commonjs/theme/Gradient.d.ts +18 -3
  105. package/lib/typescript/commonjs/theme/index.d.ts +16 -1
  106. package/lib/typescript/commonjs/theme/types.d.ts +2 -2
  107. package/lib/typescript/commonjs/utils/hapticUtils.d.ts +4 -0
  108. package/lib/typescript/module/components/AuthedImage/AuthedImage.d.ts +22 -0
  109. package/lib/typescript/module/components/AuthedImage/authedImageCache.d.ts +48 -0
  110. package/lib/typescript/module/components/AuthedImage/index.d.ts +5 -0
  111. package/lib/typescript/module/components/Button/Button.d.ts +1 -3
  112. package/lib/typescript/module/components/Card/Card.d.ts +1 -2
  113. package/lib/typescript/module/components/ErrorBoundary/ErrorBoundary.d.ts +38 -0
  114. package/lib/typescript/module/components/ErrorBoundary/index.d.ts +5 -0
  115. package/lib/typescript/module/components/ErrorBoundary/useErrorHandler.d.ts +9 -0
  116. package/lib/typescript/module/components/ErrorBoundary/withErrorBoundary.d.ts +9 -0
  117. package/lib/typescript/module/components/KeyboardAwareScrollView/KeyboardAwareScrollView.d.ts +15 -0
  118. package/lib/typescript/module/components/LoaderOverlay/LoaderOverlay.d.ts +25 -0
  119. package/lib/typescript/module/components/LoaderOverlay/index.d.ts +3 -0
  120. package/lib/typescript/module/components/Markdown/Markdown.d.ts +29 -0
  121. package/lib/typescript/module/components/Markdown/index.d.ts +3 -0
  122. package/lib/typescript/module/components/Markdown/parse.d.ts +58 -0
  123. package/lib/typescript/module/components/OptionSheet/OptionSheet.d.ts +8 -1
  124. package/lib/typescript/module/components/RichTextEditor/RichTextEditor.d.ts +48 -0
  125. package/lib/typescript/module/components/RichTextEditor/index.d.ts +5 -0
  126. package/lib/typescript/module/components/RichTextEditor/markdownActions.d.ts +35 -0
  127. package/lib/typescript/module/components/Skeleton/Skeleton.d.ts +1 -1
  128. package/lib/typescript/module/components/index.d.ts +10 -0
  129. package/lib/typescript/module/form/FormContext.d.ts +13 -3
  130. package/lib/typescript/module/theme/Gradient.d.ts +18 -3
  131. package/lib/typescript/module/theme/index.d.ts +16 -1
  132. package/lib/typescript/module/theme/types.d.ts +2 -2
  133. package/lib/typescript/module/utils/hapticUtils.d.ts +4 -0
  134. package/package.json +20 -13
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+
3
+ import { ACCESS_TOKEN_KEY, getToken, isStorageAvailable, Logger, Storage } from '@webority-technologies/mobile-core';
4
+ const KEY_PREFIX = '@webority/authed-image/v1/';
5
+ const DATA_PREFIX = `${KEY_PREFIX}d/`;
6
+ const META_PREFIX = `${KEY_PREFIX}m/`;
7
+ const DEFAULT_BYTE_CEILING = 50 * 1024 * 1024;
8
+ export const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
9
+ let byteCeiling = DEFAULT_BYTE_CEILING;
10
+
11
+ /**
12
+ * Ceiling for the total stored size of cached images. Exceeding it evicts the
13
+ * least recently used entries. An image cache with no ceiling grows until the
14
+ * OS reclaims the app's storage, which is the defect this replaces.
15
+ */
16
+ export const setAuthedImageCacheLimit = bytes => {
17
+ if (!Number.isFinite(bytes) || bytes <= 0) {
18
+ throw new Error('[@webority-technologies/mobile-ui] setAuthedImageCacheLimit() needs a positive number of ' + `bytes, received ${String(bytes)}. Pass e.g. 50 * 1024 * 1024 for 50 MB.`);
19
+ }
20
+ byteCeiling = bytes;
21
+ };
22
+ export const getAuthedImageCacheLimit = () => byteCeiling;
23
+ const hashString = input => {
24
+ let h1 = 0x811c9dc5;
25
+ let h2 = 0x1000193;
26
+ for (let i = 0; i < input.length; i += 1) {
27
+ const code = input.charCodeAt(i);
28
+ h1 = Math.imul(h1 ^ code, 16777619) >>> 0;
29
+ h2 = (Math.imul(h2 + code, 2654435761) ^ h2 >>> 7) >>> 0;
30
+ }
31
+ return `${h1.toString(16)}${h2.toString(16)}${input.length.toString(16)}`;
32
+ };
33
+
34
+ /**
35
+ * Hex only by construction, so it is always a safe storage key segment.
36
+ */
37
+ export const deriveCacheKey = (uri, override) => hashString(override ?? uri);
38
+ const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
39
+ const encodeBase64 = buffer => {
40
+ const bytes = new Uint8Array(buffer);
41
+ let out = '';
42
+ for (let i = 0; i < bytes.length; i += 3) {
43
+ const b0 = bytes[i];
44
+ const b1 = i + 1 < bytes.length ? bytes[i + 1] : undefined;
45
+ const b2 = i + 2 < bytes.length ? bytes[i + 2] : undefined;
46
+ out += B64_ALPHABET[b0 >> 2];
47
+ out += B64_ALPHABET[(b0 & 0x03) << 4 | (b1 ?? 0) >> 4];
48
+ out += b1 === undefined ? '=' : B64_ALPHABET[((b1 & 0x0f) << 2 | (b2 ?? 0) >> 6) & 0x3f];
49
+ out += b2 === undefined ? '=' : B64_ALPHABET[b2 & 0x3f];
50
+ }
51
+ return out;
52
+ };
53
+
54
+ /**
55
+ * The cache is an OPTIMISATION, so a failing store must never stop an image
56
+ * rendering. That used to mean guarding on whether a backend existed at all,
57
+ * warning once and skipping the cache; under Expo-first the backend ships with
58
+ * the library, so "no backend" is no longer a state an app can be in and that
59
+ * branch was unreachable.
60
+ *
61
+ * What remains is a store that exists and FAILS, which the per-operation error
62
+ * handling below already covers: each read returns null and each write returns
63
+ * false, the image still fetches, and nothing is logged per image.
64
+ */
65
+ const cacheUsable = () => isStorageAvailable();
66
+ export const readEntry = async cacheKey => {
67
+ if (!cacheUsable()) {
68
+ return null;
69
+ }
70
+ const values = await Storage.multiGet([`${DATA_PREFIX}${cacheKey}`, `${META_PREFIX}${cacheKey}`]);
71
+ const dataUri = values[`${DATA_PREFIX}${cacheKey}`];
72
+ const rawMeta = values[`${META_PREFIX}${cacheKey}`];
73
+ if (!dataUri || !rawMeta) {
74
+ return null;
75
+ }
76
+ try {
77
+ return {
78
+ dataUri,
79
+ meta: JSON.parse(rawMeta)
80
+ };
81
+ } catch {
82
+ return null;
83
+ }
84
+ };
85
+ export const isFresh = (meta, ttlMs, now = Date.now()) => now - meta.fetchedAt < ttlMs;
86
+ export const touchEntry = async (cacheKey, meta) => {
87
+ if (!cacheUsable()) {
88
+ return;
89
+ }
90
+ await Storage.setItem(`${META_PREFIX}${cacheKey}`, JSON.stringify({
91
+ ...meta,
92
+ lastAccessedAt: Date.now()
93
+ }));
94
+ };
95
+ const listMeta = async () => {
96
+ if (!cacheUsable()) {
97
+ return [];
98
+ }
99
+ const keys = await Storage.getAllKeys();
100
+ const metaKeys = keys.filter(key => key.startsWith(META_PREFIX));
101
+ if (metaKeys.length === 0) {
102
+ return [];
103
+ }
104
+ const values = await Storage.multiGet(metaKeys);
105
+ const out = [];
106
+ for (const key of metaKeys) {
107
+ const raw = values[key];
108
+ if (!raw) {
109
+ continue;
110
+ }
111
+ try {
112
+ out.push({
113
+ cacheKey: key.slice(META_PREFIX.length),
114
+ meta: JSON.parse(raw)
115
+ });
116
+ } catch {
117
+ // A meta entry we cannot parse is unusable; dropping it from the ledger
118
+ // lets the next eviction pass remove its orphaned data entry.
119
+ }
120
+ }
121
+ return out;
122
+ };
123
+ const evictIfNeeded = async () => {
124
+ const entries = await listMeta();
125
+ let total = entries.reduce((sum, entry) => sum + entry.meta.bytes, 0);
126
+ if (total <= byteCeiling) {
127
+ return;
128
+ }
129
+ const ordered = [...entries].sort((a, b) => a.meta.lastAccessedAt - b.meta.lastAccessedAt);
130
+ const doomed = [];
131
+ for (const entry of ordered) {
132
+ if (total <= byteCeiling) {
133
+ break;
134
+ }
135
+ doomed.push(`${DATA_PREFIX}${entry.cacheKey}`, `${META_PREFIX}${entry.cacheKey}`);
136
+ total -= entry.meta.bytes;
137
+ }
138
+ if (doomed.length > 0) {
139
+ await Storage.multiRemove(doomed);
140
+ Logger.info(`[AuthedImage] evicted ${doomed.length / 2} cached image(s) over the byte ceiling`);
141
+ }
142
+ };
143
+ export const clearAuthedImageCache = async () => {
144
+ if (!cacheUsable()) {
145
+ return;
146
+ }
147
+ const keys = await Storage.getAllKeys();
148
+ const ours = keys.filter(key => key.startsWith(KEY_PREFIX));
149
+ if (ours.length > 0) {
150
+ await Storage.multiRemove(ours);
151
+ }
152
+ };
153
+ export const authedImageCacheStats = async () => {
154
+ const entries = await listMeta();
155
+ return {
156
+ entries: entries.length,
157
+ bytes: entries.reduce((sum, entry) => sum + entry.meta.bytes, 0)
158
+ };
159
+ };
160
+ const fetchAndStore = async (cacheKey, options, signal) => {
161
+ const headers = {
162
+ ...options.headers
163
+ };
164
+ if (options.authenticated) {
165
+ const stored = await getToken(ACCESS_TOKEN_KEY);
166
+ if (stored?.token) {
167
+ headers.Authorization = `Bearer ${stored.token}`;
168
+ }
169
+ }
170
+ const response = await fetch(options.uri, {
171
+ headers,
172
+ signal
173
+ });
174
+ if (!response.ok) {
175
+ throw new Error(`[@webority-technologies/mobile-ui] AuthedImage could not load "${options.uri}": the server ` + `answered ${response.status}. Check the URL and whether the request needs a bearer token ` + '(pass authenticated={false} for public media).');
176
+ }
177
+ const buffer = await response.arrayBuffer();
178
+ const rawType = response.headers?.get?.('content-type');
179
+ const contentType = (rawType ? rawType.split(';')[0].trim() : '') || 'image/jpeg';
180
+ const dataUri = `data:${contentType};base64,${encodeBase64(buffer)}`;
181
+ const now = Date.now();
182
+ // `bytes` measures the stored data URI, not the decoded image: the ceiling
183
+ // exists to bound what sits in storage, and base64 costs about a third more.
184
+ const meta = {
185
+ fetchedAt: now,
186
+ bytes: dataUri.length,
187
+ contentType,
188
+ lastAccessedAt: now
189
+ };
190
+ if (cacheUsable()) {
191
+ await Storage.multiSet({
192
+ [`${DATA_PREFIX}${cacheKey}`]: dataUri,
193
+ [`${META_PREFIX}${cacheKey}`]: JSON.stringify(meta)
194
+ });
195
+ await evictIfNeeded();
196
+ }
197
+ return {
198
+ dataUri,
199
+ meta
200
+ };
201
+ };
202
+ const inFlight = new Map();
203
+
204
+ /**
205
+ * Two mounts of the same image share one request. The controller is aborted
206
+ * only when the last waiter releases, so one unmount cannot cancel the fetch
207
+ * another mount is still waiting on.
208
+ */
209
+ export const acquireFetch = (cacheKey, options) => {
210
+ const existing = inFlight.get(cacheKey);
211
+ if (existing) {
212
+ existing.waiters += 1;
213
+ return existing;
214
+ }
215
+ const controller = new AbortController();
216
+ const entry = {
217
+ controller,
218
+ waiters: 1,
219
+ promise: Promise.resolve()
220
+ };
221
+ entry.promise = fetchAndStore(cacheKey, options, controller.signal).finally(() => {
222
+ if (inFlight.get(cacheKey) === entry) {
223
+ inFlight.delete(cacheKey);
224
+ }
225
+ });
226
+ inFlight.set(cacheKey, entry);
227
+ return entry;
228
+ };
229
+ export const releaseFetch = (cacheKey, entry) => {
230
+ entry.waiters -= 1;
231
+ if (entry.waiters > 0) {
232
+ return;
233
+ }
234
+ if (inFlight.get(cacheKey) === entry) {
235
+ inFlight.delete(cacheKey);
236
+ }
237
+ entry.controller.abort();
238
+ };
239
+ //# sourceMappingURL=authedImageCache.js.map
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ export { AuthedImage, default } from "./AuthedImage.js";
4
+ export { authedImageCacheStats, clearAuthedImageCache, getAuthedImageCacheLimit, setAuthedImageCacheLimit } from "./authedImageCache.js";
5
+ //# sourceMappingURL=index.js.map
@@ -120,6 +120,12 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
120
120
 
121
121
  // Convert a snap-point index → translateY position. -1 = closed.
122
122
  const yForIndex = useCallback(idx => {
123
+ // NOT dead code, despite every ordinary call site clamping first.
124
+ // `expand()` has no empty-snapPoints guard (unlike `collapse`), so with
125
+ // snapPoints={[]} it arrives here out of range — and without this,
126
+ // `screenHeight - resolvedSnapPoints[idx]` is `screenHeight - undefined`,
127
+ // i.e. NaN into the sheet position, which breaks layout silently rather
128
+ // than degrading. Covered by the "degenerate snapPoints" tests.
123
129
  if (idx < 0 || idx >= resolvedSnapPoints.length) {
124
130
  return closedY;
125
131
  }
@@ -256,7 +262,7 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
256
262
  const closed = closedY;
257
263
  const top = minTopY;
258
264
  const canPanDownToClose = enablePanDownToClose;
259
- return Gesture.Pan().activeOffsetY([-DRAG_ACTIVATION_OFFSET, DRAG_ACTIVATION_OFFSET]).failOffsetX([-15, 15]).onStart(() => {
265
+ return Gesture.Pan().withTestId(testID ? `${testID}-pan-gesture` : 'bottom-sheet-pan-gesture').activeOffsetY([-DRAG_ACTIVATION_OFFSET, DRAG_ACTIVATION_OFFSET]).failOffsetX([-15, 15]).onStart(() => {
260
266
  'worklet';
261
267
 
262
268
  dragStartY.value = translateY.value;
@@ -306,7 +312,7 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
306
312
  }
307
313
  runOnJS(handleSnapEnd)(nearestIdx, targets[nearestIdx]);
308
314
  });
309
- }, [closedY, dragStartY, enablePanDownToClose, handleSnapEnd, minTopY, snapTargets, translateY]);
315
+ }, [closedY, dragStartY, enablePanDownToClose, handleSnapEnd, minTopY, snapTargets, testID, translateY]);
310
316
 
311
317
  // ───────── Keyboard tracking ─────────
312
318
  // keyboardOffset is added to translateY in sheetStyle so we don't disturb
@@ -378,14 +384,14 @@ const BottomSheet = /*#__PURE__*/forwardRef((props, ref) => {
378
384
  // gesture-handler/reanimated sheet doesn't fire reliably on RN 0.85 / Fabric
379
385
  // (the gesture system swallows the touch sequence). gorhom/bottom-sheet uses
380
386
  // the same Gesture.Tap() pattern for the same reason.
381
- const backdropTapGesture = useMemo(() => Gesture.Tap().maxDuration(250).onEnd((_e, success) => {
387
+ const backdropTapGesture = useMemo(() => Gesture.Tap().withTestId(testID ? `${testID}-backdrop-gesture` : 'bottom-sheet-backdrop-gesture').maxDuration(250).onEnd((_e, success) => {
382
388
  'worklet';
383
389
 
384
390
  if (!success) {
385
391
  return;
386
392
  }
387
393
  runOnJS(handleBackdropPress)();
388
- }), [handleBackdropPress]);
394
+ }), [handleBackdropPress, testID]);
389
395
  const contextValue = useMemo(() => ({
390
396
  snapIndex: currentIndex,
391
397
  snapPoints: resolvedSnapPoints,
@@ -77,8 +77,12 @@ const Drawer = /*#__PURE__*/forwardRef((props, ref) => {
77
77
  return sheetSize;
78
78
  }
79
79
  }, [side, sheetSize]);
80
- const offset = useSharedValue(closedOffset);
81
- const dragStart = useSharedValue(closedOffset);
80
+
81
+ // Uncontrolled defaultVisible starts already open — the offset must start at the
82
+ // open position (0) too, or the sheet is logically open while visually still
83
+ // pinned at the closed edge until the next explicit open()/close()/drag.
84
+ const offset = useSharedValue(defaultVisible ? 0 : closedOffset);
85
+ const dragStart = useSharedValue(defaultVisible ? 0 : closedOffset);
82
86
  const [isOpen, setIsOpen] = useState(defaultVisible);
83
87
  const isOpenShared = useSharedValue(defaultVisible);
84
88
 
@@ -330,7 +334,6 @@ const Drawer = /*#__PURE__*/forwardRef((props, ref) => {
330
334
  children: /*#__PURE__*/_jsx(Animated.View, {
331
335
  accessibilityRole: "alert",
332
336
  accessibilityLabel: accessibilityLabel ?? 'Drawer',
333
- accessibilityViewIsModal: isOpen,
334
337
  style: [sideLayout, {
335
338
  backgroundColor: theme.colors.background.elevated,
336
339
  ...theme.shadows.xl
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+
3
+ import { Logger } from '@webority-technologies/mobile-core';
4
+ import React, { useMemo } from 'react';
5
+ import { StyleSheet, Text, View } from 'react-native';
6
+ import { useTheme } from "../../theme/index.js";
7
+ import { EmptyState } from "../EmptyState/index.js";
8
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
+ const COMPONENT_STACK_LIMIT = 500;
10
+ const DEFAULT_TITLE = 'Something went wrong';
11
+ const DEFAULT_MESSAGE = 'An unexpected error occurred. Please try again.';
12
+ const hasResetKeysChanged = (prev, next) => {
13
+ if (prev === next) {
14
+ return false;
15
+ }
16
+ if (!prev || !next || prev.length !== next.length) {
17
+ return true;
18
+ }
19
+ return prev.some((value, index) => value !== next[index]);
20
+ };
21
+ const DefaultFallback = ({
22
+ error,
23
+ reset,
24
+ title,
25
+ message,
26
+ testID
27
+ }) => {
28
+ const theme = useTheme();
29
+ const styles = useMemo(() => buildStyles(theme), [theme]);
30
+ const showRawError = typeof __DEV__ !== 'undefined' && __DEV__;
31
+ return /*#__PURE__*/_jsxs(View, {
32
+ style: styles.root,
33
+ testID: testID,
34
+ children: [/*#__PURE__*/_jsx(EmptyState, {
35
+ title: title ?? DEFAULT_TITLE,
36
+ description: message ?? DEFAULT_MESSAGE,
37
+ action: {
38
+ label: 'Try again',
39
+ onPress: reset,
40
+ testID: testID ? `${testID}-retry` : undefined
41
+ }
42
+ }), showRawError ? /*#__PURE__*/_jsx(Text, {
43
+ style: styles.devError,
44
+ testID: testID ? `${testID}-dev-error` : undefined,
45
+ children: error.message
46
+ }) : null]
47
+ });
48
+ };
49
+ const buildStyles = theme => StyleSheet.create({
50
+ root: {
51
+ flex: 1,
52
+ alignItems: 'center',
53
+ justifyContent: 'center',
54
+ backgroundColor: theme.colors.background.primary,
55
+ padding: theme.spacing.lg
56
+ },
57
+ devError: {
58
+ marginTop: theme.spacing.md,
59
+ color: theme.colors.error,
60
+ fontSize: theme.typography.fontSize.sm,
61
+ textAlign: 'center'
62
+ }
63
+ });
64
+
65
+ /**
66
+ * Catches render errors in its subtree and shows a themed fallback instead of
67
+ * a white screen. Mount this once near the navigation root per the fleet
68
+ * convention, and optionally again around any subtree that should recover
69
+ * independently (a single failed card should not blank the whole screen).
70
+ */
71
+ export class ErrorBoundary extends React.Component {
72
+ state = {
73
+ error: null
74
+ };
75
+ static getDerivedStateFromError(error) {
76
+ return {
77
+ error
78
+ };
79
+ }
80
+ componentDidUpdate(prevProps) {
81
+ if (this.state.error && hasResetKeysChanged(prevProps.resetKeys, this.props.resetKeys)) {
82
+ this.reset();
83
+ }
84
+ }
85
+ componentDidCatch(error, info) {
86
+ const componentStack = (info.componentStack ?? '').slice(0, COMPONENT_STACK_LIMIT);
87
+ Logger.recordError(error, {
88
+ errorType: 'RenderError',
89
+ componentStack
90
+ });
91
+ try {
92
+ this.props.onError?.(error, {
93
+ componentStack
94
+ });
95
+ } catch (onErrorError) {
96
+ Logger.error('[@webority-technologies/mobile-ui] ErrorBoundary onError threw; the boundary itself is unaffected.', onErrorError);
97
+ }
98
+ }
99
+ reset = () => {
100
+ this.setState({
101
+ error: null
102
+ });
103
+ this.props.onReset?.();
104
+ };
105
+ render() {
106
+ const {
107
+ error
108
+ } = this.state;
109
+ if (!error) {
110
+ return this.props.children;
111
+ }
112
+ const {
113
+ fallback,
114
+ title,
115
+ message,
116
+ testID
117
+ } = this.props;
118
+ if (fallback !== undefined) {
119
+ return typeof fallback === 'function' ? fallback(error, this.reset) : fallback;
120
+ }
121
+ return /*#__PURE__*/_jsx(DefaultFallback, {
122
+ error: error,
123
+ reset: this.reset,
124
+ title: title,
125
+ message: message,
126
+ testID: testID
127
+ });
128
+ }
129
+ }
130
+ //# sourceMappingURL=ErrorBoundary.js.map
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+
3
+ export { ErrorBoundary } from "./ErrorBoundary.js";
4
+ export { useErrorHandler } from "./useErrorHandler.js";
5
+ export { withErrorBoundary } from "./withErrorBoundary.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+
3
+ import { useCallback, useState } from 'react';
4
+
5
+ /**
6
+ * Returns a stable callback for surfacing an error caught outside React's
7
+ * render/commit cycle (an async callback, a rejected promise, a native event
8
+ * handler) to the nearest ErrorBoundary. React only catches synchronous
9
+ * render errors on its own; re-throwing during a state update is the
10
+ * documented way to route an async error into that same path.
11
+ */
12
+ export const useErrorHandler = () => {
13
+ const [, setState] = useState();
14
+ return useCallback(error => {
15
+ setState(() => {
16
+ throw error instanceof Error ? error : new Error(String(error));
17
+ });
18
+ }, []);
19
+ };
20
+ //# sourceMappingURL=useErrorHandler.js.map
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+ import { ErrorBoundary } from "./ErrorBoundary.js";
5
+
6
+ /**
7
+ * Wraps a component in an `ErrorBoundary`. Prefer this over hand-wrapping a
8
+ * screen at every call site when the same fallback/reset behaviour applies
9
+ * everywhere that component is rendered.
10
+ */
11
+ import { jsx as _jsx } from "react/jsx-runtime";
12
+ export const withErrorBoundary = (Component, boundaryProps) => {
13
+ const displayName = Component.displayName ?? Component.name ?? 'Component';
14
+ const Wrapped = props => /*#__PURE__*/_jsx(ErrorBoundary, {
15
+ ...boundaryProps,
16
+ children: /*#__PURE__*/_jsx(Component, {
17
+ ...props
18
+ })
19
+ });
20
+ Wrapped.displayName = `withErrorBoundary(${displayName})`;
21
+ return Wrapped;
22
+ };
23
+ //# sourceMappingURL=withErrorBoundary.js.map
@@ -604,7 +604,7 @@ const FilePicker = /*#__PURE__*/forwardRef((props, ref) => {
604
604
  title: labels.sheetTitle,
605
605
  options: sources,
606
606
  labelOf: s => s.label,
607
- valueOf: s => s.key,
607
+ getValue: s => s.key,
608
608
  disabledOf: () => false,
609
609
  descriptionOf: s => s.description,
610
610
  iconOf: s => s.icon,
@@ -390,7 +390,20 @@ const FloatingActionButtonGroup = props => {
390
390
  importantForAccessibility: "no",
391
391
  testID: testID ? `${testID}-backdrop` : undefined
392
392
  })
393
- }) : null, /*#__PURE__*/_jsxs(View, {
393
+ }) : null, /*#__PURE__*/_jsxs(View
394
+ // The container is absolutely positioned and sized to the primary FAB,
395
+ // while the secondary actions sit ABOVE it via negative translateY, so
396
+ // they render outside their parent's bounds. Without box-none the
397
+ // container is itself a touch target across its box, and on Android a
398
+ // child drawn outside the parent's bounds does not receive touches at
399
+ // all. box-none makes the container transparent to touches while its
400
+ // children stay interactive, which is what an expanded menu needs.
401
+ //
402
+ // A consumer app carried this as a patch-package fix against two
403
+ // published versions rather than it landing here, which is how the
404
+ // library kept shipping the bug.
405
+ , {
406
+ pointerEvents: "box-none",
394
407
  style: [positionStyle, containerStyle],
395
408
  accessibilityRole: 'menu',
396
409
  testID: testID,
@@ -392,6 +392,15 @@ const ZoomableImage = ({
392
392
  savedScale.value = minScale;
393
393
  savedTranslateX.value = 0;
394
394
  savedTranslateY.value = 0;
395
+ // Mutation-tested equivalent under the jest reanimated mock (withSpring
396
+ // resolves synchronously to its target there): with this return removed,
397
+ // execution falls into `scale.value > maxScale` (always false, since
398
+ // minScale <= maxScale) and then `savedScale.value = scale.value`, which
399
+ // reads back the minScale just assigned above — same final state, so no
400
+ // jest assertion can distinguish the two. On a real device withSpring is
401
+ // asynchronous, so that fallthrough read could observe a stale
402
+ // mid-animation scale instead; kept as an explicit early return for that
403
+ // reason even though it isn't independently testable here.
395
404
  return;
396
405
  }
397
406
  if (scale.value > maxScale) {
@@ -554,6 +563,10 @@ const buildLightboxStyles = theme => StyleSheet.create({
554
563
  paddingHorizontal: 24
555
564
  },
556
565
  captionText: {
566
+ // Fixed white, not a theme token, and deliberately so: the lightbox is an
567
+ // always-dark surface (#000 backdrop with rgba scrims above), so it does
568
+ // not follow the colour scheme. `text.primary` would be dark ink on black
569
+ // in light mode. Do not "themify" this.
557
570
  color: '#FFFFFF',
558
571
  fontSize: 14,
559
572
  lineHeight: 20,
@@ -145,7 +145,7 @@ const InlineSelect = /*#__PURE__*/forwardRef((props, ref) => {
145
145
  accessibilityLabel: label ?? placeholder ?? 'Options',
146
146
  options: options,
147
147
  labelOf: labelOf,
148
- valueOf: valueOf,
148
+ getValue: valueOf,
149
149
  disabledOf: disabledOf,
150
150
  descriptionOf: descriptionOf,
151
151
  iconOf: iconOf,
@@ -110,6 +110,12 @@ const Input = /*#__PURE__*/forwardRef((props, ref) => {
110
110
  const shakeOnError = theme.components.input?.shakeOnError ?? false;
111
111
  const prevErrorRef = useRef(hasError);
112
112
  useEffect(() => {
113
+ // Equivalent-mutant note: removing this `return` cannot change behaviour.
114
+ // The line above it always sets `prevErrorRef.current = hasError` first,
115
+ // so falling through to the check below becomes `hasError &&
116
+ // !prevErrorRef.current` === `hasError && !hasError`, which is always
117
+ // false — the shake/haptic branch can never fire, and the redundant
118
+ // `prevErrorRef.current = hasError` reassignment at the bottom is a no-op.
113
119
  if (!shakeOnError) {
114
120
  prevErrorRef.current = hasError;
115
121
  return;