@colixsystems/widget-sdk 0.202.0 → 0.205.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -124,6 +124,11 @@ export {
124
124
  AudioPlayerError,
125
125
  } from "./audio.js";
126
126
  export { useDatastoreFigure } from "./datastore-figure.js";
127
+ export {
128
+ useUserStats,
129
+ useUserActivity,
130
+ useInactiveUsers,
131
+ } from "./stats-hooks.js";
127
132
  // sc-6607 — `<Overlay>` is one shared component (./overlay-view.js) bound to
128
133
  // each platform's primitives, so a widget's modal escapes its container box
129
134
  // identically on the Player and the Expo export.
@@ -261,6 +266,8 @@ export { TextField } from "./text-field.js";
261
266
  export { Accordion, AccordionItem } from "./accordion.js";
262
267
  // A themed tab strip; follows the theme's `tabs` scope.
263
268
  export { Tabs } from "./tabs.js";
269
+ // Themed loading states; follow the theme's `loading` scope.
270
+ export { Spinner, Skeleton } from "./loading.js";
264
271
  // The semantic text tiers, shared with the built-in widgets.
265
272
  export {
266
273
  textRoleStyle,
@@ -122,6 +122,11 @@ export {
122
122
  AudioPlayerError,
123
123
  } from "./audio.js";
124
124
  export { useDatastoreFigure } from "./datastore-figure.js";
125
+ export {
126
+ useUserStats,
127
+ useUserActivity,
128
+ useInactiveUsers,
129
+ } from "./stats-hooks.js";
125
130
  // sc-6607 — see the note on the web mirror in ./index.js.
126
131
  export { Overlay } from "./overlay.native.js";
127
132
  // sc-8104 — see the note on the web mirror in ./index.js.
@@ -254,6 +259,8 @@ export { TextField } from "./text-field.native.js";
254
259
  export { Accordion, AccordionItem } from "./accordion.native.js";
255
260
  // A themed tab strip; follows the theme's `tabs` scope.
256
261
  export { Tabs } from "./tabs.native.js";
262
+ // Themed loading states; follow the theme's `loading` scope.
263
+ export { Spinner, Skeleton } from "./loading.native.js";
257
264
  // The semantic text tiers, shared with the built-in widgets.
258
265
  export {
259
266
  textRoleStyle,
package/dist/linter.cjs CHANGED
@@ -771,7 +771,8 @@ function _scopeRules(source, manifest) {
771
771
  // the edit-style write scope above.
772
772
  if (usesUsersHook && !firedUsersDelete) {
773
773
  for (const m of USER_DELETE_METHODS) {
774
- const re = new RegExp(`\\.${m}\\s*\\(`);
774
+ // remove(userId) takes an id; an event subscription's remove() does not.
775
+ const re = new RegExp(`\\.${m}\\s*\\(\\s*[^)\\s]`);
775
776
  if (re.test(line)) {
776
777
  if (
777
778
  !declared.has("users.delete:*") &&
@@ -791,7 +792,10 @@ function _scopeRules(source, manifest) {
791
792
  }
792
793
  if (usesGroupsHook && !firedGroupsWrite) {
793
794
  for (const m of GROUP_MUTATION_METHODS) {
794
- const re = new RegExp(`\\.${m}\\s*\\(`);
795
+ // Not group writes: `StyleSheet.create(`, and a subscription's bare remove().
796
+ const lookbehind = m === "create" ? "(?<!StyleSheet)" : "";
797
+ const needsArgs = m === "remove" ? "\\s*[^)\\s]" : "";
798
+ const re = new RegExp(`${lookbehind}\\.${m}\\s*\\(${needsArgs}`);
795
799
  if (re.test(line)) {
796
800
  if (m === "remove" && usesUsersHook) continue;
797
801
  if (
package/dist/linter.js CHANGED
@@ -723,7 +723,8 @@ function _scopeRules(source, manifest) {
723
723
  // the edit-style write scope above.
724
724
  if (usesUsersHook && !firedUsersDelete) {
725
725
  for (const m of USER_DELETE_METHODS) {
726
- const re = new RegExp(`\\.${m}\\s*\\(`);
726
+ // remove(userId) takes an id; an event subscription's remove() does not.
727
+ const re = new RegExp(`\\.${m}\\s*\\(\\s*[^)\\s]`);
727
728
  if (re.test(line)) {
728
729
  if (
729
730
  !declared.has("users.delete:*") &&
@@ -743,7 +744,10 @@ function _scopeRules(source, manifest) {
743
744
  }
744
745
  if (usesGroupsHook && !firedGroupsWrite) {
745
746
  for (const m of GROUP_MUTATION_METHODS) {
746
- const re = new RegExp(`\\.${m}\\s*\\(`);
747
+ // Not group writes: `StyleSheet.create(`, and a subscription's bare remove().
748
+ const lookbehind = m === "create" ? "(?<!StyleSheet)" : "";
749
+ const needsArgs = m === "remove" ? "\\s*[^)\\s]" : "";
750
+ const re = new RegExp(`${lookbehind}\\.${m}\\s*\\(${needsArgs}`);
747
751
  if (re.test(line)) {
748
752
  // `.remove(` is also a useUsers method; only blame groups
749
753
  // when the source also uses the groups hook AND the source
@@ -0,0 +1,122 @@
1
+ // The values `<Spinner>` and `<Skeleton>` paint with. Platform-free, like
2
+ // tabs-tokens.js, so both hosts draw one loading look and tests can pin it.
3
+
4
+ import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
5
+ import { normaliseThemeComponents } from "./theme-components.js";
6
+
7
+ // `md` and `lg` are the platform spinner's small and large.
8
+ export const SPINNER_SIZES = Object.freeze({ sm: 16, md: 20, lg: 36 });
9
+ export const DEFAULT_SPINNER_SIZE = "md";
10
+ export const SPINNER_TURN_MS = 750;
11
+
12
+ export const SKELETON_SHAPES = Object.freeze(["text", "block", "circle", "listRow"]);
13
+ export const DEFAULT_SKELETON_SHAPE = "text";
14
+ // One way of the pulse; a full breath is twice this.
15
+ export const SKELETON_PULSE_MS = 900;
16
+ export const SKELETON_PULSE_MIN_OPACITY = 0.5;
17
+ export const SKELETON_TEXT_HEIGHT = 12;
18
+ export const SKELETON_TEXT_GAP = 8;
19
+ export const SKELETON_BLOCK_HEIGHT = 96;
20
+ export const SKELETON_CIRCLE_SIZE = 40;
21
+ export const SKELETON_LIST_ROWS = 3;
22
+
23
+ // A paragraph's last line stops short, as real text does.
24
+ const LAST_LINE_WIDTH = "60%";
25
+ // A list row's title and subtitle.
26
+ export const SKELETON_LIST_ROW_LINE_WIDTHS = Object.freeze(["45%", "30%"]);
27
+
28
+ // Per-instance overrides a widget may pass, named as the theme tokens are.
29
+ export const LOADING_TOKEN_NAMES = Object.freeze(["skeletonColor", "skeletonRadius"]);
30
+
31
+ function isSet(value) {
32
+ return value !== undefined && value !== null && value !== "";
33
+ }
34
+
35
+ function pick(overrides, scope, name) {
36
+ if (overrides && isSet(overrides[name])) return overrides[name];
37
+ if (scope && isSet(scope[name])) return scope[name];
38
+ return undefined;
39
+ }
40
+
41
+ /**
42
+ * Resolve a skeleton's look: explicit override, then the theme's `loading`
43
+ * scope, then the theme tokens. The scope passes the contract's own bounds, so
44
+ * a hand-edited theme cannot reach a native style unvalidated.
45
+ *
46
+ * @param {object} theme — the resolved workspace theme (`useTheme()` shape).
47
+ * @param {object} [overrides] — per-instance values keyed by LOADING_TOKEN_NAMES.
48
+ */
49
+ export function resolveLoadingTokens(theme, overrides) {
50
+ const base = theme && typeof theme === "object" ? theme : DEFAULT_THEME_TOKENS;
51
+ const colors = base.colors || DEFAULT_THEME_TOKENS.colors;
52
+ const radii = base.radii || DEFAULT_THEME_TOKENS.radii;
53
+ const scope = normaliseThemeComponents({
54
+ loading: base.components && base.components.loading,
55
+ }).loading;
56
+ return {
57
+ skeletonColor: pick(overrides, scope, "skeletonColor") ?? colors.surfaceMuted,
58
+ skeletonRadius: pick(overrides, scope, "skeletonRadius") ?? radii.sm,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * A spinner's colour: the one a widget passed, else the theme's `loader`, the
64
+ * app's Loading Indicator colour, which each painted surface re-derives.
65
+ */
66
+ export function resolveSpinnerColor(theme, color) {
67
+ if (isSet(color)) return color;
68
+ const colors = (theme && theme.colors) || DEFAULT_THEME_TOKENS.colors;
69
+ return colors.loader || DEFAULT_THEME_TOKENS.colors.loader;
70
+ }
71
+
72
+ /** A spinner's diameter: a named size, or the number a widget passed. */
73
+ export function spinnerDiameter(size) {
74
+ if (typeof size === "number" && Number.isFinite(size) && size > 0) return size;
75
+ return SPINNER_SIZES[size] || SPINNER_SIZES[DEFAULT_SPINNER_SIZE];
76
+ }
77
+
78
+ export function spinnerStrokeWidth(diameter) {
79
+ return Math.max(2, Math.round(diameter / 8));
80
+ }
81
+
82
+ export function normaliseSkeletonShape(shape) {
83
+ return SKELETON_SHAPES.includes(shape) ? shape : DEFAULT_SKELETON_SHAPE;
84
+ }
85
+
86
+ /**
87
+ * The box one bone is drawn in.
88
+ *
89
+ * @param {"text"|"block"|"circle"} shape
90
+ * @param {{ width?: number|string, height?: number, size?: number }} dims
91
+ * @param {number} radius — the resolved `skeletonRadius`.
92
+ */
93
+ export function skeletonBox(shape, dims, radius) {
94
+ const { width, height, size } = dims || {};
95
+ if (shape === "circle") {
96
+ const diameter = isSet(size) ? size : SKELETON_CIRCLE_SIZE;
97
+ return { width: diameter, height: diameter, borderRadius: diameter / 2 };
98
+ }
99
+ return {
100
+ width: isSet(width) ? width : "100%",
101
+ height: isSet(height)
102
+ ? height
103
+ : shape === "block"
104
+ ? SKELETON_BLOCK_HEIGHT
105
+ : SKELETON_TEXT_HEIGHT,
106
+ borderRadius: radius,
107
+ };
108
+ }
109
+
110
+ /** The width of each line of a `text` skeleton, top to bottom. */
111
+ export function skeletonLineWidths(lines, width) {
112
+ const count = Math.max(1, Math.floor(Number(lines) || 1));
113
+ const full = isSet(width) ? width : "100%";
114
+ return Array.from({ length: count }, (_, i) =>
115
+ count > 1 && i === count - 1 ? LAST_LINE_WIDTH : full,
116
+ );
117
+ }
118
+
119
+ export function skeletonListRowCount(count) {
120
+ const n = Math.floor(Number(count));
121
+ return Number.isFinite(n) && n > 0 ? n : SKELETON_LIST_ROWS;
122
+ }
@@ -0,0 +1,330 @@
1
+ // The ONE implementation behind `<Spinner>` and `<Skeleton>`. The bindings
2
+ // (`loading.js` / `loading.native.js`) differ only in their primitives.
3
+
4
+ import React from "react";
5
+ import { useHostTheme, useHostSharedText } from "./hooks.js";
6
+ import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
7
+ import { CONTRACT } from "./contract.js";
8
+ import { resolveListTokens, listRowFrame } from "./list-tokens.js";
9
+ import { textRoleStyle } from "./text-roles.js";
10
+ import { flattenStyle } from "./flatten-style.js";
11
+ import {
12
+ resolveLoadingTokens,
13
+ resolveSpinnerColor,
14
+ spinnerDiameter,
15
+ spinnerStrokeWidth,
16
+ normaliseSkeletonShape,
17
+ skeletonBox,
18
+ skeletonLineWidths,
19
+ skeletonListRowCount,
20
+ SPINNER_TURN_MS,
21
+ SKELETON_PULSE_MS,
22
+ SKELETON_PULSE_MIN_OPACITY,
23
+ SKELETON_TEXT_GAP,
24
+ SKELETON_LIST_ROW_LINE_WIDTHS,
25
+ } from "./loading-tokens.js";
26
+
27
+ const LOADING_FALLBACK = CONTRACT.sharedTranslationKeys.loading.en;
28
+ const FILL = { position: "absolute", top: 0, right: 0, bottom: 0, left: 0 };
29
+ const TRACK_OPACITY = 0.2;
30
+ const HIDDEN = { "aria-hidden": true, importantForAccessibility: "no-hide-descendants" };
31
+
32
+ // A labelled element in the busy state, on both hosts.
33
+ function busyRegion(label) {
34
+ return { accessible: true, accessibilityLabel: label, "aria-busy": true };
35
+ }
36
+
37
+ // The web needs a role to expose the label; native has none for `status`.
38
+ const LOADING_REGION = { role: "status" };
39
+
40
+ /**
41
+ * Bind the shared loading components to one platform's primitives.
42
+ *
43
+ * @param {{View: any, Text: any, Animated: any, Easing: any, AccessibilityInfo: any}} rn
44
+ * @param {{ useNativeDriver: boolean }} options — false where no native driver exists.
45
+ */
46
+ export function makeLoading(rn, { useNativeDriver }) {
47
+ const { View, Text, Animated, Easing, AccessibilityInfo } = rn;
48
+
49
+ // One listener for every loading state on screen. Reduced until the host
50
+ // answers, so nothing moves for a user who asked it not to.
51
+ const motion = (() => {
52
+ let reduced = true;
53
+ let subscription = null;
54
+ const listeners = new Set();
55
+ const set = (value) => {
56
+ if (reduced === !!value) return;
57
+ reduced = !!value;
58
+ listeners.forEach((notify) => notify());
59
+ };
60
+ const subscribe = (notify) => {
61
+ listeners.add(notify);
62
+ if (listeners.size === 1) {
63
+ AccessibilityInfo.isReduceMotionEnabled().then(set, () => {});
64
+ subscription = AccessibilityInfo.addEventListener("reduceMotionChanged", set);
65
+ }
66
+ return () => {
67
+ listeners.delete(notify);
68
+ if (listeners.size > 0) return;
69
+ if (subscription && typeof subscription.remove === "function") subscription.remove();
70
+ subscription = null;
71
+ };
72
+ };
73
+ return { subscribe, isReduced: () => reduced };
74
+ })();
75
+
76
+ function useReducedMotion() {
77
+ return React.useSyncExternalStore(motion.subscribe, motion.isReduced, motion.isReduced);
78
+ }
79
+
80
+ // One running animation however many components draw from it. Nothing is
81
+ // allocated until the first loading state asks for it.
82
+ function sharedLoop(build, project) {
83
+ let value = null;
84
+ let output = null;
85
+ let users = 0;
86
+ let loop = null;
87
+ const animated = () => {
88
+ if (!value) {
89
+ value = new Animated.Value(0);
90
+ output = project(value);
91
+ }
92
+ return output;
93
+ };
94
+ const retain = () => {
95
+ animated();
96
+ users += 1;
97
+ if (users === 1) {
98
+ loop = Animated.loop(build(value));
99
+ loop.start();
100
+ }
101
+ return () => {
102
+ users -= 1;
103
+ if (users > 0) return;
104
+ loop.stop();
105
+ loop = null;
106
+ value.setValue(0);
107
+ };
108
+ };
109
+ return { animated, retain };
110
+ }
111
+
112
+ const turn = sharedLoop(
113
+ (value) =>
114
+ Animated.timing(value, {
115
+ toValue: 1,
116
+ duration: SPINNER_TURN_MS,
117
+ easing: Easing.linear,
118
+ useNativeDriver,
119
+ }),
120
+ (value) => value.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "360deg"] }),
121
+ );
122
+
123
+ const breathe = (value, toValue) =>
124
+ Animated.timing(value, {
125
+ toValue,
126
+ duration: SKELETON_PULSE_MS,
127
+ easing: Easing.inOut(Easing.ease),
128
+ useNativeDriver,
129
+ });
130
+ const pulse = sharedLoop(
131
+ (value) => Animated.sequence([breathe(value, 1), breathe(value, 0)]),
132
+ (value) =>
133
+ value.interpolate({ inputRange: [0, 1], outputRange: [1, SKELETON_PULSE_MIN_OPACITY] }),
134
+ );
135
+
136
+ function useSharedLoop(loop, moving) {
137
+ React.useEffect(() => (moving ? loop.retain() : undefined), [loop, moving]);
138
+ }
139
+
140
+ function useLoadingTheme(explicitTheme) {
141
+ const hostTheme = useHostTheme();
142
+ // A host drawing outside a widget context passes its own theme.
143
+ return explicitTheme || hostTheme || DEFAULT_THEME_TOKENS;
144
+ }
145
+
146
+ function Spinner({
147
+ size,
148
+ color,
149
+ label,
150
+ accessibilityLabel,
151
+ theme: explicitTheme,
152
+ style,
153
+ testID,
154
+ }) {
155
+ const theme = useLoadingTheme(explicitTheme);
156
+ const loadingText = useHostSharedText("loading", LOADING_FALLBACK);
157
+ const moving = !useReducedMotion();
158
+ useSharedLoop(turn, moving);
159
+
160
+ const ink = resolveSpinnerColor(theme, color);
161
+ const spacing = theme.spacing || DEFAULT_THEME_TOKENS.spacing;
162
+ const diameter = spinnerDiameter(size);
163
+ const ring = {
164
+ ...FILL,
165
+ borderRadius: diameter / 2,
166
+ borderWidth: spinnerStrokeWidth(diameter),
167
+ };
168
+ const hasLabel = label !== undefined && label !== null && label !== "";
169
+ // A string caption is already the spinner's name; a node speaks for itself.
170
+ const named = typeof label === "string" && label !== "";
171
+
172
+ return React.createElement(
173
+ View,
174
+ {
175
+ ...busyRegion(accessibilityLabel || (named && label) || loadingText),
176
+ accessibilityRole: "progressbar",
177
+ style: [{ alignItems: "center", justifyContent: "center", gap: spacing.sm }, style],
178
+ testID,
179
+ },
180
+ React.createElement(
181
+ Animated.View,
182
+ {
183
+ ...HIDDEN,
184
+ style: [
185
+ { width: diameter, height: diameter },
186
+ moving ? { transform: [{ rotate: turn.animated() }] } : null,
187
+ ],
188
+ },
189
+ React.createElement(View, {
190
+ style: [ring, { borderColor: ink, opacity: TRACK_OPACITY }],
191
+ }),
192
+ React.createElement(View, {
193
+ style: [ring, { borderColor: "transparent", borderTopColor: ink }],
194
+ }),
195
+ ),
196
+ hasLabel
197
+ ? React.createElement(
198
+ Text,
199
+ {
200
+ ...(named ? HIDDEN : null),
201
+ style: textRoleStyle(
202
+ "caption",
203
+ theme.typography || DEFAULT_THEME_TOKENS.typography,
204
+ theme.colors || DEFAULT_THEME_TOKENS.colors,
205
+ ),
206
+ },
207
+ label,
208
+ )
209
+ : null,
210
+ );
211
+ }
212
+ Spinner.displayName = "Spinner";
213
+
214
+ // True inside a `<Skeleton>` that already is the loading region.
215
+ const RegionContext = React.createContext(false);
216
+
217
+ // A bone the widget sizes by aspect ratio must not keep the default height.
218
+ function boneBox(box, style) {
219
+ if (flattenStyle(style).aspectRatio === undefined) return box;
220
+ const { height: _defaultHeight, ...rest } = box;
221
+ return rest;
222
+ }
223
+
224
+ function boneStyle(box, tokens, style) {
225
+ return [boneBox(box, style), { backgroundColor: tokens.skeletonColor }, style];
226
+ }
227
+
228
+ function bone(shape, dims, tokens, key) {
229
+ return React.createElement(View, {
230
+ ...HIDDEN,
231
+ key,
232
+ style: boneStyle(skeletonBox(shape, dims, tokens.skeletonRadius), tokens),
233
+ });
234
+ }
235
+
236
+ function listRows(theme, tokens, { count, leading = true }) {
237
+ const spacing = theme.spacing || DEFAULT_THEME_TOKENS.spacing;
238
+ // The real rows' own frame, so nothing shifts when they arrive.
239
+ const list = resolveListTokens(theme);
240
+ const rows = skeletonListRowCount(count);
241
+ return Array.from({ length: rows }, (_, row) =>
242
+ React.createElement(
243
+ View,
244
+ {
245
+ key: row,
246
+ style: {
247
+ ...listRowFrame(list, spacing),
248
+ paddingVertical: list.rowPadding,
249
+ minHeight: list.rowMinHeight,
250
+ ...(!list.separated && row < rows - 1 && list.dividerWidth > 0
251
+ ? { borderBottomWidth: list.dividerWidth, borderColor: list.dividerColor }
252
+ : null),
253
+ },
254
+ },
255
+ leading ? bone("circle", null, tokens, "leading") : null,
256
+ React.createElement(
257
+ View,
258
+ { style: { flex: 1, gap: SKELETON_TEXT_GAP } },
259
+ ...SKELETON_LIST_ROW_LINE_WIDTHS.map((width) => bone("text", { width }, tokens, width)),
260
+ ),
261
+ ),
262
+ );
263
+ }
264
+
265
+ // What a skeleton of several bones holds; null when it is a single bone.
266
+ function composite(kind, props, theme, tokens) {
267
+ if (props.children !== undefined && props.children !== null) {
268
+ return { content: props.children, layout: null };
269
+ }
270
+ if (kind === "listRow") {
271
+ return {
272
+ content: listRows(theme, tokens, props),
273
+ layout: { gap: resolveListTokens(theme).gap },
274
+ };
275
+ }
276
+ if (kind !== "text" || !(props.lines > 1)) return null;
277
+ const { lines, width, height } = props;
278
+ return {
279
+ content: skeletonLineWidths(lines, width).map((lineWidth, i) =>
280
+ bone("text", { width: lineWidth, height }, tokens, i),
281
+ ),
282
+ layout: { gap: SKELETON_TEXT_GAP },
283
+ };
284
+ }
285
+
286
+ function Skeleton(props) {
287
+ const { shape, accessibilityLabel, theme: explicitTheme, style, testID } = props;
288
+ const theme = useLoadingTheme(explicitTheme);
289
+ const loadingText = useHostSharedText("loading", LOADING_FALLBACK);
290
+ const nested = React.useContext(RegionContext);
291
+ // The outermost skeleton pulses as one; the bones inside it never animate.
292
+ const moving = !useReducedMotion() && !nested;
293
+ useSharedLoop(pulse, moving);
294
+
295
+ const tokens = resolveLoadingTokens(theme, {
296
+ skeletonColor: props.color,
297
+ skeletonRadius: props.radius,
298
+ });
299
+ const kind = normaliseSkeletonShape(shape);
300
+ const group = composite(kind, props, theme, tokens);
301
+ const own = group
302
+ ? [group.layout, style]
303
+ : boneStyle(skeletonBox(kind, props, tokens.skeletonRadius), tokens, style);
304
+
305
+ if (nested) {
306
+ return React.createElement(
307
+ View,
308
+ { ...(group ? null : HIDDEN), style: own, testID },
309
+ group && group.content,
310
+ );
311
+ }
312
+ return React.createElement(
313
+ RegionContext.Provider,
314
+ { value: true },
315
+ React.createElement(
316
+ Animated.View,
317
+ {
318
+ ...busyRegion(accessibilityLabel || loadingText),
319
+ ...LOADING_REGION,
320
+ style: [own, moving ? { opacity: pulse.animated() } : null],
321
+ testID,
322
+ },
323
+ group && group.content,
324
+ ),
325
+ );
326
+ }
327
+ Skeleton.displayName = "Skeleton";
328
+
329
+ return { Spinner, Skeleton };
330
+ }
@@ -0,0 +1,7 @@
1
+ // `<Spinner>` and `<Skeleton>` (web). Binds ./loading-view.js to
2
+ // react-native-web; see ./primitives.js for why not the bare specifier.
3
+
4
+ import * as ReactNative from "react-native-web";
5
+ import { makeLoading } from "./loading-view.js";
6
+
7
+ export const { Spinner, Skeleton } = makeLoading(ReactNative, { useNativeDriver: false });
@@ -0,0 +1,10 @@
1
+ // `<Spinner>` and `<Skeleton>` (native). Binds ./loading-view.js to React
2
+ // Native's own primitives; only the import differs from ./loading.js.
3
+
4
+ import { View, Text, Animated, Easing, AccessibilityInfo } from "react-native";
5
+ import { makeLoading } from "./loading-view.js";
6
+
7
+ export const { Spinner, Skeleton } = makeLoading(
8
+ { View, Text, Animated, Easing, AccessibilityInfo },
9
+ { useNativeDriver: true },
10
+ );