@opndev/react-native-events 0.0.12 → 0.0.14
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 +11 -0
- package/lib/components/hero-screen-header.jsx +43 -2
- package/lib/components/hero-screen.jsx +9 -1
- package/lib/components/panel.jsx +65 -0
- package/lib/components.js +1 -0
- package/lib/index.js +1 -1
- package/lib/screens/news-item-screen.jsx +40 -25
- package/lib/widgets/news-widget.jsx +190 -0
- package/lib/widgets.js +5 -0
- package/package.json +3 -3
package/Changes
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
Revision history for @opndev/opndev-react-native-events
|
|
2
2
|
|
|
3
|
+
0.0.14 2026-06-26 04:53:23Z
|
|
4
|
+
|
|
5
|
+
* Add Panel instead of a Tile, Tiles are more buttony thing. Panels are
|
|
6
|
+
panels. ha.
|
|
7
|
+
|
|
8
|
+
0.0.13 2026-06-26 03:59:55Z
|
|
9
|
+
|
|
10
|
+
* Update news item segment to be included as a widget.
|
|
11
|
+
This infra is highly sus for generic dynamic pages infra. I need some more
|
|
12
|
+
hours in a day to get perhaps get that to work nicely. but getting there.
|
|
13
|
+
|
|
3
14
|
0.0.12 2026-06-25 23:24:45Z
|
|
4
15
|
|
|
5
16
|
* Add top bar to package
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
4
|
|
|
5
|
-
import { View, Image, StyleSheet } from 'react-native';
|
|
6
|
-
import
|
|
5
|
+
import { View, Image, Pressable, StyleSheet } from 'react-native';
|
|
6
|
+
import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
|
|
7
|
+
import { useAppScreenOffset } from '../hooks/use-app-screen-offset';
|
|
7
8
|
|
|
8
9
|
const styles = StyleSheet.create({
|
|
9
10
|
headerImage: { width: '100%', height: '100%' },
|
|
@@ -11,8 +12,48 @@ const styles = StyleSheet.create({
|
|
|
11
12
|
// Matches ParallaxScrollView's own `content` style — single source of
|
|
12
13
|
// truth so every Hero variant lines up on body padding/gap.
|
|
13
14
|
heroContent: { padding: 32, gap: 16 },
|
|
15
|
+
backButton: {
|
|
16
|
+
width: 40,
|
|
17
|
+
height: 40,
|
|
18
|
+
borderRadius: 20,
|
|
19
|
+
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
justifyContent: 'center',
|
|
22
|
+
elevation: 4,
|
|
23
|
+
shadowColor: '#000',
|
|
24
|
+
shadowOffset: { width: 0, height: 2 },
|
|
25
|
+
shadowOpacity: 0.3,
|
|
26
|
+
shadowRadius: 3,
|
|
27
|
+
},
|
|
14
28
|
});
|
|
15
29
|
|
|
30
|
+
/**
|
|
31
|
+
* BackButton
|
|
32
|
+
*
|
|
33
|
+
* Floating back-chevron button meant to sit over a Hero variant's
|
|
34
|
+
* header image. Router-agnostic — same pattern as onSlidePress
|
|
35
|
+
* elsewhere — the package renders the button, the app decides what
|
|
36
|
+
* tapping it does (typically router.back()).
|
|
37
|
+
*
|
|
38
|
+
* Renders nothing if `onBack` isn't provided, so adding this to a
|
|
39
|
+
* Hero variant is a no-op for screens that don't pass it.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} props
|
|
42
|
+
* @param {Function} [props.onBack]
|
|
43
|
+
* @param {string} [props.iconColor] Defaults to white.
|
|
44
|
+
* @param {number} [props.iconSize] Defaults to 22.
|
|
45
|
+
* @param {object} [props.style] Positioning is the caller's responsibility (e.g. absolute top/left) — this component doesn't position itself.
|
|
46
|
+
*/
|
|
47
|
+
export function BackButton({ onBack, iconColor = '#fff', iconSize = 22, style }) {
|
|
48
|
+
if (!onBack) return null;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Pressable onPress={onBack} style={[styles.backButton, style]}>
|
|
52
|
+
<MaterialCommunityIcons name="arrow-left" size={iconSize} color={iconColor} />
|
|
53
|
+
</Pressable>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
16
57
|
export function renderHeaderImage(headerImage) {
|
|
17
58
|
if (!headerImage) return null;
|
|
18
59
|
const fit = headerImage.fit || 'cover';
|
|
@@ -6,6 +6,7 @@ import { View, ScrollView, StyleSheet } from 'react-native';
|
|
|
6
6
|
import {
|
|
7
7
|
renderHeaderImage,
|
|
8
8
|
HeaderOverlay,
|
|
9
|
+
BackButton,
|
|
9
10
|
useHeroHeaderHeight,
|
|
10
11
|
heroContentStyle,
|
|
11
12
|
} from './hero-screen-header';
|
|
@@ -37,7 +38,7 @@ const defaultStyle = StyleSheet.create({
|
|
|
37
38
|
* headerBackgroundColor strip (sized to the safe-area top inset,
|
|
38
39
|
* via useHeroHeaderHeight) sits above it. Body content is wrapped
|
|
39
40
|
* the same way ParallaxScrollView wraps its children — padding: 32,
|
|
40
|
-
* gap:
|
|
41
|
+
* gap: 16 — so spacing matches the parallax variant exactly. The
|
|
41
42
|
* spacer that reserves room for the header stays outside that
|
|
42
43
|
* padded wrapper, same as Parallax keeps its header outside its
|
|
43
44
|
* own padded content area.
|
|
@@ -46,6 +47,7 @@ const defaultStyle = StyleSheet.create({
|
|
|
46
47
|
* @param {React.ReactNode} props.children
|
|
47
48
|
* @param {object} [props.headerImage]
|
|
48
49
|
* @param {React.ReactNode} [props.headerOverlay]
|
|
50
|
+
* @param {Function} [props.onBack] Renders a back button over the header image when provided. Omit for no back button.
|
|
49
51
|
* @param {string} [props.backgroundColor]
|
|
50
52
|
* @param {string} [props.headerBackgroundColor]
|
|
51
53
|
* @param {number} [props.headerHeight] Visible image height (excludes the safe-area inset)
|
|
@@ -60,6 +62,7 @@ export default function HeroScreenOverlay({
|
|
|
60
62
|
children,
|
|
61
63
|
headerImage,
|
|
62
64
|
headerOverlay,
|
|
65
|
+
onBack,
|
|
63
66
|
backgroundColor,
|
|
64
67
|
headerBackgroundColor,
|
|
65
68
|
headerHeight = 220,
|
|
@@ -106,6 +109,11 @@ export default function HeroScreenOverlay({
|
|
|
106
109
|
>
|
|
107
110
|
{renderHeaderImage(headerImage)}
|
|
108
111
|
</View>
|
|
112
|
+
|
|
113
|
+
<BackButton
|
|
114
|
+
onBack={onBack}
|
|
115
|
+
style={{ position: 'absolute', top: topOffset + 12, left: 12 }}
|
|
116
|
+
/>
|
|
109
117
|
</View>
|
|
110
118
|
</View>
|
|
111
119
|
);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { View, Pressable, StyleSheet } from 'react-native';
|
|
7
|
+
|
|
8
|
+
const defaultStyle = StyleSheet.create({
|
|
9
|
+
surface: {
|
|
10
|
+
borderRadius: 16,
|
|
11
|
+
overflow: 'hidden',
|
|
12
|
+
},
|
|
13
|
+
content: {
|
|
14
|
+
padding: 16,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Panel
|
|
20
|
+
*
|
|
21
|
+
* Simple rounded-corner surface that sizes to its content. Unlike
|
|
22
|
+
* Tile (built for a centered icon+label grid cell with flex: 1),
|
|
23
|
+
* Panel imposes no layout assumptions on its children at all — no
|
|
24
|
+
* forced centering, no forced flex sizing — so content-driven
|
|
25
|
+
* widgets (lists, text blocks, NewsWidget, etc.) render exactly as
|
|
26
|
+
* they would un-wrapped, just inside a rounded/backgrounded card.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} props
|
|
29
|
+
* @param {React.ReactNode} props.children
|
|
30
|
+
* @param {string} [props.backgroundColor]
|
|
31
|
+
* @param {Function} [props.onPress] If provided, the whole panel becomes pressable.
|
|
32
|
+
* @param {object} [props.style] Style for the outer rounded surface.
|
|
33
|
+
* @param {object} [props.contentStyle] Style for the inner content wrapper (default padding: 16).
|
|
34
|
+
*
|
|
35
|
+
* @returns {JSX.Element}
|
|
36
|
+
*/
|
|
37
|
+
export default function Panel({
|
|
38
|
+
children,
|
|
39
|
+
backgroundColor,
|
|
40
|
+
onPress,
|
|
41
|
+
style,
|
|
42
|
+
contentStyle,
|
|
43
|
+
}) {
|
|
44
|
+
const surfaceStyle = [
|
|
45
|
+
defaultStyle.surface,
|
|
46
|
+
backgroundColor ? { backgroundColor } : null,
|
|
47
|
+
style,
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const content = (
|
|
51
|
+
<View style={[defaultStyle.content, contentStyle]}>
|
|
52
|
+
{children}
|
|
53
|
+
</View>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (onPress) {
|
|
57
|
+
return (
|
|
58
|
+
<Pressable onPress={onPress} style={surfaceStyle}>
|
|
59
|
+
{content}
|
|
60
|
+
</Pressable>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return <View style={surfaceStyle}>{content}</View>;
|
|
65
|
+
}
|
package/lib/components.js
CHANGED
|
@@ -11,6 +11,7 @@ export { default as Tile } from './components/tile.jsx';
|
|
|
11
11
|
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
|
+
export { default as Panel } from './components/panel.jsx';
|
|
14
15
|
|
|
15
16
|
export { openUrl, openApp, openExternal } from './utils/launch.js';
|
|
16
17
|
|
package/lib/index.js
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
View,
|
|
14
14
|
} from 'react-native';
|
|
15
15
|
import Markdown from 'react-native-markdown-display';
|
|
16
|
-
|
|
17
16
|
import HeroScreen from '../components/hero-screen';
|
|
18
17
|
import { fetchNewsJson } from '../actions/news';
|
|
19
18
|
|
|
@@ -53,18 +52,16 @@ export default function NewsItemScreen({
|
|
|
53
52
|
TextComponent = Text,
|
|
54
53
|
MarkdownComponent = Markdown,
|
|
55
54
|
defaultHeaderImage,
|
|
56
|
-
|
|
55
|
+
onBack,
|
|
57
56
|
backgroundColor,
|
|
58
57
|
headerBackgroundColor,
|
|
59
58
|
headerHeight,
|
|
60
|
-
|
|
61
59
|
containerStyle,
|
|
62
60
|
headerStyle,
|
|
63
61
|
contentStyle,
|
|
64
62
|
summaryStyle,
|
|
65
63
|
titleStyle,
|
|
66
64
|
markdownStyle,
|
|
67
|
-
|
|
68
65
|
loadingLabel = 'Loading...',
|
|
69
66
|
errorLabel = 'Unable to load news item.',
|
|
70
67
|
}) {
|
|
@@ -100,10 +97,7 @@ export default function NewsItemScreen({
|
|
|
100
97
|
setLoading(false);
|
|
101
98
|
}
|
|
102
99
|
|
|
103
|
-
if (!item) return;
|
|
104
|
-
|
|
105
100
|
const headerImage = defaultHeaderImage;
|
|
106
|
-
|
|
107
101
|
// const headerImage = item?.image
|
|
108
102
|
// ? {
|
|
109
103
|
// source: { uri: item.image },
|
|
@@ -112,11 +106,47 @@ export default function NewsItemScreen({
|
|
|
112
106
|
// }
|
|
113
107
|
// : defaultHeaderImage;
|
|
114
108
|
|
|
109
|
+
// Loading and error states now render through the same HeroScreen
|
|
110
|
+
// shell (so chrome/background/header stay consistent) instead of
|
|
111
|
+
// bailing out to a blank screen before item is set.
|
|
112
|
+
if (loading || error || !item) {
|
|
113
|
+
return (
|
|
114
|
+
<HeroScreen
|
|
115
|
+
TextComponent={TextComponent}
|
|
116
|
+
titleStyle={titleStyle}
|
|
117
|
+
headerImage={headerImage}
|
|
118
|
+
onBack={onBack}
|
|
119
|
+
backgroundColor={backgroundColor}
|
|
120
|
+
headerBackgroundColor={headerBackgroundColor}
|
|
121
|
+
headerHeight={headerHeight}
|
|
122
|
+
containerStyle={containerStyle}
|
|
123
|
+
headerStyle={headerStyle}
|
|
124
|
+
contentStyle={contentStyle}
|
|
125
|
+
>
|
|
126
|
+
<View style={defaultStyle.content}>
|
|
127
|
+
{loading ? (
|
|
128
|
+
<View>
|
|
129
|
+
<ActivityIndicator />
|
|
130
|
+
<TextComponent>{loadingLabel}</TextComponent>
|
|
131
|
+
</View>
|
|
132
|
+
) : null}
|
|
133
|
+
|
|
134
|
+
{!loading && error ? (
|
|
135
|
+
<TextComponent>
|
|
136
|
+
{error}
|
|
137
|
+
</TextComponent>
|
|
138
|
+
) : null}
|
|
139
|
+
</View>
|
|
140
|
+
</HeroScreen>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
115
144
|
return (
|
|
116
145
|
<HeroScreen
|
|
117
146
|
TextComponent={TextComponent}
|
|
118
147
|
titleStyle={titleStyle}
|
|
119
148
|
headerImage={headerImage}
|
|
149
|
+
onBack={onBack}
|
|
120
150
|
headerOverlay={
|
|
121
151
|
<TextComponent type="title">
|
|
122
152
|
{item.title}
|
|
@@ -130,24 +160,9 @@ export default function NewsItemScreen({
|
|
|
130
160
|
contentStyle={contentStyle}
|
|
131
161
|
>
|
|
132
162
|
<View style={defaultStyle.content}>
|
|
133
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
<TextComponent>{loadingLabel}</TextComponent>
|
|
137
|
-
</View>
|
|
138
|
-
) : null}
|
|
139
|
-
|
|
140
|
-
{!loading && error ? (
|
|
141
|
-
<TextComponent>
|
|
142
|
-
{error}
|
|
143
|
-
</TextComponent>
|
|
144
|
-
) : null}
|
|
145
|
-
|
|
146
|
-
{!loading && !error ? (
|
|
147
|
-
<MarkdownComponent style={markdownStyle}>
|
|
148
|
-
{item?.content || ''}
|
|
149
|
-
</MarkdownComponent>
|
|
150
|
-
) : null}
|
|
163
|
+
<MarkdownComponent style={markdownStyle}>
|
|
164
|
+
{item?.content || ''}
|
|
165
|
+
</MarkdownComponent>
|
|
151
166
|
</View>
|
|
152
167
|
</HeroScreen>
|
|
153
168
|
);
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
+
|
|
5
|
+
import React, {
|
|
6
|
+
forwardRef,
|
|
7
|
+
useEffect,
|
|
8
|
+
useImperativeHandle,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import {
|
|
13
|
+
ActivityIndicator,
|
|
14
|
+
Pressable,
|
|
15
|
+
StyleSheet,
|
|
16
|
+
Text,
|
|
17
|
+
View,
|
|
18
|
+
} from 'react-native';
|
|
19
|
+
|
|
20
|
+
import { fetchNewsJson } from '../actions/news';
|
|
21
|
+
|
|
22
|
+
const defaultStyle = StyleSheet.create({
|
|
23
|
+
container: {
|
|
24
|
+
gap: 8,
|
|
25
|
+
},
|
|
26
|
+
title: {
|
|
27
|
+
fontWeight: '700',
|
|
28
|
+
},
|
|
29
|
+
divider: {
|
|
30
|
+
height: 1,
|
|
31
|
+
backgroundColor: 'rgba(0,0,0,0.15)',
|
|
32
|
+
marginTop: 4,
|
|
33
|
+
marginBottom: 4,
|
|
34
|
+
},
|
|
35
|
+
item: {
|
|
36
|
+
flexDirection: 'row',
|
|
37
|
+
gap: 8,
|
|
38
|
+
},
|
|
39
|
+
bullet: {
|
|
40
|
+
opacity: 0.6,
|
|
41
|
+
},
|
|
42
|
+
itemText: {
|
|
43
|
+
flex: 1,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* NewsWidget
|
|
49
|
+
*
|
|
50
|
+
* Compact, embeddable "latest N news items" panel — meant to sit
|
|
51
|
+
* inside another page's body (e.g. as one of HeroScreen's children
|
|
52
|
+
* sections), not to own the screen itself. Unlike NewsListScreen,
|
|
53
|
+
* it has no ScrollView and no pull-to-refresh, since both break
|
|
54
|
+
* once nested inside a parent page that's already scrollable.
|
|
55
|
+
*
|
|
56
|
+
* Refreshes itself automatically every `refreshIntervalMs` (default
|
|
57
|
+
* 15 minutes). For anything else that should trigger a refetch —
|
|
58
|
+
* most notably a push notification once that infra exists — attach
|
|
59
|
+
* a ref and call `ref.current.refresh()` from wherever that handler
|
|
60
|
+
* ends up living:
|
|
61
|
+
*
|
|
62
|
+
* const newsRef = useRef(null);
|
|
63
|
+
* <NewsWidget ref={newsRef} uri={...} />
|
|
64
|
+
* // later, e.g. inside a notification handler:
|
|
65
|
+
* newsRef.current?.refresh();
|
|
66
|
+
*
|
|
67
|
+
* @param {object} props
|
|
68
|
+
* @param {string} props.uri
|
|
69
|
+
* @param {string} [props.bearerToken]
|
|
70
|
+
* @param {number} [props.limit] Max items shown. Defaults to 4.
|
|
71
|
+
* @param {string} [props.title] Defaults to 'News'.
|
|
72
|
+
* @param {boolean} [props.showDivider] Thin line under the title, like 'News' / '-----'. Defaults to true. Ignored if title is falsy.
|
|
73
|
+
* @param {object} [props.dividerStyle]
|
|
74
|
+
* @param {React.ComponentType} [props.TextComponent] Defaults to Text.
|
|
75
|
+
* @param {Function} [props.onPressItem]
|
|
76
|
+
* @param {number} [props.refreshIntervalMs] Defaults to 15 minutes.
|
|
77
|
+
* @param {string} [props.backgroundColor]
|
|
78
|
+
* @param {object} [props.containerStyle]
|
|
79
|
+
* @param {object} [props.titleStyle]
|
|
80
|
+
* @param {object} [props.itemStyle]
|
|
81
|
+
* @param {object} [props.itemTextStyle]
|
|
82
|
+
* @param {object} [props.errorTextStyle]
|
|
83
|
+
* @param {string} [props.emptyLabel]
|
|
84
|
+
* @param {string} [props.errorLabel]
|
|
85
|
+
*
|
|
86
|
+
* @returns {JSX.Element}
|
|
87
|
+
*/
|
|
88
|
+
const NewsWidget = forwardRef(function NewsWidget(
|
|
89
|
+
{
|
|
90
|
+
uri,
|
|
91
|
+
bearerToken,
|
|
92
|
+
limit = 4,
|
|
93
|
+
title = 'News',
|
|
94
|
+
showDivider = true,
|
|
95
|
+
dividerStyle,
|
|
96
|
+
TextComponent = Text,
|
|
97
|
+
onPressItem,
|
|
98
|
+
refreshIntervalMs = 15 * 60 * 1000,
|
|
99
|
+
|
|
100
|
+
backgroundColor,
|
|
101
|
+
containerStyle,
|
|
102
|
+
titleStyle,
|
|
103
|
+
itemStyle,
|
|
104
|
+
itemTextStyle,
|
|
105
|
+
errorTextStyle,
|
|
106
|
+
|
|
107
|
+
emptyLabel = 'No news items.',
|
|
108
|
+
errorLabel = 'Unable to load news.',
|
|
109
|
+
},
|
|
110
|
+
ref
|
|
111
|
+
) {
|
|
112
|
+
const [items, setItems] = useState([]);
|
|
113
|
+
const [loading, setLoading] = useState(true);
|
|
114
|
+
const [error, setError] = useState(null);
|
|
115
|
+
const intervalRef = useRef(null);
|
|
116
|
+
|
|
117
|
+
async function load() {
|
|
118
|
+
setError(null);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const data = await fetchNewsJson({ uri, bearerToken, force: true });
|
|
122
|
+
setItems((data.items || []).slice(0, limit));
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
setError(e.message || errorLabel);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
setLoading(false);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
useImperativeHandle(ref, () => ({
|
|
132
|
+
refresh: load,
|
|
133
|
+
}), [uri, bearerToken, limit]);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
setLoading(true);
|
|
137
|
+
load();
|
|
138
|
+
|
|
139
|
+
intervalRef.current = setInterval(load, refreshIntervalMs);
|
|
140
|
+
|
|
141
|
+
return () => clearInterval(intervalRef.current);
|
|
142
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
143
|
+
}, [uri, bearerToken, limit, refreshIntervalMs]);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<View
|
|
147
|
+
style={[
|
|
148
|
+
defaultStyle.container,
|
|
149
|
+
backgroundColor ? { backgroundColor } : null,
|
|
150
|
+
containerStyle,
|
|
151
|
+
]}
|
|
152
|
+
>
|
|
153
|
+
{title ? (
|
|
154
|
+
<TextComponent style={[defaultStyle.title, titleStyle]}>
|
|
155
|
+
{title}
|
|
156
|
+
</TextComponent>
|
|
157
|
+
) : null}
|
|
158
|
+
|
|
159
|
+
{title && showDivider ? (
|
|
160
|
+
<View style={[defaultStyle.divider, dividerStyle]} />
|
|
161
|
+
) : null}
|
|
162
|
+
|
|
163
|
+
{loading ? <ActivityIndicator /> : null}
|
|
164
|
+
|
|
165
|
+
{!loading && error ? (
|
|
166
|
+
<TextComponent style={errorTextStyle}>{error}</TextComponent>
|
|
167
|
+
) : null}
|
|
168
|
+
|
|
169
|
+
{!loading && !error && !items.length ? (
|
|
170
|
+
<TextComponent style={errorTextStyle}>{emptyLabel}</TextComponent>
|
|
171
|
+
) : null}
|
|
172
|
+
|
|
173
|
+
{!loading && !error && items.map((item) => (
|
|
174
|
+
<Pressable
|
|
175
|
+
key={item.id}
|
|
176
|
+
style={[defaultStyle.item, itemStyle]}
|
|
177
|
+
onPress={() => onPressItem?.(item)}
|
|
178
|
+
>
|
|
179
|
+
<TextComponent style={defaultStyle.bullet}>{'\u2022'}</TextComponent>
|
|
180
|
+
<TextComponent style={[defaultStyle.itemText, itemTextStyle]}>
|
|
181
|
+
{item.title}
|
|
182
|
+
</TextComponent>
|
|
183
|
+
</Pressable>
|
|
184
|
+
))}
|
|
185
|
+
</View>
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
export default NewsWidget;
|
|
190
|
+
|
package/lib/widgets.js
ADDED
package/package.json
CHANGED
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
"./components": "./lib/components.js",
|
|
14
14
|
"./globals": "./lib/screen-registry.js",
|
|
15
15
|
"./hero-screen-registry": "./lib/hero-screen-registry.js",
|
|
16
|
-
"./hooks/use-safe-area-top-inset": "./lib/hooks/use-safe-area-top-inset.js",
|
|
17
16
|
"./lite": "./lib/hero-screen-registry.js",
|
|
18
17
|
"./notifications": "./lib/notifications.js",
|
|
19
18
|
"./notifications-fcm": "./lib/notifications/fcm.js",
|
|
20
19
|
"./screen-registry": "./lib/screen-registry.js",
|
|
21
|
-
"./screens": "./lib/screens.js"
|
|
20
|
+
"./screens": "./lib/screens.js",
|
|
21
|
+
"./widgets": "./lib/widgets.js"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [],
|
|
24
24
|
"license": "GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions",
|
|
@@ -34,5 +34,5 @@
|
|
|
34
34
|
},
|
|
35
35
|
"sideEffects": false,
|
|
36
36
|
"type": "module",
|
|
37
|
-
"version": "0.0.
|
|
37
|
+
"version": "0.0.14"
|
|
38
38
|
}
|