@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
package/README.md CHANGED
@@ -10,6 +10,7 @@ npm install @webority-technologies/mobile-ui
10
10
 
11
11
  No external runtime dependencies beyond `@webority-technologies/mobile-core`. Requires
12
12
  `react-native-gesture-handler`, `react-native-reanimated` and `react-native-safe-area-context` as
13
- peers; `react-native-linear-gradient` and `react-native-haptic-feedback` are optional.
13
+ peers; `react-native-haptic-feedback` is optional. Gradients use React Native's own
14
+ `experimental_backgroundImage`, so no gradient library is needed.
14
15
 
15
16
  Full documentation lives in the repository's `docs/` directory.
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = exports.AuthedImage = void 0;
7
+ var _react = require("react");
8
+ var _reactNative = require("react-native");
9
+ var _index = require("../../theme/index.js");
10
+ var _index2 = require("../Spinner/index.js");
11
+ var _authedImageCache = require("./authedImageCache.js");
12
+ var _jsxRuntime = require("react/jsx-runtime");
13
+ const toError = value => value instanceof Error ? value : new Error(String(value));
14
+ const AuthedImage = props => {
15
+ const {
16
+ uri,
17
+ headers,
18
+ authenticated = true,
19
+ ttlMs = _authedImageCache.DEFAULT_TTL_MS,
20
+ placeholder,
21
+ fallback,
22
+ onLoadStart,
23
+ onLoadEnd,
24
+ onError,
25
+ cacheKey,
26
+ testID,
27
+ accessibilityLabel,
28
+ style,
29
+ ...imageProps
30
+ } = props;
31
+ const theme = (0, _index.useTheme)();
32
+ const styles = (0, _react.useMemo)(() => buildStyles(theme), [theme]);
33
+ const [state, setState] = (0, _react.useState)({
34
+ status: 'loading'
35
+ });
36
+
37
+ // Callbacks live behind refs so a caller passing inline arrows does not
38
+ // restart the fetch on every render.
39
+ const callbacks = (0, _react.useRef)({
40
+ onLoadStart,
41
+ onLoadEnd,
42
+ onError
43
+ });
44
+ callbacks.current = {
45
+ onLoadStart,
46
+ onLoadEnd,
47
+ onError
48
+ };
49
+
50
+ // Compared by value: a headers object literal is a new identity every render.
51
+ const headersKey = JSON.stringify(headers ?? {});
52
+ (0, _react.useEffect)(() => {
53
+ let cancelled = false;
54
+ let acquired = null;
55
+ const finish = next => {
56
+ if (cancelled) {
57
+ return;
58
+ }
59
+ setState(next);
60
+ callbacks.current.onLoadEnd?.();
61
+ };
62
+ const run = async () => {
63
+ if (!uri) {
64
+ const error = new Error('[@webority-technologies/mobile-ui] AuthedImage was given an empty `uri`. Pass the ' + 'image URL, or render nothing while the URL is still unknown.');
65
+ if (!cancelled) {
66
+ setState({
67
+ status: 'error'
68
+ });
69
+ callbacks.current.onError?.(error);
70
+ }
71
+ return;
72
+ }
73
+ setState({
74
+ status: 'loading'
75
+ });
76
+ callbacks.current.onLoadStart?.();
77
+ const key = (0, _authedImageCache.deriveCacheKey)(uri, cacheKey);
78
+ const cached = await (0, _authedImageCache.readEntry)(key);
79
+ if (cancelled) {
80
+ return;
81
+ }
82
+ if (cached && (0, _authedImageCache.isFresh)(cached.meta, ttlMs)) {
83
+ void (0, _authedImageCache.touchEntry)(key, cached.meta);
84
+ finish({
85
+ status: 'ready',
86
+ dataUri: cached.dataUri
87
+ });
88
+ return;
89
+ }
90
+ const entry = (0, _authedImageCache.acquireFetch)(key, {
91
+ uri,
92
+ headers: JSON.parse(headersKey),
93
+ authenticated
94
+ });
95
+ acquired = {
96
+ key,
97
+ entry
98
+ };
99
+ try {
100
+ const result = await entry.promise;
101
+ finish({
102
+ status: 'ready',
103
+ dataUri: result.dataUri
104
+ });
105
+ } catch (error) {
106
+ if (cancelled) {
107
+ return;
108
+ }
109
+ // A stale image beats an empty box: the entry is only expired, and the
110
+ // refetch that would have replaced it failed.
111
+ if (cached) {
112
+ finish({
113
+ status: 'ready',
114
+ dataUri: cached.dataUri
115
+ });
116
+ return;
117
+ }
118
+ setState({
119
+ status: 'error'
120
+ });
121
+ callbacks.current.onError?.(toError(error));
122
+ callbacks.current.onLoadEnd?.();
123
+ }
124
+ };
125
+ void run();
126
+ return () => {
127
+ cancelled = true;
128
+ if (acquired) {
129
+ (0, _authedImageCache.releaseFetch)(acquired.key, acquired.entry);
130
+ }
131
+ };
132
+ }, [uri, cacheKey, ttlMs, authenticated, headersKey]);
133
+
134
+ // The base `testID` sits on the root in EVERY state, with the state-specific
135
+ // suffixes nested inside it. A consumer that can only find the component while
136
+ // it happens to be loaded cannot wait for it to load.
137
+ if (state.status === 'loading') {
138
+ if (placeholder !== undefined) {
139
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
140
+ style: style,
141
+ testID: testID,
142
+ children: placeholder
143
+ });
144
+ }
145
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
146
+ style: [styles.placeholder, style],
147
+ testID: testID,
148
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
149
+ style: styles.stateFill,
150
+ testID: testID ? `${testID}-placeholder` : undefined,
151
+ pointerEvents: "none",
152
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index2.Spinner, {
153
+ size: "sm",
154
+ accessibilityLabel: accessibilityLabel ?? 'Loading image'
155
+ })
156
+ })
157
+ });
158
+ }
159
+ if (state.status === 'error') {
160
+ if (fallback !== undefined) {
161
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
162
+ style: style,
163
+ testID: testID,
164
+ children: fallback
165
+ });
166
+ }
167
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
168
+ style: [styles.fallback, style],
169
+ testID: testID,
170
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
171
+ accessible: true,
172
+ accessibilityRole: "image",
173
+ accessibilityLabel: accessibilityLabel ? `${accessibilityLabel}, image unavailable` : 'Image unavailable',
174
+ testID: testID ? `${testID}-fallback` : undefined,
175
+ style: styles.stateFill,
176
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
177
+ style: styles.fallbackText,
178
+ children: "!"
179
+ })
180
+ })
181
+ });
182
+ }
183
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
184
+ ...imageProps,
185
+ accessible: true,
186
+ accessibilityRole: "image",
187
+ accessibilityLabel: accessibilityLabel,
188
+ testID: testID,
189
+ style: style,
190
+ source: {
191
+ uri: state.dataUri
192
+ }
193
+ });
194
+ };
195
+ exports.AuthedImage = AuthedImage;
196
+ AuthedImage.displayName = 'AuthedImage';
197
+ const buildStyles = theme => _reactNative.StyleSheet.create({
198
+ placeholder: {
199
+ alignItems: 'center',
200
+ justifyContent: 'center',
201
+ backgroundColor: theme.colors.background.secondary,
202
+ borderRadius: theme.radius.md
203
+ },
204
+ fallback: {
205
+ alignItems: 'center',
206
+ justifyContent: 'center',
207
+ backgroundColor: theme.colors.background.secondary,
208
+ borderRadius: theme.radius.md,
209
+ borderWidth: theme.colors.border.width,
210
+ borderColor: theme.colors.border.secondary,
211
+ padding: theme.spacing.sm
212
+ },
213
+ stateFill: {
214
+ alignItems: 'center',
215
+ flex: 1,
216
+ justifyContent: 'center'
217
+ },
218
+ fallbackText: {
219
+ color: theme.colors.text.tertiary,
220
+ fontSize: theme.typography.fontSize.lg,
221
+ fontWeight: theme.typography.fontWeight.bold
222
+ }
223
+ });
224
+ var _default = exports.default = AuthedImage;
225
+ //# sourceMappingURL=AuthedImage.js.map
@@ -0,0 +1,253 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.touchEntry = exports.setAuthedImageCacheLimit = exports.releaseFetch = exports.readEntry = exports.isFresh = exports.getAuthedImageCacheLimit = exports.deriveCacheKey = exports.clearAuthedImageCache = exports.authedImageCacheStats = exports.acquireFetch = exports.DEFAULT_TTL_MS = void 0;
7
+ var _mobileCore = require("@webority-technologies/mobile-core");
8
+ const KEY_PREFIX = '@webority/authed-image/v1/';
9
+ const DATA_PREFIX = `${KEY_PREFIX}d/`;
10
+ const META_PREFIX = `${KEY_PREFIX}m/`;
11
+ const DEFAULT_BYTE_CEILING = 50 * 1024 * 1024;
12
+ const DEFAULT_TTL_MS = exports.DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
13
+ let byteCeiling = DEFAULT_BYTE_CEILING;
14
+
15
+ /**
16
+ * Ceiling for the total stored size of cached images. Exceeding it evicts the
17
+ * least recently used entries. An image cache with no ceiling grows until the
18
+ * OS reclaims the app's storage, which is the defect this replaces.
19
+ */
20
+ const setAuthedImageCacheLimit = bytes => {
21
+ if (!Number.isFinite(bytes) || bytes <= 0) {
22
+ 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.`);
23
+ }
24
+ byteCeiling = bytes;
25
+ };
26
+ exports.setAuthedImageCacheLimit = setAuthedImageCacheLimit;
27
+ const getAuthedImageCacheLimit = () => byteCeiling;
28
+ exports.getAuthedImageCacheLimit = getAuthedImageCacheLimit;
29
+ const hashString = input => {
30
+ let h1 = 0x811c9dc5;
31
+ let h2 = 0x1000193;
32
+ for (let i = 0; i < input.length; i += 1) {
33
+ const code = input.charCodeAt(i);
34
+ h1 = Math.imul(h1 ^ code, 16777619) >>> 0;
35
+ h2 = (Math.imul(h2 + code, 2654435761) ^ h2 >>> 7) >>> 0;
36
+ }
37
+ return `${h1.toString(16)}${h2.toString(16)}${input.length.toString(16)}`;
38
+ };
39
+
40
+ /**
41
+ * Hex only by construction, so it is always a safe storage key segment.
42
+ */
43
+ const deriveCacheKey = (uri, override) => hashString(override ?? uri);
44
+ exports.deriveCacheKey = deriveCacheKey;
45
+ const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
46
+ const encodeBase64 = buffer => {
47
+ const bytes = new Uint8Array(buffer);
48
+ let out = '';
49
+ for (let i = 0; i < bytes.length; i += 3) {
50
+ const b0 = bytes[i];
51
+ const b1 = i + 1 < bytes.length ? bytes[i + 1] : undefined;
52
+ const b2 = i + 2 < bytes.length ? bytes[i + 2] : undefined;
53
+ out += B64_ALPHABET[b0 >> 2];
54
+ out += B64_ALPHABET[(b0 & 0x03) << 4 | (b1 ?? 0) >> 4];
55
+ out += b1 === undefined ? '=' : B64_ALPHABET[((b1 & 0x0f) << 2 | (b2 ?? 0) >> 6) & 0x3f];
56
+ out += b2 === undefined ? '=' : B64_ALPHABET[b2 & 0x3f];
57
+ }
58
+ return out;
59
+ };
60
+
61
+ /**
62
+ * The cache is an OPTIMISATION, so a failing store must never stop an image
63
+ * rendering. That used to mean guarding on whether a backend existed at all,
64
+ * warning once and skipping the cache; under Expo-first the backend ships with
65
+ * the library, so "no backend" is no longer a state an app can be in and that
66
+ * branch was unreachable.
67
+ *
68
+ * What remains is a store that exists and FAILS, which the per-operation error
69
+ * handling below already covers: each read returns null and each write returns
70
+ * false, the image still fetches, and nothing is logged per image.
71
+ */
72
+ const cacheUsable = () => (0, _mobileCore.isStorageAvailable)();
73
+ const readEntry = async cacheKey => {
74
+ if (!cacheUsable()) {
75
+ return null;
76
+ }
77
+ const values = await _mobileCore.Storage.multiGet([`${DATA_PREFIX}${cacheKey}`, `${META_PREFIX}${cacheKey}`]);
78
+ const dataUri = values[`${DATA_PREFIX}${cacheKey}`];
79
+ const rawMeta = values[`${META_PREFIX}${cacheKey}`];
80
+ if (!dataUri || !rawMeta) {
81
+ return null;
82
+ }
83
+ try {
84
+ return {
85
+ dataUri,
86
+ meta: JSON.parse(rawMeta)
87
+ };
88
+ } catch {
89
+ return null;
90
+ }
91
+ };
92
+ exports.readEntry = readEntry;
93
+ const isFresh = (meta, ttlMs, now = Date.now()) => now - meta.fetchedAt < ttlMs;
94
+ exports.isFresh = isFresh;
95
+ const touchEntry = async (cacheKey, meta) => {
96
+ if (!cacheUsable()) {
97
+ return;
98
+ }
99
+ await _mobileCore.Storage.setItem(`${META_PREFIX}${cacheKey}`, JSON.stringify({
100
+ ...meta,
101
+ lastAccessedAt: Date.now()
102
+ }));
103
+ };
104
+ exports.touchEntry = touchEntry;
105
+ const listMeta = async () => {
106
+ if (!cacheUsable()) {
107
+ return [];
108
+ }
109
+ const keys = await _mobileCore.Storage.getAllKeys();
110
+ const metaKeys = keys.filter(key => key.startsWith(META_PREFIX));
111
+ if (metaKeys.length === 0) {
112
+ return [];
113
+ }
114
+ const values = await _mobileCore.Storage.multiGet(metaKeys);
115
+ const out = [];
116
+ for (const key of metaKeys) {
117
+ const raw = values[key];
118
+ if (!raw) {
119
+ continue;
120
+ }
121
+ try {
122
+ out.push({
123
+ cacheKey: key.slice(META_PREFIX.length),
124
+ meta: JSON.parse(raw)
125
+ });
126
+ } catch {
127
+ // A meta entry we cannot parse is unusable; dropping it from the ledger
128
+ // lets the next eviction pass remove its orphaned data entry.
129
+ }
130
+ }
131
+ return out;
132
+ };
133
+ const evictIfNeeded = async () => {
134
+ const entries = await listMeta();
135
+ let total = entries.reduce((sum, entry) => sum + entry.meta.bytes, 0);
136
+ if (total <= byteCeiling) {
137
+ return;
138
+ }
139
+ const ordered = [...entries].sort((a, b) => a.meta.lastAccessedAt - b.meta.lastAccessedAt);
140
+ const doomed = [];
141
+ for (const entry of ordered) {
142
+ if (total <= byteCeiling) {
143
+ break;
144
+ }
145
+ doomed.push(`${DATA_PREFIX}${entry.cacheKey}`, `${META_PREFIX}${entry.cacheKey}`);
146
+ total -= entry.meta.bytes;
147
+ }
148
+ if (doomed.length > 0) {
149
+ await _mobileCore.Storage.multiRemove(doomed);
150
+ _mobileCore.Logger.info(`[AuthedImage] evicted ${doomed.length / 2} cached image(s) over the byte ceiling`);
151
+ }
152
+ };
153
+ const clearAuthedImageCache = async () => {
154
+ if (!cacheUsable()) {
155
+ return;
156
+ }
157
+ const keys = await _mobileCore.Storage.getAllKeys();
158
+ const ours = keys.filter(key => key.startsWith(KEY_PREFIX));
159
+ if (ours.length > 0) {
160
+ await _mobileCore.Storage.multiRemove(ours);
161
+ }
162
+ };
163
+ exports.clearAuthedImageCache = clearAuthedImageCache;
164
+ const authedImageCacheStats = async () => {
165
+ const entries = await listMeta();
166
+ return {
167
+ entries: entries.length,
168
+ bytes: entries.reduce((sum, entry) => sum + entry.meta.bytes, 0)
169
+ };
170
+ };
171
+ exports.authedImageCacheStats = authedImageCacheStats;
172
+ const fetchAndStore = async (cacheKey, options, signal) => {
173
+ const headers = {
174
+ ...options.headers
175
+ };
176
+ if (options.authenticated) {
177
+ const stored = await (0, _mobileCore.getToken)(_mobileCore.ACCESS_TOKEN_KEY);
178
+ if (stored?.token) {
179
+ headers.Authorization = `Bearer ${stored.token}`;
180
+ }
181
+ }
182
+ const response = await fetch(options.uri, {
183
+ headers,
184
+ signal
185
+ });
186
+ if (!response.ok) {
187
+ 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).');
188
+ }
189
+ const buffer = await response.arrayBuffer();
190
+ const rawType = response.headers?.get?.('content-type');
191
+ const contentType = (rawType ? rawType.split(';')[0].trim() : '') || 'image/jpeg';
192
+ const dataUri = `data:${contentType};base64,${encodeBase64(buffer)}`;
193
+ const now = Date.now();
194
+ // `bytes` measures the stored data URI, not the decoded image: the ceiling
195
+ // exists to bound what sits in storage, and base64 costs about a third more.
196
+ const meta = {
197
+ fetchedAt: now,
198
+ bytes: dataUri.length,
199
+ contentType,
200
+ lastAccessedAt: now
201
+ };
202
+ if (cacheUsable()) {
203
+ await _mobileCore.Storage.multiSet({
204
+ [`${DATA_PREFIX}${cacheKey}`]: dataUri,
205
+ [`${META_PREFIX}${cacheKey}`]: JSON.stringify(meta)
206
+ });
207
+ await evictIfNeeded();
208
+ }
209
+ return {
210
+ dataUri,
211
+ meta
212
+ };
213
+ };
214
+ const inFlight = new Map();
215
+
216
+ /**
217
+ * Two mounts of the same image share one request. The controller is aborted
218
+ * only when the last waiter releases, so one unmount cannot cancel the fetch
219
+ * another mount is still waiting on.
220
+ */
221
+ const acquireFetch = (cacheKey, options) => {
222
+ const existing = inFlight.get(cacheKey);
223
+ if (existing) {
224
+ existing.waiters += 1;
225
+ return existing;
226
+ }
227
+ const controller = new AbortController();
228
+ const entry = {
229
+ controller,
230
+ waiters: 1,
231
+ promise: Promise.resolve()
232
+ };
233
+ entry.promise = fetchAndStore(cacheKey, options, controller.signal).finally(() => {
234
+ if (inFlight.get(cacheKey) === entry) {
235
+ inFlight.delete(cacheKey);
236
+ }
237
+ });
238
+ inFlight.set(cacheKey, entry);
239
+ return entry;
240
+ };
241
+ exports.acquireFetch = acquireFetch;
242
+ const releaseFetch = (cacheKey, entry) => {
243
+ entry.waiters -= 1;
244
+ if (entry.waiters > 0) {
245
+ return;
246
+ }
247
+ if (inFlight.get(cacheKey) === entry) {
248
+ inFlight.delete(cacheKey);
249
+ }
250
+ entry.controller.abort();
251
+ };
252
+ exports.releaseFetch = releaseFetch;
253
+ //# sourceMappingURL=authedImageCache.js.map
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "AuthedImage", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _AuthedImage.AuthedImage;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "authedImageCacheStats", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _authedImageCache.authedImageCacheStats;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "clearAuthedImageCache", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _authedImageCache.clearAuthedImageCache;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "default", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _AuthedImage.default;
28
+ }
29
+ });
30
+ Object.defineProperty(exports, "getAuthedImageCacheLimit", {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _authedImageCache.getAuthedImageCacheLimit;
34
+ }
35
+ });
36
+ Object.defineProperty(exports, "setAuthedImageCacheLimit", {
37
+ enumerable: true,
38
+ get: function () {
39
+ return _authedImageCache.setAuthedImageCacheLimit;
40
+ }
41
+ });
42
+ var _AuthedImage = _interopRequireWildcard(require("./AuthedImage.js"));
43
+ var _authedImageCache = require("./authedImageCache.js");
44
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
45
+ //# sourceMappingURL=index.js.map
@@ -126,6 +126,12 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
126
126
 
127
127
  // Convert a snap-point index → translateY position. -1 = closed.
128
128
  const yForIndex = (0, _react.useCallback)(idx => {
129
+ // NOT dead code, despite every ordinary call site clamping first.
130
+ // `expand()` has no empty-snapPoints guard (unlike `collapse`), so with
131
+ // snapPoints={[]} it arrives here out of range — and without this,
132
+ // `screenHeight - resolvedSnapPoints[idx]` is `screenHeight - undefined`,
133
+ // i.e. NaN into the sheet position, which breaks layout silently rather
134
+ // than degrading. Covered by the "degenerate snapPoints" tests.
129
135
  if (idx < 0 || idx >= resolvedSnapPoints.length) {
130
136
  return closedY;
131
137
  }
@@ -262,7 +268,7 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
262
268
  const closed = closedY;
263
269
  const top = minTopY;
264
270
  const canPanDownToClose = enablePanDownToClose;
265
- return _reactNativeGestureHandler.Gesture.Pan().activeOffsetY([-DRAG_ACTIVATION_OFFSET, DRAG_ACTIVATION_OFFSET]).failOffsetX([-15, 15]).onStart(() => {
271
+ return _reactNativeGestureHandler.Gesture.Pan().withTestId(testID ? `${testID}-pan-gesture` : 'bottom-sheet-pan-gesture').activeOffsetY([-DRAG_ACTIVATION_OFFSET, DRAG_ACTIVATION_OFFSET]).failOffsetX([-15, 15]).onStart(() => {
266
272
  'worklet';
267
273
 
268
274
  dragStartY.value = translateY.value;
@@ -312,7 +318,7 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
312
318
  }
313
319
  (0, _reactNativeReanimated.runOnJS)(handleSnapEnd)(nearestIdx, targets[nearestIdx]);
314
320
  });
315
- }, [closedY, dragStartY, enablePanDownToClose, handleSnapEnd, minTopY, snapTargets, translateY]);
321
+ }, [closedY, dragStartY, enablePanDownToClose, handleSnapEnd, minTopY, snapTargets, testID, translateY]);
316
322
 
317
323
  // ───────── Keyboard tracking ─────────
318
324
  // keyboardOffset is added to translateY in sheetStyle so we don't disturb
@@ -384,14 +390,14 @@ const BottomSheet = exports.BottomSheet = /*#__PURE__*/(0, _react.forwardRef)((p
384
390
  // gesture-handler/reanimated sheet doesn't fire reliably on RN 0.85 / Fabric
385
391
  // (the gesture system swallows the touch sequence). gorhom/bottom-sheet uses
386
392
  // the same Gesture.Tap() pattern for the same reason.
387
- const backdropTapGesture = (0, _react.useMemo)(() => _reactNativeGestureHandler.Gesture.Tap().maxDuration(250).onEnd((_e, success) => {
393
+ const backdropTapGesture = (0, _react.useMemo)(() => _reactNativeGestureHandler.Gesture.Tap().withTestId(testID ? `${testID}-backdrop-gesture` : 'bottom-sheet-backdrop-gesture').maxDuration(250).onEnd((_e, success) => {
388
394
  'worklet';
389
395
 
390
396
  if (!success) {
391
397
  return;
392
398
  }
393
399
  (0, _reactNativeReanimated.runOnJS)(handleBackdropPress)();
394
- }), [handleBackdropPress]);
400
+ }), [handleBackdropPress, testID]);
395
401
  const contextValue = (0, _react.useMemo)(() => ({
396
402
  snapIndex: currentIndex,
397
403
  snapPoints: resolvedSnapPoints,
@@ -82,8 +82,12 @@ const Drawer = exports.Drawer = /*#__PURE__*/(0, _react.forwardRef)((props, ref)
82
82
  return sheetSize;
83
83
  }
84
84
  }, [side, sheetSize]);
85
- const offset = (0, _reactNativeReanimated.useSharedValue)(closedOffset);
86
- const dragStart = (0, _reactNativeReanimated.useSharedValue)(closedOffset);
85
+
86
+ // Uncontrolled defaultVisible starts already open — the offset must start at the
87
+ // open position (0) too, or the sheet is logically open while visually still
88
+ // pinned at the closed edge until the next explicit open()/close()/drag.
89
+ const offset = (0, _reactNativeReanimated.useSharedValue)(defaultVisible ? 0 : closedOffset);
90
+ const dragStart = (0, _reactNativeReanimated.useSharedValue)(defaultVisible ? 0 : closedOffset);
87
91
  const [isOpen, setIsOpen] = (0, _react.useState)(defaultVisible);
88
92
  const isOpenShared = (0, _reactNativeReanimated.useSharedValue)(defaultVisible);
89
93
 
@@ -335,7 +339,6 @@ const Drawer = exports.Drawer = /*#__PURE__*/(0, _react.forwardRef)((props, ref)
335
339
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
336
340
  accessibilityRole: "alert",
337
341
  accessibilityLabel: accessibilityLabel ?? 'Drawer',
338
- accessibilityViewIsModal: isOpen,
339
342
  style: [sideLayout, {
340
343
  backgroundColor: theme.colors.background.elevated,
341
344
  ...theme.shadows.xl