react-native-gleam 1.0.0-beta.6 → 1.0.0-beta.8

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/README.md CHANGED
@@ -52,6 +52,32 @@ function UserCard({ loading, user }) {
52
52
 
53
53
  When `loading={true}`, children are hidden and a shimmer animation plays. When `loading={false}`, the shimmer fades out and children fade in.
54
54
 
55
+ ### Multi-line skeleton (`GleamView.Line`)
56
+
57
+ Use `GleamView.Line` to create individual shimmer bars that inherit props from a parent `GleamView`. No conditional rendering — the wrapper pattern works for multi-line skeletons too.
58
+
59
+ ```tsx
60
+ <GleamView loading={loading} speed={800} baseColor="#E0E0E0">
61
+ <GleamView.Line style={{ height: 22, borderRadius: 6, width: '70%' }}>
62
+ <Text style={{ fontSize: 16 }}>{title}</Text>
63
+ </GleamView.Line>
64
+ <GleamView.Line
65
+ style={{ height: 16, borderRadius: 4, width: '50%' }}
66
+ delay={100}
67
+ >
68
+ <Text style={{ fontSize: 13 }}>{subtitle}</Text>
69
+ </GleamView.Line>
70
+ </GleamView>
71
+ ```
72
+
73
+ When `loading={true}`, each `GleamView.Line` renders its own shimmer bar, sized by `style`. The parent acts as a plain container (no block shimmer). When `loading={false}`, Lines become transparent and children render normally.
74
+
75
+ Lines inherit `loading`, `speed`, `direction`, `baseColor`, `highlightColor`, `intensity`, `transitionDuration`, and `transitionType` from the parent. `delay` and `onTransitionEnd` are per-line.
76
+
77
+ For best performance, place `GleamView.Line` as direct children of `GleamView` (or inside fragments). Lines nested inside intermediate wrappers (e.g., `<View>`) still work, but require an extra render cycle to detect.
78
+
79
+ Every `GleamView` provides context to its subtree. A `GleamView.Line` always binds to its nearest `GleamView` ancestor — nested `GleamView` components each control their own Lines independently.
80
+
55
81
  ### Staggered skeleton
56
82
 
57
83
  ```tsx
@@ -95,10 +121,21 @@ When `loading={true}`, children are hidden and a shimmer animation plays. When `
95
121
  | `intensity` | `number` | `1` | Highlight strength (0-1). Lower = more subtle shimmer |
96
122
  | `baseColor` | `string` | `#E0E0E0` | Background color of the shimmer |
97
123
  | `highlightColor` | `string` | `#F5F5F5` | Color of the moving highlight |
98
- | `onTransitionEnd` | `function` | — | Called when the fade transition completes. Receives `{ nativeEvent: { finished: boolean } }` |
124
+ | `onTransitionEnd` | `function` | — | Called when the transition completes or is interrupted. Receives `{ nativeEvent: { finished: boolean } }` — `true` if completed, `false` if interrupted (e.g., `loading` toggled back to `true`) |
99
125
 
100
126
  All standard `View` props are also supported (`style`, `testID`, etc.). Note: the shimmer overlay supports uniform `borderRadius` only — per-corner radii are not applied to the shimmer.
101
127
 
128
+ ### GleamView.Line Props
129
+
130
+ | Prop | Type | Default | Description |
131
+ |------|------|---------|-------------|
132
+ | `style` | `ViewStyle` | — | Style for the shimmer bar (height, width, borderRadius) |
133
+ | `delay` | `number` | `0` | Phase offset for this line (useful for stagger) |
134
+ | `onTransitionEnd` | `function` | — | Called when this line's transition completes |
135
+ | `testID` | `string` | — | Test identifier |
136
+
137
+ All standard accessibility props (`accessibilityLabel`, `accessibilityRole`, etc.) are accepted directly. Shimmer props (`loading`, `speed`, `direction`, etc.) cannot be passed to `GleamView.Line` — they are inherited automatically from the parent `GleamView`.
138
+
102
139
  ### GleamDirection
103
140
 
104
141
  ```tsx
@@ -121,7 +158,8 @@ GleamTransition.Collapse // 'collapse' — shimmer collapses vertically then ho
121
158
 
122
159
  ## Requirements
123
160
 
124
- - React Native **0.76+** (New Architecture / Fabric)
161
+ - React **19+**
162
+ - React Native **0.78+** (New Architecture / Fabric)
125
163
  - iOS 15+
126
164
  - Android SDK 24+
127
165
 
@@ -139,12 +177,20 @@ When `loading` switches to `false`:
139
177
 
140
178
  1. The shimmer transitions out over `transitionDuration` ms (style depends on `transitionType`)
141
179
  2. Children fade in simultaneously
142
- 3. `onTransitionEnd` fires when complete
180
+ 3. `onTransitionEnd` fires with `finished: true` (or `finished: false` if interrupted)
143
181
 
144
182
  All shimmer instances sharing the same `speed` are automatically synchronized via a shared clock.
145
183
 
146
184
  The shimmer respects uniform `borderRadius` and standard view styles.
147
185
 
186
+ ## Breaking changes (beta)
187
+
188
+ - When `GleamView.Line` children are present, the parent `GleamView` renders as a plain `View` container. `onTransitionEnd` on the parent is ignored in this mode — use `onTransitionEnd` on individual `GleamView.Line` components instead. A dev warning is emitted if this happens.
189
+
190
+ ## Limitations
191
+
192
+ - The shimmer overlay supports uniform `borderRadius` only — per-corner radii are not applied to the shimmer.
193
+
148
194
  ## License
149
195
 
150
196
  MIT
@@ -12,6 +12,7 @@ import android.graphics.Shader
12
12
  import android.os.SystemClock
13
13
  import android.view.Choreographer
14
14
  import android.view.animation.DecelerateInterpolator
15
+ import androidx.annotation.UiThread
15
16
  import com.facebook.react.bridge.Arguments
16
17
  import com.facebook.react.bridge.ReactContext
17
18
  import com.facebook.react.bridge.WritableMap
@@ -325,6 +326,9 @@ class GleamView(context: Context) : ReactViewGroup(context) {
325
326
 
326
327
  private fun applyLoadingState(wasLoading: Boolean) {
327
328
  if (loading) {
329
+ if (isTransitioning) {
330
+ emitTransitionEnd(false)
331
+ }
328
332
  // Set isTransitioning=false BEFORE cancel to prevent stale onAnimationEnd
329
333
  isTransitioning = false
330
334
  transitionGeneration++
@@ -409,9 +413,9 @@ class GleamView(context: Context) : ReactViewGroup(context) {
409
413
  */
410
414
  companion object SharedClock {
411
415
  private val views = mutableListOf<GleamView>()
412
- private val iterationSnapshot = mutableListOf<GleamView>()
413
416
  private var frameCallback: Choreographer.FrameCallback? = null
414
417
 
418
+ @UiThread
415
419
  fun register(view: GleamView) {
416
420
  if (!views.contains(view)) {
417
421
  views.add(view)
@@ -419,6 +423,7 @@ class GleamView(context: Context) : ReactViewGroup(context) {
419
423
  if (views.size == 1) start()
420
424
  }
421
425
 
426
+ @UiThread
422
427
  fun unregister(view: GleamView) {
423
428
  views.remove(view)
424
429
  if (views.isEmpty()) stop()
@@ -427,9 +432,10 @@ class GleamView(context: Context) : ReactViewGroup(context) {
427
432
  private fun start() {
428
433
  frameCallback = Choreographer.FrameCallback { frameTimeNanos ->
429
434
  val timeMs = frameTimeNanos / 1_000_000f
430
- iterationSnapshot.clear()
431
- iterationSnapshot.addAll(views)
432
- iterationSnapshot.forEach { it.onFrame(timeMs) }
435
+ // Reverse index iteration — safe if onFrame triggers unregister (shrinks tail)
436
+ for (i in views.indices.reversed()) {
437
+ views[i].onFrame(timeMs)
438
+ }
433
439
  frameCallback?.let { Choreographer.getInstance().postFrameCallback(it) }
434
440
  }
435
441
  Choreographer.getInstance().postFrameCallback(frameCallback!!)
package/ios/GleamView.mm CHANGED
@@ -223,6 +223,12 @@ static void _unregisterView(GleamView *view) {
223
223
  _didInitialSetup = NO;
224
224
  }
225
225
 
226
+ // Invariant: a registered view (_isRegistered=YES) is held by the static
227
+ // __strong _views array, which prevents ARC dealloc while registered.
228
+ // Off-main dealloc with _isRegistered=YES should therefore be unreachable.
229
+ // If the invariant is violated in release, we skip _unregisterView to avoid
230
+ // racing on the statics (_views, _viewCount, _displayLink) that are read/written
231
+ // by the display link on the main thread. The view leaks in _views but no crash.
226
232
  - (void)dealloc
227
233
  {
228
234
  if (_isRegistered) {
@@ -230,9 +236,7 @@ static void _unregisterView(GleamView *view) {
230
236
  if ([NSThread isMainThread]) {
231
237
  _unregisterView(self);
232
238
  } else {
233
- dispatch_async(dispatch_get_main_queue(), ^{
234
- _stopDisplayLinkIfNeeded();
235
- });
239
+ NSAssert(NO, @"GleamView deallocated off main thread — static __strong array should prevent this");
236
240
  }
237
241
  }
238
242
  }
@@ -494,6 +498,9 @@ static void _unregisterView(GleamView *view) {
494
498
  - (void)_applyLoadingState
495
499
  {
496
500
  if (_loading) {
501
+ if (_isTransitioning) {
502
+ [self _emitTransitionEnd:NO];
503
+ }
497
504
  _isTransitioning = NO;
498
505
  _contentAlpha = 0.0;
499
506
  _lastSetChildrenAlpha = -1.0;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ import { createContext } from 'react';
4
+ export const GleamContext = /*#__PURE__*/createContext(null);
5
+ //# sourceMappingURL=GleamContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["createContext","GleamContext"],"sourceRoot":"../../src","sources":["GleamContext.ts"],"mappings":";;AAAA,SAASA,aAAa,QAAQ,OAAO;AAerC,OAAO,MAAMC,YAAY,gBAAGD,aAAa,CAA2B,IAAI,CAAC","ignoreList":[]}
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+
3
+ import { useContext, useLayoutEffect } from 'react';
4
+ import { View } from 'react-native';
5
+ import NativeGleamView from './GleamViewNativeComponent';
6
+ import { GleamContext } from "./GleamContext.js";
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ export function GleamLine({
9
+ children,
10
+ style,
11
+ testID,
12
+ delay,
13
+ onTransitionEnd,
14
+ ...accessibilityProps
15
+ }) {
16
+ const ctx = useContext(GleamContext);
17
+ const register = ctx?.registerLine;
18
+ useLayoutEffect(() => {
19
+ if (!register) return;
20
+ return register();
21
+ }, [register]);
22
+ if (!ctx) {
23
+ if (__DEV__) {
24
+ console.warn('GleamView.Line must be used inside a GleamView');
25
+ }
26
+ return /*#__PURE__*/_jsx(View, {
27
+ style: style,
28
+ testID: testID,
29
+ ...accessibilityProps,
30
+ children: children
31
+ });
32
+ }
33
+ return /*#__PURE__*/_jsx(NativeGleamView, {
34
+ loading: ctx.loading,
35
+ speed: ctx.speed,
36
+ direction: ctx.direction,
37
+ delay: delay,
38
+ transitionDuration: ctx.transitionDuration,
39
+ transitionType: ctx.transitionType,
40
+ intensity: ctx.intensity,
41
+ baseColor: ctx.baseColor,
42
+ highlightColor: ctx.highlightColor,
43
+ onTransitionEnd: onTransitionEnd,
44
+ style: style,
45
+ testID: testID,
46
+ ...accessibilityProps,
47
+ children: children
48
+ });
49
+ }
50
+ //# sourceMappingURL=GleamLine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useContext","useLayoutEffect","View","NativeGleamView","GleamContext","jsx","_jsx","GleamLine","children","style","testID","delay","onTransitionEnd","accessibilityProps","ctx","register","registerLine","__DEV__","console","warn","loading","speed","direction","transitionDuration","transitionType","intensity","baseColor","highlightColor"],"sourceRoot":"../../src","sources":["GleamLine.tsx"],"mappings":";;AAAA,SAASA,UAAU,EAAEC,eAAe,QAAwB,OAAO;AACnE,SACEC,IAAI,QAIC,cAAc;AACrB,OAAOC,eAAe,MAA4B,4BAA4B;AAC9E,SAASC,YAAY,QAAQ,mBAAgB;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAU9C,OAAO,SAASC,SAASA,CAAC;EACxBC,QAAQ;EACRC,KAAK;EACLC,MAAM;EACNC,KAAK;EACLC,eAAe;EACf,GAAGC;AACW,CAAC,EAAE;EACjB,MAAMC,GAAG,GAAGd,UAAU,CAACI,YAAY,CAAC;EACpC,MAAMW,QAAQ,GAAGD,GAAG,EAAEE,YAAY;EAElCf,eAAe,CAAC,MAAM;IACpB,IAAI,CAACc,QAAQ,EAAE;IACf,OAAOA,QAAQ,CAAC,CAAC;EACnB,CAAC,EAAE,CAACA,QAAQ,CAAC,CAAC;EAEd,IAAI,CAACD,GAAG,EAAE;IACR,IAAIG,OAAO,EAAE;MACXC,OAAO,CAACC,IAAI,CAAC,gDAAgD,CAAC;IAChE;IACA,oBACEb,IAAA,CAACJ,IAAI;MAACO,KAAK,EAAEA,KAAM;MAACC,MAAM,EAAEA,MAAO;MAAA,GAAKG,kBAAkB;MAAAL,QAAA,EACvDA;IAAQ,CACL,CAAC;EAEX;EAEA,oBACEF,IAAA,CAACH,eAAe;IACdiB,OAAO,EAAEN,GAAG,CAACM,OAAQ;IACrBC,KAAK,EAAEP,GAAG,CAACO,KAAM;IACjBC,SAAS,EAAER,GAAG,CAACQ,SAAU;IACzBX,KAAK,EAAEA,KAAM;IACbY,kBAAkB,EAAET,GAAG,CAACS,kBAAmB;IAC3CC,cAAc,EAAEV,GAAG,CAACU,cAAe;IACnCC,SAAS,EAAEX,GAAG,CAACW,SAAU;IACzBC,SAAS,EAAEZ,GAAG,CAACY,SAAU;IACzBC,cAAc,EAAEb,GAAG,CAACa,cAAe;IACnCf,eAAe,EAAEA,eAAgB;IACjCH,KAAK,EAAEA,KAAM;IACbC,MAAM,EAAEA,MAAO;IAAA,GACXG,kBAAkB;IAAAL,QAAA,EAErBA;EAAQ,CACM,CAAC;AAEtB","ignoreList":[]}
@@ -1,6 +1,17 @@
1
1
  "use strict";
2
2
 
3
- export { default as GleamView } from './GleamViewNativeComponent';
3
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
4
+ import { View } from 'react-native';
5
+ import NativeGleamView from './GleamViewNativeComponent';
6
+ import { GleamContext } from "./GleamContext.js";
7
+ import { GleamLine } from "./GleamLine.js";
8
+
9
+ /**
10
+ * Props accepted by GleamView, including ref (React 19 ref-as-prop).
11
+ * Use this type instead of `ComponentProps<typeof GleamView>` for
12
+ * accurate ref typing.
13
+ */
14
+ import { jsx as _jsx } from "react/jsx-runtime";
4
15
  export let GleamDirection = /*#__PURE__*/function (GleamDirection) {
5
16
  GleamDirection["LeftToRight"] = "ltr";
6
17
  GleamDirection["RightToLeft"] = "rtl";
@@ -13,4 +24,115 @@ export let GleamTransition = /*#__PURE__*/function (GleamTransition) {
13
24
  GleamTransition["Collapse"] = "collapse";
14
25
  return GleamTransition;
15
26
  }({});
27
+
28
+ // Shimmer-specific keys that must be destructured from props before spreading
29
+ // viewProps onto <View> in Line mode.
30
+ // Direction 1 (compile-time): `satisfies` catches stale/typo keys.
31
+ // Direction 2 (DEV runtime): check inside GleamViewComponent catches
32
+ // new NativeProps keys that weren't added to this list or destructured.
33
+ const SHIMMER_KEYS = new Set(['loading', 'speed', 'direction', 'delay', 'transitionDuration', 'transitionType', 'intensity', 'baseColor', 'highlightColor', 'onTransitionEnd']);
34
+ function hasLineChildren(children) {
35
+ let found = false;
36
+ React.Children.forEach(children, child => {
37
+ if (found) return;
38
+ if (! /*#__PURE__*/React.isValidElement(child)) return;
39
+ if (child.type === GleamLine) {
40
+ found = true;
41
+ } else if (child.type === React.Fragment) {
42
+ found = hasLineChildren(child.props.children);
43
+ }
44
+ });
45
+ return found;
46
+ }
47
+
48
+ // React 19: ref is a regular prop, no forwardRef needed.
49
+ // Internal ref type is loosened to avoid monorepo type conflicts between
50
+ // root and example workspace @types/react copies. The exported GleamViewProps
51
+ // provides the correct consumer-facing type.
52
+ function GleamViewComponent({
53
+ ref,
54
+ ...props
55
+ }) {
56
+ const {
57
+ loading,
58
+ speed,
59
+ direction,
60
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
61
+ delay,
62
+ transitionDuration,
63
+ transitionType,
64
+ intensity,
65
+ baseColor,
66
+ highlightColor,
67
+ onTransitionEnd,
68
+ children,
69
+ ...viewProps
70
+ } = props;
71
+ if (__DEV__) {
72
+ for (const key of Object.keys(viewProps)) {
73
+ if (SHIMMER_KEYS.has(key)) {
74
+ console.error(`GleamView: shimmer prop "${key}" leaked into viewProps. ` + 'Add it to the destructuring in GleamViewComponent and to SHIMMER_KEYS.');
75
+ }
76
+ }
77
+ }
78
+ const lineCountRef = useRef(0);
79
+ const warnedTransitionRef = useRef(false);
80
+ const [hasLines, setHasLines] = useState(() => hasLineChildren(children));
81
+ const registerLine = useCallback(() => {
82
+ lineCountRef.current++;
83
+ setHasLines(true);
84
+ return () => {
85
+ lineCountRef.current--;
86
+ if (lineCountRef.current === 0) {
87
+ queueMicrotask(() => {
88
+ if (lineCountRef.current === 0) {
89
+ setHasLines(false);
90
+ warnedTransitionRef.current = false;
91
+ }
92
+ });
93
+ }
94
+ };
95
+ }, []);
96
+ const contextValue = useMemo(() => ({
97
+ loading,
98
+ speed,
99
+ direction,
100
+ transitionDuration,
101
+ transitionType,
102
+ intensity,
103
+ baseColor,
104
+ highlightColor,
105
+ registerLine
106
+ }), [loading, speed, direction, transitionDuration, transitionType, intensity, baseColor, highlightColor, registerLine]);
107
+
108
+ // Cast needed: View and NativeGleamView accept Ref<ReactNativeElement>
109
+ // but that type isn't publicly exported from react-native. Safe at runtime.
110
+ const nativeRef = ref;
111
+ if (hasLines) {
112
+ if (__DEV__ && onTransitionEnd && !warnedTransitionRef.current) {
113
+ warnedTransitionRef.current = true;
114
+ console.warn('GleamView: onTransitionEnd is ignored when GleamView.Line children are present. ' + 'Use onTransitionEnd on individual GleamView.Line components instead.');
115
+ }
116
+ return /*#__PURE__*/_jsx(GleamContext.Provider, {
117
+ value: contextValue,
118
+ children: /*#__PURE__*/_jsx(View, {
119
+ ref: nativeRef,
120
+ ...viewProps,
121
+ children: children
122
+ })
123
+ });
124
+ }
125
+ return /*#__PURE__*/_jsx(GleamContext.Provider, {
126
+ value: contextValue,
127
+ children: /*#__PURE__*/_jsx(NativeGleamView, {
128
+ ref: nativeRef,
129
+ ...props,
130
+ children: children
131
+ })
132
+ });
133
+ }
134
+ GleamViewComponent.displayName = 'GleamView';
135
+ export const GleamView = Object.assign(GleamViewComponent, {
136
+ Line: GleamLine
137
+ });
16
138
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["default","GleamView","GleamDirection","GleamTransition"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,OAAO,IAAIC,SAAS,QAAQ,4BAA4B;AAGjE,WAAYC,cAAc,0BAAdA,cAAc;EAAdA,cAAc;EAAdA,cAAc;EAAdA,cAAc;EAAA,OAAdA,cAAc;AAAA;AAM1B,WAAYC,eAAe,0BAAfA,eAAe;EAAfA,eAAe;EAAfA,eAAe;EAAfA,eAAe;EAAA,OAAfA,eAAe;AAAA","ignoreList":[]}
1
+ {"version":3,"names":["React","useCallback","useMemo","useRef","useState","View","NativeGleamView","GleamContext","GleamLine","jsx","_jsx","GleamDirection","GleamTransition","SHIMMER_KEYS","Set","hasLineChildren","children","found","Children","forEach","child","isValidElement","type","Fragment","props","GleamViewComponent","ref","loading","speed","direction","delay","transitionDuration","transitionType","intensity","baseColor","highlightColor","onTransitionEnd","viewProps","__DEV__","key","Object","keys","has","console","error","lineCountRef","warnedTransitionRef","hasLines","setHasLines","registerLine","current","queueMicrotask","contextValue","nativeRef","warn","Provider","value","displayName","GleamView","assign","Line"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,14 @@
1
+ import { type NativeProps } from './GleamViewNativeComponent';
2
+ export interface GleamContextValue {
3
+ loading: NativeProps['loading'];
4
+ speed: NativeProps['speed'];
5
+ direction: NativeProps['direction'];
6
+ transitionDuration: NativeProps['transitionDuration'];
7
+ transitionType: NativeProps['transitionType'];
8
+ intensity: NativeProps['intensity'];
9
+ baseColor: NativeProps['baseColor'];
10
+ highlightColor: NativeProps['highlightColor'];
11
+ registerLine: () => () => void;
12
+ }
13
+ export declare const GleamContext: import("react").Context<GleamContextValue | null>;
14
+ //# sourceMappingURL=GleamContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GleamContext.d.ts","sourceRoot":"","sources":["../../../src/GleamContext.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAE9D,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IAChC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC5B,SAAS,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IACpC,kBAAkB,EAAE,WAAW,CAAC,oBAAoB,CAAC,CAAC;IACtD,cAAc,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC9C,SAAS,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IACpC,SAAS,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC;IACpC,cAAc,EAAE,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAC9C,YAAY,EAAE,MAAM,MAAM,IAAI,CAAC;CAChC;AAED,eAAO,MAAM,YAAY,mDAAgD,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type AccessibilityProps, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { type NativeProps } from './GleamViewNativeComponent';
4
+ export interface GleamLineProps extends AccessibilityProps {
5
+ children?: ReactNode;
6
+ style?: StyleProp<ViewStyle>;
7
+ testID?: string;
8
+ delay?: NativeProps['delay'];
9
+ onTransitionEnd?: NativeProps['onTransitionEnd'];
10
+ }
11
+ export declare function GleamLine({ children, style, testID, delay, onTransitionEnd, ...accessibilityProps }: GleamLineProps): import("react/jsx-runtime").JSX.Element;
12
+ //# sourceMappingURL=GleamLine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GleamLine.d.ts","sourceRoot":"","sources":["../../../src/GleamLine.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AACtB,OAAwB,EAAE,KAAK,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAG/E,MAAM,WAAW,cAAe,SAAQ,kBAAkB;IACxD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAC7B,eAAe,CAAC,EAAE,WAAW,CAAC,iBAAiB,CAAC,CAAC;CAClD;AAED,wBAAgB,SAAS,CAAC,EACxB,QAAQ,EACR,KAAK,EACL,MAAM,EACN,KAAK,EACL,eAAe,EACf,GAAG,kBAAkB,EACtB,EAAE,cAAc,2CAuChB"}
@@ -1,5 +1,17 @@
1
- export { default as GleamView } from './GleamViewNativeComponent';
2
- export type { NativeProps as GleamViewProps } from './GleamViewNativeComponent';
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import { type NativeProps } from './GleamViewNativeComponent';
4
+ import { GleamLine } from './GleamLine';
5
+ export type { NativeProps } from './GleamViewNativeComponent';
6
+ export type { GleamLineProps } from './GleamLine';
7
+ /**
8
+ * Props accepted by GleamView, including ref (React 19 ref-as-prop).
9
+ * Use this type instead of `ComponentProps<typeof GleamView>` for
10
+ * accurate ref typing.
11
+ */
12
+ export type GleamViewProps = NativeProps & {
13
+ ref?: React.Ref<View>;
14
+ };
3
15
  export declare enum GleamDirection {
4
16
  LeftToRight = "ltr",
5
17
  RightToLeft = "rtl",
@@ -10,4 +22,13 @@ export declare enum GleamTransition {
10
22
  Shrink = "shrink",
11
23
  Collapse = "collapse"
12
24
  }
25
+ declare function GleamViewComponent({ ref, ...props }: NativeProps & {
26
+ ref?: React.Ref<unknown>;
27
+ }): import("react/jsx-runtime").JSX.Element;
28
+ declare namespace GleamViewComponent {
29
+ var displayName: string;
30
+ }
31
+ export declare const GleamView: typeof GleamViewComponent & {
32
+ Line: typeof GleamLine;
33
+ };
13
34
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAClE,YAAY,EAAE,WAAW,IAAI,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEhF,oBAAY,cAAc;IACxB,WAAW,QAAQ;IACnB,WAAW,QAAQ;IACnB,WAAW,QAAQ;CACpB;AAED,oBAAY,eAAe;IACzB,IAAI,SAAS;IACb,MAAM,WAAW;IACjB,QAAQ,aAAa;CACtB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAiD,MAAM,OAAO,CAAC;AACtE,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACpC,OAAwB,EAAE,KAAK,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAE/E,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,YAAY,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC9D,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG;IACzC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;CACvB,CAAC;AAEF,oBAAY,cAAc;IACxB,WAAW,QAAQ;IACnB,WAAW,QAAQ;IACnB,WAAW,QAAQ;CACpB;AAED,oBAAY,eAAe;IACzB,IAAI,SAAS;IACb,MAAM,WAAW;IACjB,QAAQ,aAAa;CACtB;AAwCD,iBAAS,kBAAkB,CAAC,EAC1B,GAAG,EACH,GAAG,KAAK,EACT,EAAE,WAAW,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;CAAE,2CAqG5C;kBAxGQ,kBAAkB;;;AA4G3B,eAAO,MAAM,SAAS;;CAEpB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-gleam",
3
- "version": "1.0.0-beta.6",
3
+ "version": "1.0.0-beta.8",
4
4
  "description": "Native-powered shimmer loading effect for React Native",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -92,8 +92,8 @@
92
92
  "typescript": "^5.9.2"
93
93
  },
94
94
  "peerDependencies": {
95
- "react": ">=18.0.0",
96
- "react-native": ">=0.76.0"
95
+ "react": ">=19.0.0",
96
+ "react-native": ">=0.78.0"
97
97
  },
98
98
  "workspaces": [
99
99
  "example"
@@ -0,0 +1,16 @@
1
+ import { createContext } from 'react';
2
+ import { type NativeProps } from './GleamViewNativeComponent';
3
+
4
+ export interface GleamContextValue {
5
+ loading: NativeProps['loading'];
6
+ speed: NativeProps['speed'];
7
+ direction: NativeProps['direction'];
8
+ transitionDuration: NativeProps['transitionDuration'];
9
+ transitionType: NativeProps['transitionType'];
10
+ intensity: NativeProps['intensity'];
11
+ baseColor: NativeProps['baseColor'];
12
+ highlightColor: NativeProps['highlightColor'];
13
+ registerLine: () => () => void;
14
+ }
15
+
16
+ export const GleamContext = createContext<GleamContextValue | null>(null);
@@ -0,0 +1,65 @@
1
+ import { useContext, useLayoutEffect, type ReactNode } from 'react';
2
+ import {
3
+ View,
4
+ type AccessibilityProps,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ } from 'react-native';
8
+ import NativeGleamView, { type NativeProps } from './GleamViewNativeComponent';
9
+ import { GleamContext } from './GleamContext';
10
+
11
+ export interface GleamLineProps extends AccessibilityProps {
12
+ children?: ReactNode;
13
+ style?: StyleProp<ViewStyle>;
14
+ testID?: string;
15
+ delay?: NativeProps['delay'];
16
+ onTransitionEnd?: NativeProps['onTransitionEnd'];
17
+ }
18
+
19
+ export function GleamLine({
20
+ children,
21
+ style,
22
+ testID,
23
+ delay,
24
+ onTransitionEnd,
25
+ ...accessibilityProps
26
+ }: GleamLineProps) {
27
+ const ctx = useContext(GleamContext);
28
+ const register = ctx?.registerLine;
29
+
30
+ useLayoutEffect(() => {
31
+ if (!register) return;
32
+ return register();
33
+ }, [register]);
34
+
35
+ if (!ctx) {
36
+ if (__DEV__) {
37
+ console.warn('GleamView.Line must be used inside a GleamView');
38
+ }
39
+ return (
40
+ <View style={style} testID={testID} {...accessibilityProps}>
41
+ {children}
42
+ </View>
43
+ );
44
+ }
45
+
46
+ return (
47
+ <NativeGleamView
48
+ loading={ctx.loading}
49
+ speed={ctx.speed}
50
+ direction={ctx.direction}
51
+ delay={delay}
52
+ transitionDuration={ctx.transitionDuration}
53
+ transitionType={ctx.transitionType}
54
+ intensity={ctx.intensity}
55
+ baseColor={ctx.baseColor}
56
+ highlightColor={ctx.highlightColor}
57
+ onTransitionEnd={onTransitionEnd}
58
+ style={style}
59
+ testID={testID}
60
+ {...accessibilityProps}
61
+ >
62
+ {children}
63
+ </NativeGleamView>
64
+ );
65
+ }
package/src/index.tsx CHANGED
@@ -1,5 +1,20 @@
1
- export { default as GleamView } from './GleamViewNativeComponent';
2
- export type { NativeProps as GleamViewProps } from './GleamViewNativeComponent';
1
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
2
+ import { View } from 'react-native';
3
+ import NativeGleamView, { type NativeProps } from './GleamViewNativeComponent';
4
+ import { GleamContext, type GleamContextValue } from './GleamContext';
5
+ import { GleamLine } from './GleamLine';
6
+
7
+ export type { NativeProps } from './GleamViewNativeComponent';
8
+ export type { GleamLineProps } from './GleamLine';
9
+
10
+ /**
11
+ * Props accepted by GleamView, including ref (React 19 ref-as-prop).
12
+ * Use this type instead of `ComponentProps<typeof GleamView>` for
13
+ * accurate ref typing.
14
+ */
15
+ export type GleamViewProps = NativeProps & {
16
+ ref?: React.Ref<View>;
17
+ };
3
18
 
4
19
  export enum GleamDirection {
5
20
  LeftToRight = 'ltr',
@@ -12,3 +27,153 @@ export enum GleamTransition {
12
27
  Shrink = 'shrink',
13
28
  Collapse = 'collapse',
14
29
  }
30
+
31
+ // Shimmer-specific keys that must be destructured from props before spreading
32
+ // viewProps onto <View> in Line mode.
33
+ // Direction 1 (compile-time): `satisfies` catches stale/typo keys.
34
+ // Direction 2 (DEV runtime): check inside GleamViewComponent catches
35
+ // new NativeProps keys that weren't added to this list or destructured.
36
+ const SHIMMER_KEYS: ReadonlySet<string> = new Set([
37
+ 'loading',
38
+ 'speed',
39
+ 'direction',
40
+ 'delay',
41
+ 'transitionDuration',
42
+ 'transitionType',
43
+ 'intensity',
44
+ 'baseColor',
45
+ 'highlightColor',
46
+ 'onTransitionEnd',
47
+ ] as const satisfies ReadonlyArray<keyof NativeProps>);
48
+
49
+ function hasLineChildren(children: React.ReactNode): boolean {
50
+ let found = false;
51
+ React.Children.forEach(children, (child) => {
52
+ if (found) return;
53
+ if (!React.isValidElement(child)) return;
54
+ if (child.type === GleamLine) {
55
+ found = true;
56
+ } else if (child.type === React.Fragment) {
57
+ found = hasLineChildren(
58
+ (child.props as { children?: React.ReactNode }).children
59
+ );
60
+ }
61
+ });
62
+ return found;
63
+ }
64
+
65
+ // React 19: ref is a regular prop, no forwardRef needed.
66
+ // Internal ref type is loosened to avoid monorepo type conflicts between
67
+ // root and example workspace @types/react copies. The exported GleamViewProps
68
+ // provides the correct consumer-facing type.
69
+ function GleamViewComponent({
70
+ ref,
71
+ ...props
72
+ }: NativeProps & { ref?: React.Ref<unknown> }) {
73
+ const {
74
+ loading,
75
+ speed,
76
+ direction,
77
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
78
+ delay,
79
+ transitionDuration,
80
+ transitionType,
81
+ intensity,
82
+ baseColor,
83
+ highlightColor,
84
+ onTransitionEnd,
85
+ children,
86
+ ...viewProps
87
+ } = props;
88
+
89
+ if (__DEV__) {
90
+ for (const key of Object.keys(viewProps)) {
91
+ if (SHIMMER_KEYS.has(key)) {
92
+ console.error(
93
+ `GleamView: shimmer prop "${key}" leaked into viewProps. ` +
94
+ 'Add it to the destructuring in GleamViewComponent and to SHIMMER_KEYS.'
95
+ );
96
+ }
97
+ }
98
+ }
99
+
100
+ const lineCountRef = useRef(0);
101
+ const warnedTransitionRef = useRef(false);
102
+ const [hasLines, setHasLines] = useState(() => hasLineChildren(children));
103
+
104
+ const registerLine = useCallback(() => {
105
+ lineCountRef.current++;
106
+ setHasLines(true);
107
+ return () => {
108
+ lineCountRef.current--;
109
+ if (lineCountRef.current === 0) {
110
+ queueMicrotask(() => {
111
+ if (lineCountRef.current === 0) {
112
+ setHasLines(false);
113
+ warnedTransitionRef.current = false;
114
+ }
115
+ });
116
+ }
117
+ };
118
+ }, []);
119
+
120
+ const contextValue = useMemo<GleamContextValue>(
121
+ () => ({
122
+ loading,
123
+ speed,
124
+ direction,
125
+ transitionDuration,
126
+ transitionType,
127
+ intensity,
128
+ baseColor,
129
+ highlightColor,
130
+ registerLine,
131
+ }),
132
+ [
133
+ loading,
134
+ speed,
135
+ direction,
136
+ transitionDuration,
137
+ transitionType,
138
+ intensity,
139
+ baseColor,
140
+ highlightColor,
141
+ registerLine,
142
+ ]
143
+ );
144
+
145
+ // Cast needed: View and NativeGleamView accept Ref<ReactNativeElement>
146
+ // but that type isn't publicly exported from react-native. Safe at runtime.
147
+ const nativeRef = ref as any;
148
+
149
+ if (hasLines) {
150
+ if (__DEV__ && onTransitionEnd && !warnedTransitionRef.current) {
151
+ warnedTransitionRef.current = true;
152
+ console.warn(
153
+ 'GleamView: onTransitionEnd is ignored when GleamView.Line children are present. ' +
154
+ 'Use onTransitionEnd on individual GleamView.Line components instead.'
155
+ );
156
+ }
157
+ return (
158
+ <GleamContext.Provider value={contextValue}>
159
+ <View ref={nativeRef} {...viewProps}>
160
+ {children}
161
+ </View>
162
+ </GleamContext.Provider>
163
+ );
164
+ }
165
+
166
+ return (
167
+ <GleamContext.Provider value={contextValue}>
168
+ <NativeGleamView ref={nativeRef} {...props}>
169
+ {children}
170
+ </NativeGleamView>
171
+ </GleamContext.Provider>
172
+ );
173
+ }
174
+
175
+ GleamViewComponent.displayName = 'GleamView';
176
+
177
+ export const GleamView = Object.assign(GleamViewComponent, {
178
+ Line: GleamLine,
179
+ });