@webority-technologies/mobile-ui 0.0.1 → 0.0.2

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
@@ -13,11 +13,23 @@ const FormContext = /*#__PURE__*/(0, _react.createContext)(null);
13
13
  /**
14
14
  * Provides a {@link useForm} instance to descendants so `<FormField name>`,
15
15
  * {@link useField} and {@link useFieldArray} can bind by name.
16
+ *
17
+ * Generic in `Values`, and it has to be. `FormApi` is invariant in `Values`
18
+ * because `setValues` accepts a `(prev: Values) => Values`, so a
19
+ * `FormApi<MyValues>` is not assignable to a `FormApi<FormValues>`. While the
20
+ * prop was declared at the base type, `useForm<MyValues>()` could not be handed
21
+ * to `<Form>` by ANY consumer, which is most of the reason to type a form at
22
+ * all. Untyped callers are unaffected: `Values` defaults to `FormValues`.
16
23
  */
17
24
  const Form = ({
18
25
  form,
19
26
  children
20
- }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(FormContext.Provider, {
27
+ }) =>
28
+ /*#__PURE__*/
29
+ // Stored at the base type on purpose: descendants bind by NAME through
30
+ // useField and never observe Values, so widening here is safe and is what
31
+ // lets a typed form be provided at all.
32
+ (0, _jsxRuntime.jsx)(FormContext.Provider, {
21
33
  value: form,
22
34
  children: children
23
35
  });
@@ -38,13 +38,25 @@ function useFieldArray(name) {
38
38
  copy.splice(index, 0, item);
39
39
  set(copy);
40
40
  },
41
+ // `move` and `replace` index into the array directly, so an out-of-range
42
+ // index corrupts it rather than failing: splice(9, 1) on a 3-item array
43
+ // yields `undefined`, which then gets spliced back IN, and `copy[9] = item`
44
+ // grows the array with six holes. Either one puts nulls into the submitted
45
+ // payload of a repeating field. `insert` and `remove` need no guard —
46
+ // splice clamps and filter no-ops.
41
47
  move: (from, to) => {
48
+ if (from < 0 || from >= fields.length) {
49
+ return;
50
+ }
42
51
  const copy = [...fields];
43
52
  const [moved] = copy.splice(from, 1);
44
- copy.splice(to, 0, moved);
53
+ copy.splice(Math.max(0, Math.min(to, copy.length)), 0, moved);
45
54
  set(copy);
46
55
  },
47
56
  replace: (index, item) => {
57
+ if (index < 0 || index >= fields.length) {
58
+ return;
59
+ }
48
60
  const copy = [...fields];
49
61
  copy[index] = item;
50
62
  set(copy);
@@ -3,54 +3,57 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.isGradientAvailable = exports.Gradient = void 0;
7
- var _react = _interopRequireDefault(require("react"));
6
+ exports.toBackgroundImage = exports.gradientAngle = exports.Gradient = void 0;
8
7
  var _reactNative = require("react-native");
9
8
  var _jsxRuntime = require("react/jsx-runtime");
10
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
9
  /**
12
- * Render a linear gradient using `react-native-linear-gradient` when available.
13
- *
14
- * The dep is OPTIONAL apps that never use gradients don't need it installed.
15
- * If `<Gradient>` (or any component that accepts a `gradient` prop) is reached
16
- * without the package present, we fall back to a flat View using the first
17
- * colour and emit a one-time dev warning so the omission is loud but
18
- * non-fatal.
10
+ * Render a linear gradient with React Native's own `experimental_backgroundImage`
11
+ * style prop. `GradientDefinition` keeps its vector-based `start`/`end` shape,
12
+ * which is translated to a CSS angle here.
19
13
  */
20
- let LinearGradientImpl = null;
21
- let warned = false;
22
- try {
23
- LinearGradientImpl = require('react-native-linear-gradient').default;
24
- } catch {
25
- // Optional peer; resolved lazily by consumers that need gradients.
26
- }
14
+
15
+ const DEFAULT_START = {
16
+ x: 0.5,
17
+ y: 0
18
+ };
19
+ const DEFAULT_END = {
20
+ x: 0.5,
21
+ y: 1
22
+ };
23
+ const round = value => Math.round(value * 1000) / 1000;
24
+
25
+ /**
26
+ * CSS gradient angles are measured clockwise from "to top", while the vectors
27
+ * live in a y-down 0..1 space, so the y delta is negated before the atan2.
28
+ */
29
+ const gradientAngle = (start = DEFAULT_START, end = DEFAULT_END) => {
30
+ const degrees = Math.atan2(end.x - start.x, start.y - end.y) * 180 / Math.PI;
31
+ return round((degrees % 360 + 360) % 360);
32
+ };
33
+ exports.gradientAngle = gradientAngle;
34
+ const toBackgroundImage = gradient => ({
35
+ type: 'linear-gradient',
36
+ direction: `${gradientAngle(gradient.start, gradient.end)}deg`,
37
+ colorStops: gradient.colors.map((color, index) => {
38
+ const location = gradient.locations?.[index];
39
+ return location === undefined ? {
40
+ color
41
+ } : {
42
+ color,
43
+ positions: [`${round(location * 100)}%`]
44
+ };
45
+ })
46
+ });
47
+ exports.toBackgroundImage = toBackgroundImage;
27
48
  const Gradient = ({
28
49
  gradient,
29
50
  style,
30
51
  children
31
- }) => {
32
- if (LinearGradientImpl) {
33
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(LinearGradientImpl, {
34
- colors: gradient.colors,
35
- locations: gradient.locations,
36
- start: gradient.start,
37
- end: gradient.end,
38
- style: style,
39
- children: children
40
- });
41
- }
42
- if (__DEV__ && !warned) {
43
- warned = true;
44
- console.warn('[@webority-technologies/mobile-ui] gradient prop set but react-native-linear-gradient is not installed. ' + 'Run `npm install react-native-linear-gradient` to enable.');
45
- }
46
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
47
- style: [{
48
- backgroundColor: gradient.colors[0]
49
- }, style],
50
- children: children
51
- });
52
- };
52
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
53
+ style: [{
54
+ experimental_backgroundImage: [toBackgroundImage(gradient)]
55
+ }, style],
56
+ children: children
57
+ });
53
58
  exports.Gradient = Gradient;
