@opndev/react-native-events 0.0.16 → 0.0.17

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/Changes CHANGED
@@ -1,5 +1,11 @@
1
1
  Revision history for @opndev/opndev-react-native-events
2
2
 
3
+ 0.0.17 2026-06-28 04:29:54Z
4
+
5
+ * Add gradient panel and proper implementation of contrast color, not just
6
+ "pick white or black"
7
+ * Fix HeroScreenFixed to be actually fixed to the content
8
+
3
9
  0.0.16 2026-06-27 23:25:11Z
4
10
 
5
11
  * Add widgets for dynamic and static content:
@@ -15,11 +15,8 @@ const defaultStyle = StyleSheet.create({
15
15
  container: {
16
16
  flex: 1,
17
17
  },
18
- headerWrap: {
18
+ header: {
19
19
  width: '100%',
20
- position: 'absolute',
21
- top: 0,
22
- left: 0,
23
20
  },
24
21
  imageInner: {
25
22
  position: 'absolute',
@@ -31,12 +28,24 @@ const defaultStyle = StyleSheet.create({
31
28
  /**
32
29
  * HeroScreenFixed
33
30
  *
34
- * Header image stays pinned in place; only the content below it
35
- * scrolls. The image never overlaps the status bar a solid
36
- * headerBackgroundColor strip (sized to the safe-area top inset,
37
- * via useHeroHeaderHeight) sits above it. Body content is wrapped
38
- * the same way ParallaxScrollView wraps its children — padding: 32,
39
- * gap: 16 — so spacing matches the parallax variant exactly.
31
+ * Header image is a normal, in-flow element at the top of the
32
+ * scroll — it scrolls away together with the content below it, at
33
+ * the same rate, with no pinning and no animation. Same structural
34
+ * shape as HeroScreenParallax, just without the scroll-driven
35
+ * scale/translateY transform "parallax minus the animation."
36
+ *
37
+ * "Fixed" means fixed to the content below it, not fixed to the
38
+ * screen — the previous version of this file pinned the header via
39
+ * position:absolute, which was actually the same idea as
40
+ * HeroScreenOverlay, not what "Fixed" was meant to mean. Rebuilt
41
+ * from scratch; this file had no usages anywhere yet.
42
+ *
43
+ * ContentComponent's role matches HeroScreenParallax's: it wraps
44
+ * the body content for layout purposes (defaults to View, NOT a
45
+ * scroll container) — the actual scrolling is owned by the outer
46
+ * ScrollView, hardcoded, not swappable. If you were expecting
47
+ * ContentComponent to default to ScrollView (the old behavior),
48
+ * that's the one real API change here.
40
49
  *
41
50
  * @param {object} props
42
51
  * @param {React.ReactNode} props.children Normal body content, and/or <CarouselScreen> elements for slides.
@@ -48,7 +57,7 @@ const defaultStyle = StyleSheet.create({
48
57
  * @param {string} [props.backgroundColor]
49
58
  * @param {string} [props.headerBackgroundColor]
50
59
  * @param {number} [props.headerHeight] Visible image height (excludes the safe-area inset)
51
- * @param {React.ComponentType} [props.ContentComponent]
60
+ * @param {React.ComponentType} [props.ContentComponent] Defaults to View — wraps body content, does NOT own scrolling.
52
61
  * @param {object} [props.containerStyle]
53
62
  * @param {object} [props.headerStyle]
54
63
  * @param {object} [props.contentStyle]
@@ -65,7 +74,7 @@ export default function HeroScreenFixed({
65
74
  backgroundColor,
66
75
  headerBackgroundColor,
67
76
  headerHeight = 220,
68
- ContentComponent = ScrollView,
77
+ ContentComponent = View,
69
78
  containerStyle,
70
79
  headerStyle,
71
80
  contentStyle,
@@ -74,7 +83,7 @@ export default function HeroScreenFixed({
74
83
  const { slideElements, bodyChildren } = splitCarouselChildren(children);
75
84
 
76
85
  return (
77
- <View
86
+ <ScrollView
78
87
  style={[
79
88
  defaultStyle.container,
80
89
  backgroundColor ? { backgroundColor } : null,
@@ -83,7 +92,7 @@ export default function HeroScreenFixed({
83
92
  >
84
93
  <View
85
94
  style={[
86
- defaultStyle.headerWrap,
95
+ defaultStyle.header,
87
96
  { height: totalHeaderHeight },
88
97
  headerBackgroundColor ? { backgroundColor: headerBackgroundColor } : null,
89
98
  headerStyle,
@@ -105,15 +114,10 @@ export default function HeroScreenFixed({
105
114
  </View>
106
115
  </View>
107
116
 
108
- <ContentComponent
109
- style={{ flex: 1 }}
110
- contentContainerStyle={{ paddingTop: totalHeaderHeight }}
111
- >
112
- <View style={[heroContentStyle, contentStyle]}>
113
- <HeaderOverlay headerOverlay={headerOverlay} />
114
- {bodyChildren}
115
- </View>
117
+ <ContentComponent style={[heroContentStyle, contentStyle]}>
118
+ <HeaderOverlay headerOverlay={headerOverlay} />
119
+ {bodyChildren}
116
120
  </ContentComponent>
117
- </View>
121
+ </ScrollView>
118
122
  );
119
123
  }
@@ -0,0 +1,150 @@
1
+ import React from 'react';
2
+ import { View, Pressable, StyleSheet } from 'react-native';
3
+ import { LinearGradient } from 'expo-linear-gradient';
4
+ import { contrastColor } from '../utils/colors';
5
+ import { PanelContrastContext } from '../hooks/use-panel-contrast';
6
+
7
+ // Six named directions — end is always the geometric opposite of
8
+ // start, so only one end of each axis needs naming. Want the
9
+ // reverse (e.g. bottom-left to top-right)? Swap the order of
10
+ // `colors` instead of asking for a 7th/8th direction keyword.
11
+ const DIRECTIONS = {
12
+ top: { start: { x: 0, y: 0 }, end: { x: 0, y: 1 } },
13
+ bottom: { start: { x: 0, y: 1 }, end: { x: 0, y: 0 } },
14
+ left: { start: { x: 0, y: 0 }, end: { x: 1, y: 0 } },
15
+ right: { start: { x: 1, y: 0 }, end: { x: 0, y: 0 } },
16
+ 'top-left': { start: { x: 0, y: 0 }, end: { x: 1, y: 1 } },
17
+ 'top-right': { start: { x: 1, y: 0 }, end: { x: 0, y: 1 } },
18
+ };
19
+
20
+ // Local for now, not added to lib/utils/colors.js since I haven't
21
+ // seen that file's full current content — move it there later if
22
+ // you want it shared. Only handles 6-digit hex (#RRGGBB); named
23
+ // colors ('white') or rgba() strings aren't parsed.
24
+ function hexToRgb(hex) {
25
+ const clean = hex.replace('#', '');
26
+ const value = parseInt(clean, 16);
27
+ return { r: (value >> 16) & 255, g: (value >> 8) & 255, b: value & 255 };
28
+ }
29
+
30
+ function rgbToHex({ r, g, b }) {
31
+ return '#' + [r, g, b].map((v) => Math.round(v).toString(16).padStart(2, '0')).join('');
32
+ }
33
+
34
+ function averageColor(colors) {
35
+ const rgbs = colors.map(hexToRgb);
36
+ const sum = rgbs.reduce((acc, c) => ({ r: acc.r + c.r, g: acc.g + c.g, b: acc.b + c.b }), { r: 0, g: 0, b: 0 });
37
+ const n = rgbs.length;
38
+ return rgbToHex({ r: sum.r / n, g: sum.g / n, b: sum.b / n });
39
+ }
40
+
41
+ const defaultStyle = StyleSheet.create({
42
+ shadowWrap: {
43
+ borderRadius: 16,
44
+ },
45
+ shadowOn: {
46
+ elevation: 4,
47
+ shadowColor: '#000',
48
+ shadowOffset: { width: 0, height: 2 },
49
+ shadowOpacity: 0.15,
50
+ shadowRadius: 6,
51
+ },
52
+ surface: {
53
+ borderRadius: 16,
54
+ overflow: 'hidden',
55
+ },
56
+ content: {
57
+ padding: 16,
58
+ },
59
+ });
60
+
61
+ /**
62
+ * GradientPanel
63
+ *
64
+ * Same shape and behavior as Panel — sizes to its content, no
65
+ * forced layout on children, same shadow-split structure (an outer
66
+ * shadow-casting view, an inner clipping view, for the same reason
67
+ * Panel needs it: a shadow renders outside a view's own bounds, so
68
+ * it can't live on the same view that clips with overflow:hidden).
69
+ *
70
+ * The only difference: a gradient background instead of a flat
71
+ * `backgroundColor`. Stays theme-agnostic like Panel — pass raw
72
+ * resolved colors (e.g. ['#FFFFFF', theme.primary]), this component
73
+ * doesn't reach into your theme itself.
74
+ *
75
+ * Automatically derives a contrast text color from the AVERAGE of
76
+ * its `colors` stops (via contrastColor — same hue/saturation,
77
+ * contrasting lightness, not flat black/white), made available to
78
+ * anything rendered inside via PanelContrastContext — same
79
+ * mechanism Panel uses. This is a heuristic, not exact — a fade
80
+ * from white to a dark color averages to something mid-tone, which
81
+ * may not actually read well at either literal end of the gradient.
82
+ * Override with `contrastTextColor` if the heuristic guesses wrong.
83
+ *
84
+ * @param {object} props
85
+ * @param {React.ReactNode} props.children
86
+ * @param {string[]} [props.colors] Gradient stops, e.g. ['#FFFFFF', theme.primary]. Defaults to ['#FFFFFF', '#FFFFFF'] (solid white) if omitted.
87
+ * @param {'top'|'bottom'|'left'|'right'|'top-left'|'top-right'} [props.direction] Defaults to 'top'. Takes precedence over start/end if both are given.
88
+ * @param {{x: number, y: number}} [props.start] Raw escape hatch, ignored if `direction` is set.
89
+ * @param {{x: number, y: number}} [props.end] Raw escape hatch, ignored if `direction` is set.
90
+ * @param {number[]} [props.locations] Where each color stop lands (0-1). Optional — omit for an even spread.
91
+ * @param {string} [props.contrastTextColor] Overrides the automatically-derived (average-based) contrast color.
92
+ * @param {boolean} [props.shadow] Floating-card drop shadow. Defaults to false.
93
+ * @param {Function} [props.onPress] If provided, the whole panel becomes pressable.
94
+ * @param {object} [props.style] Style for the outer (shadow-casting) wrapper.
95
+ * @param {object} [props.contentStyle] Style for the inner content wrapper (default padding: 16).
96
+ *
97
+ * @returns {JSX.Element}
98
+ */
99
+ export default function GradientPanel({
100
+ children,
101
+ colors = ['#FFFFFF', '#FFFFFF'],
102
+ direction = 'top',
103
+ start,
104
+ end,
105
+ locations,
106
+ contrastTextColor,
107
+ shadow = false,
108
+ onPress,
109
+ style,
110
+ contentStyle,
111
+ }) {
112
+ const resolvedDirection = DIRECTIONS[direction] ?? {
113
+ start: start ?? DIRECTIONS.top.start,
114
+ end: end ?? DIRECTIONS.top.end,
115
+ };
116
+ const resolvedContrast = contrastTextColor ?? contrastColor(averageColor(colors));
117
+
118
+ const content = (
119
+ <LinearGradient
120
+ colors={colors}
121
+ start={resolvedDirection.start}
122
+ end={resolvedDirection.end}
123
+ locations={locations}
124
+ style={defaultStyle.surface}
125
+ >
126
+ <PanelContrastContext.Provider value={resolvedContrast}>
127
+ <View style={[defaultStyle.content, contentStyle]}>
128
+ {children}
129
+ </View>
130
+ </PanelContrastContext.Provider>
131
+ </LinearGradient>
132
+ );
133
+
134
+ const wrapperStyle = [
135
+ defaultStyle.shadowWrap,
136
+ shadow ? defaultStyle.shadowOn : null,
137
+ shadow ? { backgroundColor: colors[0] } : null,
138
+ style,
139
+ ];
140
+
141
+ if (onPress) {
142
+ return (
143
+ <Pressable onPress={onPress} style={wrapperStyle}>
144
+ {content}
145
+ </Pressable>
146
+ );
147
+ }
148
+
149
+ return <View style={wrapperStyle}>{content}</View>;
150
+ }
@@ -1,9 +1,10 @@
1
1
  // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
2
2
  //
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
4
-
5
4
  import React from 'react';
6
5
  import { View, Pressable, StyleSheet } from 'react-native';
6
+ import { contrastColor } from '../utils/colors';
7
+ import { PanelContrastContext } from '../hooks/use-panel-contrast';
7
8
 
8
9
  const defaultStyle = StyleSheet.create({
9
10
  shadowWrap: {
@@ -43,9 +44,19 @@ const defaultStyle = StyleSheet.create({
43
44
  * split means `shadow` can be toggled on/off with no other changes
44
45
  * needed, and panels that don't use it are unaffected.
45
46
  *
47
+ * Automatically derives a contrast text color from `backgroundColor`
48
+ * (via contrastColor) and makes it available to anything rendered
49
+ * inside, via PanelContrastContext — so e.g. DataListWidget picks up
50
+ * a correct default text color for whatever Panel it's sitting in,
51
+ * with zero per-usage color wiring. Pass `contrastTextColor`
52
+ * yourself to override the automatic guess; anything inside can
53
+ * still override further for individual pieces of text (e.g.
54
+ * DataListWidget's own itemTextStyle/titleStyle still win over this).
55
+ *
46
56
  * @param {object} props
47
57
  * @param {React.ReactNode} props.children
48
58
  * @param {string} [props.backgroundColor]
59
+ * @param {string} [props.contrastTextColor] Overrides the automatically-derived contrast color.
49
60
  * @param {boolean} [props.shadow] Floating-card drop shadow. Defaults to false.
50
61
  * @param {Function} [props.onPress] If provided, the whole panel becomes pressable.
51
62
  * @param {object} [props.style] Style for the outer (shadow-casting) wrapper.
@@ -56,20 +67,26 @@ const defaultStyle = StyleSheet.create({
56
67
  export default function Panel({
57
68
  children,
58
69
  backgroundColor,
70
+ contrastTextColor,
59
71
  shadow = false,
60
72
  onPress,
61
73
  style,
62
74
  contentStyle,
63
75
  }) {
76
+ const resolvedContrast = contrastTextColor
77
+ ?? (backgroundColor ? contrastColor(backgroundColor) : null);
78
+
64
79
  const surfaceStyle = [
65
80
  defaultStyle.surface,
66
81
  backgroundColor ? { backgroundColor } : null,
67
82
  ];
68
83
 
69
84
  const content = (
70
- <View style={[defaultStyle.content, contentStyle]}>
71
- {children}
72
- </View>
85
+ <PanelContrastContext.Provider value={resolvedContrast}>
86
+ <View style={[defaultStyle.content, contentStyle]}>
87
+ {children}
88
+ </View>
89
+ </PanelContrastContext.Provider>
73
90
  );
74
91
 
75
92
  const inner = onPress ? (
package/lib/components.js CHANGED
@@ -12,6 +12,7 @@ export { default as GradientTile } from './components/gradient-tile.jsx';
12
12
  export { default as QRCodeForm } from './components/qr-code-form.jsx';
13
13
  export { default as ScaledLogo } from './components/scaled-logo.jsx';
14
14
  export { default as Panel } from './components/panel.jsx';
15
+ export { default as GradientPanel } from './components/panel-gradient.jsx';
15
16
 
16
17
  export { openUrl, openApp, openExternal } from './utils/launch.js';
17
18
 
@@ -0,0 +1,20 @@
1
+ import { createContext, useContext } from 'react';
2
+
3
+ /**
4
+ * PanelContrastContext
5
+ *
6
+ * Provided by Panel — the contrast color automatically derived from
7
+ * its backgroundColor (or a manual override, if Panel was given
8
+ * one). Defaults to null, so anything reading this outside a Panel
9
+ * just gets "no opinion," not a wrong color.
10
+ */
11
+ export const PanelContrastContext = createContext(null);
12
+
13
+ /**
14
+ * usePanelContrast
15
+ *
16
+ * @returns {string|null} The enclosing Panel's contrast color, or null if there isn't one.
17
+ */
18
+ export function usePanelContrast() {
19
+ return useContext(PanelContrastContext);
20
+ }
package/lib/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
4
4
 
5
- const VERSION = "0.0.16";
5
+ const VERSION = "0.0.17";
6
6
 
7
7
  // TODO: @opndev/util?
8
8
  export { formatPrice } from './utils/format-price.js';
@@ -18,19 +18,15 @@
18
18
  export function mixHexColors(a, b, amount = 0.5) {
19
19
  const ah = a.replace('#', '');
20
20
  const bh = b.replace('#', '');
21
-
22
21
  const ar = parseInt(ah.slice(0, 2), 16);
23
22
  const ag = parseInt(ah.slice(2, 4), 16);
24
23
  const ab = parseInt(ah.slice(4, 6), 16);
25
-
26
24
  const br = parseInt(bh.slice(0, 2), 16);
27
25
  const bg = parseInt(bh.slice(2, 4), 16);
28
26
  const bb = parseInt(bh.slice(4, 6), 16);
29
-
30
27
  const r = Math.round(ar + (br - ar) * amount);
31
28
  const g = Math.round(ag + (bg - ag) * amount);
32
29
  const b2 = Math.round(ab + (bb - ab) * amount);
33
-
34
30
  return (
35
31
  '#' +
36
32
  r.toString(16).padStart(2, '0') +
@@ -40,27 +36,116 @@ export function mixHexColors(a, b, amount = 0.5) {
40
36
  }
41
37
 
42
38
  /**
43
- * Return black or white depending on which has better contrast.
39
+ * Convert a hex color to HSL.
40
+ *
41
+ * @param {string} hex
42
+ * Color as "#rrggbb".
43
+ *
44
+ * @returns {{h: number, s: number, l: number}}
45
+ * Each in the 0-1 range (h is fraction of 360°, not degrees).
46
+ */
47
+ function hexToHsl(hex) {
48
+ const value = hex.replace('#', '');
49
+ const r = parseInt(value.slice(0, 2), 16) / 255;
50
+ const g = parseInt(value.slice(2, 4), 16) / 255;
51
+ const b = parseInt(value.slice(4, 6), 16) / 255;
52
+
53
+ const max = Math.max(r, g, b);
54
+ const min = Math.min(r, g, b);
55
+ const l = (max + min) / 2;
56
+
57
+ let h = 0;
58
+ let s = 0;
59
+
60
+ if (max !== min) {
61
+ const d = max - min;
62
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
63
+
64
+ switch (max) {
65
+ case r:
66
+ h = (g - b) / d + (g < b ? 6 : 0);
67
+ break;
68
+ case g:
69
+ h = (b - r) / d + 2;
70
+ break;
71
+ default:
72
+ h = (r - g) / d + 4;
73
+ break;
74
+ }
75
+ h /= 6;
76
+ }
77
+
78
+ return { h, s, l };
79
+ }
80
+
81
+ /**
82
+ * Convert HSL back to a hex color.
83
+ *
84
+ * @param {{h: number, s: number, l: number}} hsl
85
+ * Each in the 0-1 range (h is fraction of 360°, not degrees).
86
+ *
87
+ * @returns {string}
88
+ * Color as "#rrggbb".
89
+ */
90
+ function hslToHex({ h, s, l }) {
91
+ function hueToRgb(p, q, t) {
92
+ if (t < 0) t += 1;
93
+ if (t > 1) t -= 1;
94
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
95
+ if (t < 1 / 2) return q;
96
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
97
+ return p;
98
+ }
99
+
100
+ let r;
101
+ let g;
102
+ let b;
103
+
104
+ if (s === 0) {
105
+ r = g = b = l;
106
+ }
107
+ else {
108
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
109
+ const p = 2 * l - q;
110
+ r = hueToRgb(p, q, h + 1 / 3);
111
+ g = hueToRgb(p, q, h);
112
+ b = hueToRgb(p, q, h - 1 / 3);
113
+ }
114
+
115
+ const toHex = (c) => Math.round(c * 255).toString(16).padStart(2, '0');
116
+ return '#' + toHex(r) + toHex(g) + toHex(b);
117
+ }
118
+
119
+ /**
120
+ * Return a contrasting color in the SAME hue/saturation family as
121
+ * the input, just pushed to a very different lightness — the same
122
+ * technique CSS color-ramp design systems use (Tailwind/Radix-style
123
+ * 50-950 scales): same color family, different shade, rather than
124
+ * jumping to flat black/white. A light, desaturated input still
125
+ * gets a colored (just very dark) result; a fully neutral input
126
+ * (pure white/black/gray, s=0) naturally falls back to plain
127
+ * gray at the target lightness, since there's no hue to preserve.
44
128
  *
45
129
  * @param {string} hex
46
130
  * Background color as "#rrggbb".
47
131
  *
48
132
  * @returns {string}
49
- * "#000000" or "#ffffff".
133
+ * A same-hue, contrasting-lightness hex color.
50
134
  */
51
135
  export function contrastColor(hex) {
52
136
  const value = hex.replace('#', '');
53
-
54
137
  const r = parseInt(value.slice(0, 2), 16) / 255;
55
138
  const g = parseInt(value.slice(2, 4), 16) / 255;
56
139
  const b = parseInt(value.slice(4, 6), 16) / 255;
57
-
58
140
  const luminance =
59
141
  0.2126 * toLinear(r) +
60
142
  0.7152 * toLinear(g) +
61
143
  0.0722 * toLinear(b);
62
144
 
63
- return luminance > 0.179 ? '#000000' : '#ffffff';
145
+ const { h, s } = hexToHsl(hex);
146
+ const targetLightness = luminance > 0.179 ? 0.15 : 0.92;
147
+
148
+ return hslToHex({ h, s, l: targetLightness });
64
149
  }
65
150
 
66
151
  /**
@@ -76,7 +161,5 @@ export function toLinear(channel) {
76
161
  if (channel <= 0.03928) {
77
162
  return channel / 12.92;
78
163
  }
79
-
80
164
  return ((channel + 0.055) / 1.055) ** 2.4;
81
165
  }
82
-
@@ -10,6 +10,7 @@ import {
10
10
  View,
11
11
  } from 'react-native';
12
12
  import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
13
+ import { usePanelContrast } from '../hooks/use-panel-contrast';
13
14
 
14
15
  const defaultStyle = StyleSheet.create({
15
16
  container: {
@@ -111,6 +112,19 @@ export default function DataListWidget({
111
112
  errorTextStyle,
112
113
  }) {
113
114
  const showTitleRow = title || showRefreshButton;
115
+ const panelContrast = usePanelContrast();
116
+ const contrastTextStyle = panelContrast ? { color: panelContrast } : null;
117
+
118
+ if (__DEV__ && !loading && !error && items.length) {
119
+ items.forEach((item, index) => {
120
+ if (item == null || item.id == null || item.title == null) {
121
+ throw new Error(
122
+ `DataListWidget: item at index ${index} is missing "id" or "title". ` +
123
+ `Check that mapItem returns { id, title, description? } — got: ${JSON.stringify(item)}`
124
+ );
125
+ }
126
+ });
127
+ }
114
128
 
115
129
  return (
116
130
  <View
@@ -127,7 +141,7 @@ export default function DataListWidget({
127
141
  ]}
128
142
  >
129
143
  {title ? (
130
- <TextComponent style={[defaultStyle.title, titleStyle]}>
144
+ <TextComponent style={[defaultStyle.title, contrastTextStyle, titleStyle]}>
131
145
  {title}
132
146
  </TextComponent>
133
147
  ) : null}
@@ -169,14 +183,14 @@ export default function DataListWidget({
169
183
  onPress={() => onPressItem?.(item)}
170
184
  >
171
185
  {showBullet ? (
172
- <TextComponent style={[defaultStyle.bullet, itemTextStyle]}>{'\u2022'}</TextComponent>
186
+ <TextComponent style={[defaultStyle.bullet, contrastTextStyle, itemTextStyle]}>{'\u2022'}</TextComponent>
173
187
  ) : null}
174
188
  <View style={defaultStyle.itemBody}>
175
- <TextComponent style={itemTextStyle}>
189
+ <TextComponent style={[contrastTextStyle, itemTextStyle]}>
176
190
  {item.title}
177
191
  </TextComponent>
178
192
  {item.description ? (
179
- <TextComponent style={descriptionStyle}>
193
+ <TextComponent style={[contrastTextStyle, descriptionStyle]}>
180
194
  {item.description}
181
195
  </TextComponent>
182
196
  ) : null}
@@ -186,4 +200,3 @@ export default function DataListWidget({
186
200
  </View>
187
201
  );
188
202
  }
189
-
package/package.json CHANGED
@@ -35,5 +35,5 @@
35
35
  },
36
36
  "sideEffects": false,
37
37
  "type": "module",
38
- "version": "0.0.16"
38
+ "version": "0.0.17"
39
39
  }