54
- const isGradientAvailable = () => LinearGradientImpl != null;
55
- exports.isGradientAvailable = isGradientAvailable;
56
59
  //# sourceMappingURL=Gradient.js.map
@@ -34,10 +34,10 @@ Object.defineProperty(exports, "fontFor", {
34
34
  }
35
35
  });
36
36
  exports.getTheme = exports.getColorMode = void 0;
37
- Object.defineProperty(exports, "isGradientAvailable", {
37
+ Object.defineProperty(exports, "gradientAngle", {
38
38
  enumerable: true,
39
39
  get: function () {
40
- return _Gradient.isGradientAvailable;
40
+ return _Gradient.gradientAngle;
41
41
  }
42
42
  });
43
43
  Object.defineProperty(exports, "lightTheme", {
@@ -60,6 +60,12 @@ Object.defineProperty(exports, "setNativeValue", {
60
60
  }
61
61
  });
62
62
  exports.subscribeTheme = exports.setTheme = void 0;
63
+ Object.defineProperty(exports, "toBackgroundImage", {
64
+ enumerable: true,
65
+ get: function () {
66
+ return _Gradient.toBackgroundImage;
67
+ }
68
+ });
63
69
  Object.defineProperty(exports, "useTheme", {
64
70
  enumerable: true,
65
71
  get: function () {
@@ -89,6 +95,22 @@ const recompute = () => {
89
95
  currentTheme = overrides ? (0, _merge.mergeTheme)(base, overrides) : base;
90
96
  subscribers.forEach(cb => cb());
91
97
  };
98
+
99
+ /**
100
+ * Set the override object for a mode (defaults to the active one).
101
+ *
102
+ * REPLACES that mode's overrides; it does not accumulate them. Calling
103
+ * `setTheme({ colors })` and then `setTheme({ spacing })` leaves ONLY the
104
+ * spacing override live — the colours silently revert to the base theme. Pass
105
+ * one complete override object per mode, or merge them yourself before calling:
106
+ *
107
+ * setTheme({ ...myColors, ...mySpacing });
108
+ *
109
+ * This is deliberate (one override object per mode, last call wins) and is
110
+ * pinned by a test. Do not "fix" it to merge without treating that as the
111
+ * behaviour change it is: an app relying on replace to CLEAR a previous
112
+ * override would silently keep it.
113
+ */
92
114
  const setTheme = (overrides, mode) => {
93
115
  if (mode) {
94
116
  currentMode = mode;
@@ -4,25 +4,20 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.triggerHaptic = exports.setHapticImplementation = exports.resolveHaptic = void 0;
7
+ var Haptics = _interopRequireWildcard(require("expo-haptics"));
7
8
  var _reactNative = require("react-native");
9
+ 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); }
8
10
  let hapticImpl = null;
9
- const tryLoadOptional = () => {
10
- if (hapticImpl) {
11
- return;
12
- }
13
- try {
14
- // Lazily try to load the user's optional haptic dep without making it a hard requirement.
15
- const haptic = require('react-native-haptic-feedback');
16
- const trigger = haptic.default?.trigger ?? haptic.trigger;
17
- if (typeof trigger === 'function') {
18
- hapticImpl = type => trigger(type, {
19
- enableVibrateFallback: true
20
- });
21
- }
22
- } catch {
23
- // Ignored: not installed; fall back to Vibration on Android, no-op on iOS.
24
- }
25
- };
11
+
12
+ /**
13
+ * Only what a test or the showcase injected. The default is expo-haptics, which
14
+ * this module imports directly: `expo` is a required peer, so the 48 components
15
+ * that fire haptics get them without any app wiring anything.
16
+ *
17
+ * The Vibration fallback below is now only the FAILURE path — a device whose
18
+ * haptic engine is disabled rejects, and Android still gets something.
19
+ */
20
+
26
21
  const fallback = type => {
27
22
  if (_reactNative.Platform.OS === 'android') {
28
23
  const map = {
@@ -37,8 +32,23 @@ const fallback = type => {
37
32
  _reactNative.Vibration.vibrate(map[type] ?? 10);
38
33
  }
39
34
  };
35
+
36
+ /**
37
+ * expo-haptics, which splits what this module treats as one vocabulary across
38
+ * three functions. The promises are deliberately swallowed: the seam is
39
+ * synchronous, a device with the engine disabled rejects, and an unhandled
40
+ * rejection from a button press must never become a crash.
41
+ */
42
+ const EXPO_HANDLERS = {
43
+ selection: () => Haptics.selectionAsync(),
44
+ impactLight: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light),
45
+ impactMedium: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium),
46
+ impactHeavy: () => Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy),
47
+ notificationSuccess: () => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success),
48
+ notificationWarning: () => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning),
49
+ notificationError: () => Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error)
50
+ };
40
51
  const triggerHaptic = (type = 'selection') => {
41
- tryLoadOptional();
42
52
  if (hapticImpl) {
43
53
  try {
44
54
  hapticImpl(type);
@@ -47,8 +57,18 @@ const triggerHaptic = (type = 'selection') => {
47
57
  /* ignore */
48
58
  }
49
59
  }
60
+ const handler = EXPO_HANDLERS[type];
61
+ if (handler) {
62
+ handler().catch(() => fallback(type));
63
+ return;
64
+ }
50
65
  fallback(type);
51
66
  };
67
+
68
+ /**
69
+ * Replace the haptic backend. Intended for TESTS and for the showcase; an app
70
+ * gets expo-haptics without wiring anything. Pass `null` to restore it.
71
+ */
52
72
  exports.triggerHaptic = triggerHaptic;
53
73
  const setHapticImplementation = impl => {
54
74
  hapticImpl = impl;
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { Image, StyleSheet, Text, View } from 'react-native';
5
+ import { useTheme } from "../../theme/index.js";
6
+ import { Spinner } from "../Spinner/index.js";
7
+ import { acquireFetch, DEFAULT_TTL_MS, deriveCacheKey, isFresh, readEntry, releaseFetch, touchEntry } from "./authedImageCache.js";
8
+ import { jsx as _jsx } from "react/jsx-runtime";
9
+ const toError = value => value instanceof Error ? value : new Error(String(value));
10
+ const AuthedImage = props => {
11
+ const {
12
+ uri,
13
+ headers,
14
+ authenticated = true,
15
+ ttlMs = DEFAULT_TTL_MS,
16
+ placeholder,
17
+ fallback,
18
+ onLoadStart,
19
+ onLoadEnd,
20
+ onError,
21
+ cacheKey,
22
+ testID,
23
+ accessibilityLabel,
24
+ style,
25
+ ...imageProps
26
+ } = props;
27
+ const theme = useTheme();
28
+ const styles = useMemo(() => buildStyles(theme), [theme]);
29
+ const [state, setState] = useState({
30
+ status: 'loading'
31
+ });
32
+
33
+ // Callbacks live behind refs so a caller passing inline arrows does not
34
+ // restart the fetch on every render.
35
+ const callbacks = useRef({
36
+ onLoadStart,
37
+ onLoadEnd,
38
+ onError
39
+ });
40
+ callbacks.current = {
41
+ onLoadStart,
42
+ onLoadEnd,
43
+ onError
44
+ };
45
+
46
+ // Compared by value: a headers object literal is a new identity every render.
47
+ const headersKey = JSON.stringify(headers ?? {});
48
+ useEffect(() => {
49
+ let cancelled = false;
50
+ let acquired = null;
51
+ const finish = next => {
52
+ if (cancelled) {
53
+ return;
54
+ }
55
+ setState(next);
56
+ callbacks.current.onLoadEnd?.();
57
+ };
58
+ const run = async () => {
59
+ if (!uri) {
60
+ 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.');
61
+ if (!cancelled) {
62
+ setState({
63
+ status: 'error'
64
+ });
65
+ callbacks.current.onError?.(error);
66
+ }
67
+ return;
68
+ }
69
+ setState({
70
+ status: 'loading'
71
+ });
72
+ callbacks.current.onLoadStart?.();
73
+ const key = deriveCacheKey(uri, cacheKey);
74
+ const cached = await readEntry(key);
75
+ if (cancelled) {
76
+ return;
77
+ }
78
+ if (cached && isFresh(cached.meta, ttlMs)) {
79
+ void touchEntry(key, cached.meta);
80
+ finish({
81
+ status: 'ready',
82
+ dataUri: cached.dataUri
83
+ });
84
+ return;
85
+ }
86
+ const entry = acquireFetch(key, {
87
+ uri,
88
+ headers: JSON.parse(headersKey),
89
+ authenticated
90
+ });
91
+ acquired = {
92
+ key,
93
+ entry
94
+ };
95
+ try {
96
+ const result = await entry.promise;
97
+ finish({
98
+ status: 'ready',
99
+ dataUri: result.dataUri
100
+ });
101
+ } catch (error) {
102
+ if (cancelled) {
103
+ return;
104
+ }
105
+ // A stale image beats an empty box: the entry is only expired, and the
106
+ // refetch that would have replaced it failed.
107
+ if (cached) {
108
+ finish({
109
+ status: 'ready',
110
+ dataUri: cached.dataUri
111
+ });
112
+ return;
113
+ }
114
+ setState({
115
+ status: 'error'
116
+ });
117
+ callbacks.current.onError?.(toError(error));
118
+ callbacks.current.onLoadEnd?.();
119
+ }
120
+ };
121
+ void run();
122
+ return () => {
123
+ cancelled = true;
124
+ if (acquired) {
125
+ releaseFetch(acquired.key, acquired.entry);
126
+ }
127
+ };
128
+ }, [uri, cacheKey, ttlMs, authenticated, headersKey]);
129
+
130
+ // The base `testID` sits on the root in EVERY state, with the state-specific
131
+ // suffixes nested inside it. A consumer that can only find the component while
132
+ // it happens to be loaded cannot wait for it to load.
133
+ if (state.status === 'loading') {
134
+ if (placeholder !== undefined) {
135
+ return /*#__PURE__*/_jsx(View, {
136
+ style: style,
137
+ testID: testID,
138
+ children: placeholder
139
+ });
140
+ }
141
+ return /*#__PURE__*/_jsx(View, {
142
+ style: [styles.placeholder, style],
143
+ testID: testID,
144
+ children: /*#__PURE__*/_jsx(View, {
145
+ style: styles.stateFill,
146
+ testID: testID ? `${testID}-placeholder` : undefined,
147
+ pointerEvents: "none",
148
+ children: /*#__PURE__*/_jsx(Spinner, {
149
+ size: "sm",
150
+ accessibilityLabel: accessibilityLabel ?? 'Loading image'
151
+ })
152
+ })
153
+ });
154
+ }
155
+ if (state.status === 'error') {
156
+ if (fallback !== undefined) {
157
+ return /*#__PURE__*/_jsx(View, {
158
+ style: style,
159
+ testID: testID,
160
+ children: fallback
161
+ });
162
+ }
163
+ return /*#__PURE__*/_jsx(View, {
164
+ style: [styles.fallback, style],
165
+ testID: testID,
166
+ children: /*#__PURE__*/_jsx(View, {
167
+ accessible: true,
168
+ accessibilityRole: "image",
169
+ accessibilityLabel: accessibilityLabel ? `${accessibilityLabel}, image unavailable` : 'Image unavailable',
170
+ testID: testID ? `${testID}-fallback` : undefined,
171
+ style: styles.stateFill,
172
+ children: /*#__PURE__*/_jsx(Text, {
173
+ style: styles.fallbackText,
174
+ children: "!"
175
+ })
176
+ })
177
+ });
178
+ }
179
+ return /*#__PURE__*/_jsx(Image, {
180
+ ...imageProps,
181
+ accessible: true,
182
+ accessibilityRole: "image",
183
+ accessibilityLabel: accessibilityLabel,
184
+ testID: testID,
185
+ style: style,
186
+ source: {
187
+ uri: state.dataUri
188
+ }
189
+ });
190
+ };
191
+ AuthedImage.displayName = 'AuthedImage';
192
+ const buildStyles = theme => StyleSheet.create({
193
+ placeholder: {
194
+ alignItems: 'center',
195
+ justifyContent: 'center',
196
+ backgroundColor: theme.colors.background.secondary,
197
+ borderRadius: theme.radius.md
198
+ },
199
+ fallback: {
200
+ alignItems: 'center',
201
+ justifyContent: 'center',
202
+ backgroundColor: theme.colors.background.secondary,
203
+ borderRadius: theme.radius.md,
204
+ borderWidth: theme.colors.border.width,
205
+ borderColor: theme.colors.border.secondary,
206
+ padding: theme.spacing.sm
207
+ },
208
+ stateFill: {
209
+ alignItems: 'center',
210
+ flex: 1,
211
+ justifyContent: 'center'
212
+ },
213
+ fallbackText: {
214
+ color: theme.colors.text.tertiary,
215
+ fontSize: theme.typography.fontSize.lg,
216
+ fontWeight: theme.typography.fontWeight.bold
217
+ }
218
+ });
219
+ export { AuthedImage };
220
+ export default AuthedImage;
221
+ //# sourceMappingURL=AuthedImage.js.map