@opndev/react-native-events 0.0.13 → 0.0.15
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 +10 -0
- package/lib/components/app-screen-top-bar.jsx +1 -1
- package/lib/components/app-screen.jsx +2 -1
- package/lib/components/hero-screen-fixed.jsx +21 -7
- package/lib/components/hero-screen-header.jsx +157 -4
- package/lib/components/hero-screen-parallax.jsx +50 -21
- package/lib/components/hero-screen.jsx +20 -6
- package/lib/components/panel.jsx +99 -0
- package/lib/components/parallax-scroll-view.js +5 -2
- package/lib/components.js +1 -0
- package/lib/debug.js +5 -0
- package/lib/index.js +1 -1
- package/lib/utils/route-debug.js +21 -0
- package/lib/widgets/news-widget.jsx +19 -0
- package/package.json +2 -1
package/Changes
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
Revision history for @opndev/opndev-react-native-events
|
|
2
2
|
|
|
3
|
+
0.0.15 2026-06-26 22:56:26Z
|
|
4
|
+
|
|
5
|
+
* Add debugRouteState() to debug export to debug route states more easily
|
|
6
|
+
* Throw shade at panels: I mean shadows, we not dissing panels here.
|
|
7
|
+
|
|
8
|
+
0.0.14 2026-06-26 04:53:23Z
|
|
9
|
+
|
|
10
|
+
* Add Panel instead of a Tile, Tiles are more buttony thing. Panels are
|
|
11
|
+
panels. ha.
|
|
12
|
+
|
|
3
13
|
0.0.13 2026-06-26 03:59:55Z
|
|
4
14
|
|
|
5
15
|
* Update news item segment to be included as a widget.
|
|
@@ -84,10 +84,10 @@ export default function AppScreenTopBar({
|
|
|
84
84
|
return (
|
|
85
85
|
<View
|
|
86
86
|
style={[
|
|
87
|
+
style,
|
|
87
88
|
styles.bar,
|
|
88
89
|
{ height, justifyContent, paddingHorizontal: horizontalPadding, paddingBottom: verticalPadding },
|
|
89
90
|
backgroundColor ? { backgroundColor } : null,
|
|
90
|
-
style,
|
|
91
91
|
]}
|
|
92
92
|
>
|
|
93
93
|
{applyBarStyling(logo, { height, maxWidthPixels })}
|
|
@@ -1,7 +1,6 @@
|
|
|
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 { View, StyleSheet } from 'react-native';
|
|
6
5
|
import { useSafeAreaTopInset } from '../hooks/use-safe-area-top-inset';
|
|
7
6
|
import { AppScreenOffsetContext } from '../hooks/use-app-screen-offset';
|
|
@@ -17,6 +16,7 @@ const defaultStyle = StyleSheet.create({
|
|
|
17
16
|
left: 0,
|
|
18
17
|
width: '100%',
|
|
19
18
|
zIndex: 10,
|
|
19
|
+
elevation: 10,
|
|
20
20
|
},
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -67,6 +67,7 @@ export default function AppScreen({
|
|
|
67
67
|
style={[
|
|
68
68
|
defaultStyle.topBarWrap,
|
|
69
69
|
{ height: offset, paddingTop: insetTop },
|
|
70
|
+
topBar.props?.backgroundColor ? { backgroundColor: topBar.props.backgroundColor } : null,
|
|
70
71
|
]}
|
|
71
72
|
>
|
|
72
73
|
{topBar}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { View, ScrollView, StyleSheet } from 'react-native';
|
|
6
6
|
import {
|
|
7
|
-
renderHeaderImage,
|
|
8
7
|
HeaderOverlay,
|
|
8
|
+
HeaderContent,
|
|
9
|
+
splitCarouselChildren,
|
|
9
10
|
useHeroHeaderHeight,
|
|
10
11
|
heroContentStyle,
|
|
11
12
|
} from './hero-screen-header';
|
|
@@ -38,9 +39,12 @@ const defaultStyle = StyleSheet.create({
|
|
|
38
39
|
* gap: 16 — so spacing matches the parallax variant exactly.
|
|
39
40
|
*
|
|
40
41
|
* @param {object} props
|
|
41
|
-
* @param {React.ReactNode} props.children
|
|
42
|
-
* @param {object} [props.headerImage]
|
|
42
|
+
* @param {React.ReactNode} props.children Normal body content, and/or <CarouselScreen> elements for slides.
|
|
43
|
+
* @param {object} [props.headerImage] Used when there are no <CarouselScreen> children.
|
|
43
44
|
* @param {React.ReactNode} [props.headerOverlay]
|
|
45
|
+
* @param {(slide: { item: any, route: any }, index: number) => void} [props.onSlidePress]
|
|
46
|
+
* @param {number} [props.autoPlayInterval] Defaults to 4000. Pass 0 to disable.
|
|
47
|
+
* @param {boolean} [props.loop] Defaults to true.
|
|
44
48
|
* @param {string} [props.backgroundColor]
|
|
45
49
|
* @param {string} [props.headerBackgroundColor]
|
|
46
50
|
* @param {number} [props.headerHeight] Visible image height (excludes the safe-area inset)
|
|
@@ -55,6 +59,9 @@ export default function HeroScreenFixed({
|
|
|
55
59
|
children,
|
|
56
60
|
headerImage,
|
|
57
61
|
headerOverlay,
|
|
62
|
+
onSlidePress,
|
|
63
|
+
autoPlayInterval,
|
|
64
|
+
loop,
|
|
58
65
|
backgroundColor,
|
|
59
66
|
headerBackgroundColor,
|
|
60
67
|
headerHeight = 220,
|
|
@@ -63,7 +70,8 @@ export default function HeroScreenFixed({
|
|
|
63
70
|
headerStyle,
|
|
64
71
|
contentStyle,
|
|
65
72
|
}) {
|
|
66
|
-
const {
|
|
73
|
+
const { topOffset, totalHeaderHeight } = useHeroHeaderHeight(headerHeight);
|
|
74
|
+
const { slideElements, bodyChildren } = splitCarouselChildren(children);
|
|
67
75
|
|
|
68
76
|
return (
|
|
69
77
|
<View
|
|
@@ -84,10 +92,16 @@ export default function HeroScreenFixed({
|
|
|
84
92
|
<View
|
|
85
93
|
style={[
|
|
86
94
|
defaultStyle.imageInner,
|
|
87
|
-
{ top:
|
|
95
|
+
{ top: topOffset, height: headerHeight },
|
|
88
96
|
]}
|
|
89
97
|
>
|
|
90
|
-
|
|
98
|
+
<HeaderContent
|
|
99
|
+
headerImage={headerImage}
|
|
100
|
+
slideElements={slideElements}
|
|
101
|
+
onSlidePress={onSlidePress}
|
|
102
|
+
autoPlayInterval={autoPlayInterval}
|
|
103
|
+
loop={loop}
|
|
104
|
+
/>
|
|
91
105
|
</View>
|
|
92
106
|
</View>
|
|
93
107
|
|
|
@@ -97,7 +111,7 @@ export default function HeroScreenFixed({
|
|
|
97
111
|
>
|
|
98
112
|
<View style={[heroContentStyle, contentStyle]}>
|
|
99
113
|
<HeaderOverlay headerOverlay={headerOverlay} />
|
|
100
|
-
{
|
|
114
|
+
{bodyChildren}
|
|
101
115
|
</View>
|
|
102
116
|
</ContentComponent>
|
|
103
117
|
</View>
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
-
|
|
1
|
+
import { useState, isValidElement, Children } from 'react';
|
|
5
2
|
import { View, Image, Pressable, StyleSheet } from 'react-native';
|
|
6
3
|
import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
|
|
4
|
+
import Carousel from 'react-native-reanimated-carousel';
|
|
7
5
|
import { useAppScreenOffset } from '../hooks/use-app-screen-offset';
|
|
8
6
|
|
|
9
7
|
const styles = StyleSheet.create({
|
|
@@ -25,6 +23,27 @@ const styles = StyleSheet.create({
|
|
|
25
23
|
shadowOpacity: 0.3,
|
|
26
24
|
shadowRadius: 3,
|
|
27
25
|
},
|
|
26
|
+
carouselFill: StyleSheet.absoluteFillObject,
|
|
27
|
+
slideFill: { flex: 1 },
|
|
28
|
+
dotsRow: {
|
|
29
|
+
position: 'absolute',
|
|
30
|
+
bottom: 12,
|
|
31
|
+
left: 0,
|
|
32
|
+
right: 0,
|
|
33
|
+
flexDirection: 'row',
|
|
34
|
+
justifyContent: 'center',
|
|
35
|
+
gap: 6,
|
|
36
|
+
},
|
|
37
|
+
dot: {
|
|
38
|
+
width: 6,
|
|
39
|
+
height: 6,
|
|
40
|
+
borderRadius: 3,
|
|
41
|
+
backgroundColor: 'rgba(255,255,255,0.5)',
|
|
42
|
+
},
|
|
43
|
+
dotActive: {
|
|
44
|
+
backgroundColor: '#FFFFFF',
|
|
45
|
+
width: 16,
|
|
46
|
+
},
|
|
28
47
|
});
|
|
29
48
|
|
|
30
49
|
/**
|
|
@@ -66,6 +85,140 @@ export function HeaderOverlay({ headerOverlay }) {
|
|
|
66
85
|
return <View style={styles.titleWrap}>{headerOverlay}</View>;
|
|
67
86
|
}
|
|
68
87
|
|
|
88
|
+
/**
|
|
89
|
+
* CarouselScreen
|
|
90
|
+
*
|
|
91
|
+
* A marker component, not a real standalone screen — used as a
|
|
92
|
+
* child of any Hero variant to declare one carousel slide:
|
|
93
|
+
*
|
|
94
|
+
* <HeroScreenFixed onSlidePress={(slide) => router.push(slide.route)}>
|
|
95
|
+
* <CarouselScreen item={fooData} route="/foo">
|
|
96
|
+
* ...whatever JSX you want for this slide...
|
|
97
|
+
* </CarouselScreen>
|
|
98
|
+
* <CarouselScreen item={barData} route="/bar">
|
|
99
|
+
* ...
|
|
100
|
+
* </CarouselScreen>
|
|
101
|
+
* </HeroScreenFixed>
|
|
102
|
+
*
|
|
103
|
+
* Every Hero variant scans its children for these by type (via
|
|
104
|
+
* splitCarouselChildren below) and pulls out `item`/`route` plus
|
|
105
|
+
* your own nested children for each slide. Any non-CarouselScreen
|
|
106
|
+
* children are treated as normal body content, same as always.
|
|
107
|
+
*
|
|
108
|
+
* If rendered standalone it just renders its own children, so it's
|
|
109
|
+
* harmless either way.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} props
|
|
112
|
+
* @param {any} [props.item]
|
|
113
|
+
* @param {any} [props.route]
|
|
114
|
+
* @param {React.ReactNode} props.children
|
|
115
|
+
*/
|
|
116
|
+
export function CarouselScreen({ children }) {
|
|
117
|
+
return children ?? null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* splitCarouselChildren
|
|
122
|
+
*
|
|
123
|
+
* Separates a Hero variant's children into carousel slides
|
|
124
|
+
* (CarouselScreen elements) and normal body content (everything
|
|
125
|
+
* else). Shared by every Hero variant so the splitting logic is
|
|
126
|
+
* identical across all of them.
|
|
127
|
+
*
|
|
128
|
+
* @param {React.ReactNode} children
|
|
129
|
+
* @returns {{ slideElements: React.ReactElement[], bodyChildren: React.ReactNode[] }}
|
|
130
|
+
*/
|
|
131
|
+
export function splitCarouselChildren(children) {
|
|
132
|
+
const childArray = Children.toArray(children);
|
|
133
|
+
|
|
134
|
+
const slideElements = childArray.filter(
|
|
135
|
+
(child) => isValidElement(child) && child.type === CarouselScreen
|
|
136
|
+
);
|
|
137
|
+
const bodyChildren = childArray.filter(
|
|
138
|
+
(child) => !(isValidElement(child) && child.type === CarouselScreen)
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return { slideElements, bodyChildren };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* HeaderContent
|
|
146
|
+
*
|
|
147
|
+
* The single thing every Hero variant renders inside its header
|
|
148
|
+
* box — decides whether that's a static image (renderHeaderImage,
|
|
149
|
+
* unchanged default behaviour) or a swipeable/auto-advancing
|
|
150
|
+
* carousel (when CarouselScreen children were found), so a Hero
|
|
151
|
+
* variant only ever needs to swap one line to gain carousel
|
|
152
|
+
* support, with zero changes to its own scroll/positioning logic.
|
|
153
|
+
*
|
|
154
|
+
* Fills whatever box it's placed in — each variant owns its own
|
|
155
|
+
* positioning (Fixed/Overlay via their own imageInner wrapper,
|
|
156
|
+
* Parallax via ParallaxScrollView's existing wrapper) — this
|
|
157
|
+
* component doesn't position itself.
|
|
158
|
+
*
|
|
159
|
+
* @param {object} props
|
|
160
|
+
* @param {object} [props.headerImage] Used when there are no slides.
|
|
161
|
+
* @param {React.ReactElement[]} [props.slideElements] From splitCarouselChildren. When empty/absent, renders the static image instead.
|
|
162
|
+
* @param {(slide: { item: any, route: any }, index: number) => void} [props.onSlidePress]
|
|
163
|
+
* @param {number} [props.autoPlayInterval] Defaults to 4000. Pass 0 to disable.
|
|
164
|
+
* @param {boolean} [props.loop] Defaults to true.
|
|
165
|
+
*
|
|
166
|
+
* @returns {JSX.Element|null}
|
|
167
|
+
*/
|
|
168
|
+
export function HeaderContent({
|
|
169
|
+
headerImage,
|
|
170
|
+
slideElements,
|
|
171
|
+
onSlidePress,
|
|
172
|
+
autoPlayInterval = 4000,
|
|
173
|
+
loop = true,
|
|
174
|
+
}) {
|
|
175
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
176
|
+
|
|
177
|
+
if (!slideElements || slideElements.length === 0) {
|
|
178
|
+
return renderHeaderImage(headerImage);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<View style={styles.carouselFill}>
|
|
183
|
+
<Carousel
|
|
184
|
+
style={{ width: '100%', height: '100%' }}
|
|
185
|
+
data={slideElements}
|
|
186
|
+
loop={loop}
|
|
187
|
+
autoPlay={autoPlayInterval > 0}
|
|
188
|
+
autoPlayInterval={autoPlayInterval}
|
|
189
|
+
onSnapToItem={setActiveIndex}
|
|
190
|
+
renderItem={({ item: slideElement, index }) => {
|
|
191
|
+
const { item, route } = slideElement.props;
|
|
192
|
+
const onPress = onSlidePress
|
|
193
|
+
? () => onSlidePress({ item, route }, index)
|
|
194
|
+
: undefined;
|
|
195
|
+
|
|
196
|
+
if (!onPress) {
|
|
197
|
+
return <View style={styles.slideFill}>{slideElement}</View>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<Pressable style={styles.slideFill} onPress={onPress}>
|
|
202
|
+
{slideElement}
|
|
203
|
+
</Pressable>
|
|
204
|
+
);
|
|
205
|
+
}}
|
|
206
|
+
/>
|
|
207
|
+
|
|
208
|
+
{slideElements.length > 1 ? (
|
|
209
|
+
<View style={styles.dotsRow}>
|
|
210
|
+
{slideElements.map((_, i) => (
|
|
211
|
+
<View
|
|
212
|
+
key={i}
|
|
213
|
+
style={[styles.dot, i === activeIndex ? styles.dotActive : null]}
|
|
214
|
+
/>
|
|
215
|
+
))}
|
|
216
|
+
</View>
|
|
217
|
+
) : null}
|
|
218
|
+
</View>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
69
222
|
/**
|
|
70
223
|
* useTopOffset
|
|
71
224
|
*
|
|
@@ -2,49 +2,78 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
HeaderOverlay,
|
|
7
|
+
HeaderContent,
|
|
8
|
+
splitCarouselChildren,
|
|
9
|
+
} from './hero-screen-header';
|
|
6
10
|
import ParallaxScrollView from './parallax-scroll-view';
|
|
7
|
-
import { renderHeaderImage, HeaderOverlay } from './hero-screen-header';
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
/**
|
|
13
|
+
* HeroScreenParallax
|
|
14
|
+
*
|
|
15
|
+
* Header image scales/translates with scroll (the classic parallax
|
|
16
|
+
* effect) via ParallaxScrollView. Carousel support needed no new
|
|
17
|
+
* positioning logic here at all — ParallaxScrollView already treats
|
|
18
|
+
* its header as an opaque element it animates, so swapping in
|
|
19
|
+
* HeaderContent (image-or-carousel) instead of a plain image just
|
|
20
|
+
* works, same one-line swap as Fixed/Overlay.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} props
|
|
23
|
+
* @param {React.ReactNode} props.children Normal body content, and/or <CarouselScreen> elements for slides.
|
|
24
|
+
* @param {object} [props.headerImage] Used when there are no <CarouselScreen> children.
|
|
25
|
+
* @param {React.ReactNode} [props.headerOverlay]
|
|
26
|
+
* @param {(slide: { item: any, route: any }, index: number) => void} [props.onSlidePress]
|
|
27
|
+
* @param {number} [props.autoPlayInterval] Defaults to 4000. Pass 0 to disable.
|
|
28
|
+
* @param {boolean} [props.loop] Defaults to true.
|
|
29
|
+
* @param {string} [props.backgroundColor]
|
|
30
|
+
* @param {string} [props.headerBackgroundColor]
|
|
31
|
+
* @param {number} [props.headerHeight] Visible image height (excludes the safe-area inset)
|
|
32
|
+
* @param {React.ComponentType} [props.ContentComponent]
|
|
33
|
+
* @param {object} [props.containerStyle]
|
|
34
|
+
* @param {object} [props.headerStyle]
|
|
35
|
+
* @param {object} [props.contentStyle]
|
|
36
|
+
*
|
|
37
|
+
* @returns {JSX.Element}
|
|
38
|
+
*/
|
|
39
|
+
export default function HeroScreenParallax({
|
|
20
40
|
children,
|
|
21
41
|
headerImage,
|
|
22
42
|
headerOverlay,
|
|
43
|
+
onSlidePress,
|
|
44
|
+
autoPlayInterval,
|
|
45
|
+
loop,
|
|
23
46
|
backgroundColor,
|
|
24
47
|
headerBackgroundColor,
|
|
25
48
|
headerHeight,
|
|
26
|
-
ContentComponent,
|
|
27
49
|
containerStyle,
|
|
28
50
|
headerStyle,
|
|
29
51
|
contentStyle,
|
|
52
|
+
ContentComponent,
|
|
30
53
|
}) {
|
|
54
|
+
const { slideElements, bodyChildren } = splitCarouselChildren(children);
|
|
55
|
+
|
|
31
56
|
return (
|
|
32
57
|
<ParallaxScrollView
|
|
58
|
+
headerImage={
|
|
59
|
+
<HeaderContent
|
|
60
|
+
headerImage={headerImage}
|
|
61
|
+
slideElements={slideElements}
|
|
62
|
+
onSlidePress={onSlidePress}
|
|
63
|
+
autoPlayInterval={autoPlayInterval}
|
|
64
|
+
loop={loop}
|
|
65
|
+
/>
|
|
66
|
+
}
|
|
33
67
|
backgroundColor={backgroundColor}
|
|
34
68
|
headerBackgroundColor={headerBackgroundColor}
|
|
35
69
|
headerHeight={headerHeight}
|
|
36
|
-
ContentComponent={ContentComponent}
|
|
37
70
|
containerStyle={containerStyle}
|
|
38
71
|
headerStyle={headerStyle}
|
|
39
72
|
contentStyle={contentStyle}
|
|
40
|
-
|
|
41
|
-
<View style={defaultStyle.headerImageWrap}>
|
|
42
|
-
{renderHeaderImage(headerImage)}
|
|
43
|
-
</View>
|
|
44
|
-
}
|
|
73
|
+
ContentComponent={ContentComponent}
|
|
45
74
|
>
|
|
46
75
|
<HeaderOverlay headerOverlay={headerOverlay} />
|
|
47
|
-
{
|
|
76
|
+
{bodyChildren}
|
|
48
77
|
</ParallaxScrollView>
|
|
49
78
|
);
|
|
50
79
|
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { View, ScrollView, StyleSheet } from 'react-native';
|
|
6
6
|
import {
|
|
7
|
-
renderHeaderImage,
|
|
8
7
|
HeaderOverlay,
|
|
8
|
+
HeaderContent,
|
|
9
|
+
splitCarouselChildren,
|
|
9
10
|
BackButton,
|
|
10
11
|
useHeroHeaderHeight,
|
|
11
12
|
heroContentStyle,
|
|
@@ -44,10 +45,13 @@ const defaultStyle = StyleSheet.create({
|
|
|
44
45
|
* own padded content area.
|
|
45
46
|
*
|
|
46
47
|
* @param {object} props
|
|
47
|
-
* @param {React.ReactNode} props.children
|
|
48
|
-
* @param {object} [props.headerImage]
|
|
48
|
+
* @param {React.ReactNode} props.children Normal body content, and/or <CarouselScreen> elements for slides.
|
|
49
|
+
* @param {object} [props.headerImage] Used when there are no <CarouselScreen> children.
|
|
49
50
|
* @param {React.ReactNode} [props.headerOverlay]
|
|
50
|
-
* @param {Function} [props.onBack] Renders a back button over the header
|
|
51
|
+
* @param {Function} [props.onBack] Renders a back button over the header when provided. Omit for no back button.
|
|
52
|
+
* @param {(slide: { item: any, route: any }, index: number) => void} [props.onSlidePress]
|
|
53
|
+
* @param {number} [props.autoPlayInterval] Defaults to 4000. Pass 0 to disable.
|
|
54
|
+
* @param {boolean} [props.loop] Defaults to true.
|
|
51
55
|
* @param {string} [props.backgroundColor]
|
|
52
56
|
* @param {string} [props.headerBackgroundColor]
|
|
53
57
|
* @param {number} [props.headerHeight] Visible image height (excludes the safe-area inset)
|
|
@@ -63,6 +67,9 @@ export default function HeroScreenOverlay({
|
|
|
63
67
|
headerImage,
|
|
64
68
|
headerOverlay,
|
|
65
69
|
onBack,
|
|
70
|
+
onSlidePress,
|
|
71
|
+
autoPlayInterval,
|
|
72
|
+
loop,
|
|
66
73
|
backgroundColor,
|
|
67
74
|
headerBackgroundColor,
|
|
68
75
|
headerHeight = 220,
|
|
@@ -72,6 +79,7 @@ export default function HeroScreenOverlay({
|
|
|
72
79
|
contentStyle,
|
|
73
80
|
}) {
|
|
74
81
|
const { topOffset, totalHeaderHeight } = useHeroHeaderHeight(headerHeight);
|
|
82
|
+
const { slideElements, bodyChildren } = splitCarouselChildren(children);
|
|
75
83
|
|
|
76
84
|
return (
|
|
77
85
|
<View
|
|
@@ -89,7 +97,7 @@ export default function HeroScreenOverlay({
|
|
|
89
97
|
|
|
90
98
|
<View style={[heroContentStyle, contentStyle]}>
|
|
91
99
|
<HeaderOverlay headerOverlay={headerOverlay} />
|
|
92
|
-
{
|
|
100
|
+
{bodyChildren}
|
|
93
101
|
</View>
|
|
94
102
|
</ContentComponent>
|
|
95
103
|
|
|
@@ -107,7 +115,13 @@ export default function HeroScreenOverlay({
|
|
|
107
115
|
{ top: topOffset, height: headerHeight },
|
|
108
116
|
]}
|
|
109
117
|
>
|
|
110
|
-
|
|
118
|
+
<HeaderContent
|
|
119
|
+
headerImage={headerImage}
|
|
120
|
+
slideElements={slideElements}
|
|
121
|
+
onSlidePress={onSlidePress}
|
|
122
|
+
autoPlayInterval={autoPlayInterval}
|
|
123
|
+
loop={loop}
|
|
124
|
+
/>
|
|
111
125
|
</View>
|
|
112
126
|
|
|
113
127
|
<BackButton
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
shadowWrap: {
|
|
10
|
+
borderRadius: 16,
|
|
11
|
+
},
|
|
12
|
+
shadowOn: {
|
|
13
|
+
elevation: 4,
|
|
14
|
+
shadowColor: '#000',
|
|
15
|
+
shadowOffset: { width: 0, height: 2 },
|
|
16
|
+
shadowOpacity: 0.15,
|
|
17
|
+
shadowRadius: 6,
|
|
18
|
+
},
|
|
19
|
+
surface: {
|
|
20
|
+
borderRadius: 16,
|
|
21
|
+
overflow: 'hidden',
|
|
22
|
+
},
|
|
23
|
+
content: {
|
|
24
|
+
padding: 16,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Panel
|
|
30
|
+
*
|
|
31
|
+
* Simple rounded-corner surface that sizes to its content. Unlike
|
|
32
|
+
* Tile (built for a centered icon+label grid cell with flex: 1),
|
|
33
|
+
* Panel imposes no layout assumptions on its children at all — no
|
|
34
|
+
* forced centering, no forced flex sizing — so content-driven
|
|
35
|
+
* widgets (lists, text blocks, NewsWidget, etc.) render exactly as
|
|
36
|
+
* they would un-wrapped, just inside a rounded/backgrounded card.
|
|
37
|
+
*
|
|
38
|
+
* Internally split into two layers — an outer view that casts the
|
|
39
|
+
* shadow (when enabled) and an inner view that clips content to the
|
|
40
|
+
* rounded corners. This is deliberate: a shadow renders outside a
|
|
41
|
+
* view's own bounds, so a single view with both `overflow: hidden`
|
|
42
|
+
* and a shadow would clip its own shadow into invisibility. The
|
|
43
|
+
* split means `shadow` can be toggled on/off with no other changes
|
|
44
|
+
* needed, and panels that don't use it are unaffected.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} props
|
|
47
|
+
* @param {React.ReactNode} props.children
|
|
48
|
+
* @param {string} [props.backgroundColor]
|
|
49
|
+
* @param {boolean} [props.shadow] Floating-card drop shadow. Defaults to false.
|
|
50
|
+
* @param {Function} [props.onPress] If provided, the whole panel becomes pressable.
|
|
51
|
+
* @param {object} [props.style] Style for the outer (shadow-casting) wrapper.
|
|
52
|
+
* @param {object} [props.contentStyle] Style for the inner content wrapper (default padding: 16).
|
|
53
|
+
*
|
|
54
|
+
* @returns {JSX.Element}
|
|
55
|
+
*/
|
|
56
|
+
export default function Panel({
|
|
57
|
+
children,
|
|
58
|
+
backgroundColor,
|
|
59
|
+
shadow = false,
|
|
60
|
+
onPress,
|
|
61
|
+
style,
|
|
62
|
+
contentStyle,
|
|
63
|
+
}) {
|
|
64
|
+
const surfaceStyle = [
|
|
65
|
+
defaultStyle.surface,
|
|
66
|
+
backgroundColor ? { backgroundColor } : null,
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const content = (
|
|
70
|
+
<View style={[defaultStyle.content, contentStyle]}>
|
|
71
|
+
{children}
|
|
72
|
+
</View>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const inner = onPress ? (
|
|
76
|
+
<Pressable onPress={onPress} style={surfaceStyle}>
|
|
77
|
+
{content}
|
|
78
|
+
</Pressable>
|
|
79
|
+
) : (
|
|
80
|
+
<View style={surfaceStyle}>{content}</View>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<View
|
|
85
|
+
style={[
|
|
86
|
+
defaultStyle.shadowWrap,
|
|
87
|
+
shadow ? defaultStyle.shadowOn : null,
|
|
88
|
+
// Android's elevation shadow needs an opaque shape with a
|
|
89
|
+
// matching borderRadius to cast a correctly-rounded shadow —
|
|
90
|
+
// a fully transparent outer view can shadow as a square box
|
|
91
|
+
// instead of following the rounded silhouette.
|
|
92
|
+
shadow && backgroundColor ? { backgroundColor } : null,
|
|
93
|
+
style,
|
|
94
|
+
]}
|
|
95
|
+
>
|
|
96
|
+
{inner}
|
|
97
|
+
</View>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -11,7 +11,7 @@ import Animated, {
|
|
|
11
11
|
} from 'react-native-reanimated';
|
|
12
12
|
import { useTopOffset, heroContentStyle } from './hero-screen-header';
|
|
13
13
|
|
|
14
|
-
const DEFAULT_HEADER_HEIGHT =
|
|
14
|
+
const DEFAULT_HEADER_HEIGHT = 220;
|
|
15
15
|
|
|
16
16
|
const defaultStyle = StyleSheet.create({
|
|
17
17
|
container: {
|
|
@@ -50,8 +50,11 @@ const defaultStyle = StyleSheet.create({
|
|
|
50
50
|
* Background color for the scroll view container.
|
|
51
51
|
* @param {string} [props.headerBackgroundColor]
|
|
52
52
|
* Background color shown behind the header image.
|
|
53
|
-
* @param {number} [props.headerHeight=
|
|
53
|
+
* @param {number} [props.headerHeight=220]
|
|
54
54
|
* Visible height of the parallax header image (excludes the top offset).
|
|
55
|
+
* Matches HeroScreenFixed/HeroScreenOverlay's default — kept in
|
|
56
|
+
* sync deliberately so all three variants render identically when
|
|
57
|
+
* no explicit headerHeight is passed.
|
|
55
58
|
* @param {React.ComponentType<{style?: any, children?: React.ReactNode}>}
|
|
56
59
|
* [props.ContentComponent=View]
|
|
57
60
|
* Wrapper used for the content area below the header.
|
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/debug.js
ADDED
package/lib/index.js
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
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 { useNavigationState } from '@react-navigation/native';
|
|
6
|
+
import { useRouter } from 'expo-router';
|
|
7
|
+
|
|
8
|
+
export function debugRouteState() {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const navState = useNavigationState((state) => state);
|
|
11
|
+
|
|
12
|
+
console.log('[news stack]',
|
|
13
|
+
JSON.stringify(navState?.routes?.map((r) => (
|
|
14
|
+
{ name: r.name, params: r.params, key: r.key }
|
|
15
|
+
)),
|
|
16
|
+
null,
|
|
17
|
+
2
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
+
|
|
1
5
|
import React, {
|
|
2
6
|
forwardRef,
|
|
3
7
|
useEffect,
|
|
@@ -22,6 +26,12 @@ const defaultStyle = StyleSheet.create({
|
|
|
22
26
|
title: {
|
|
23
27
|
fontWeight: '700',
|
|
24
28
|
},
|
|
29
|
+
divider: {
|
|
30
|
+
height: 1,
|
|
31
|
+
backgroundColor: 'rgba(0,0,0,0.15)',
|
|
32
|
+
marginTop: 4,
|
|
33
|
+
marginBottom: 4,
|
|
34
|
+
},
|
|
25
35
|
item: {
|
|
26
36
|
flexDirection: 'row',
|
|
27
37
|
gap: 8,
|
|
@@ -59,6 +69,8 @@ const defaultStyle = StyleSheet.create({
|
|
|
59
69
|
* @param {string} [props.bearerToken]
|
|
60
70
|
* @param {number} [props.limit] Max items shown. Defaults to 4.
|
|
61
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]
|
|
62
74
|
* @param {React.ComponentType} [props.TextComponent] Defaults to Text.
|
|
63
75
|
* @param {Function} [props.onPressItem]
|
|
64
76
|
* @param {number} [props.refreshIntervalMs] Defaults to 15 minutes.
|
|
@@ -79,6 +91,8 @@ const NewsWidget = forwardRef(function NewsWidget(
|
|
|
79
91
|
bearerToken,
|
|
80
92
|
limit = 4,
|
|
81
93
|
title = 'News',
|
|
94
|
+
showDivider = true,
|
|
95
|
+
dividerStyle,
|
|
82
96
|
TextComponent = Text,
|
|
83
97
|
onPressItem,
|
|
84
98
|
refreshIntervalMs = 15 * 60 * 1000,
|
|
@@ -142,6 +156,10 @@ const NewsWidget = forwardRef(function NewsWidget(
|
|
|
142
156
|
</TextComponent>
|
|
143
157
|
) : null}
|
|
144
158
|
|
|
159
|
+
{title && showDivider ? (
|
|
160
|
+
<View style={[defaultStyle.divider, dividerStyle]} />
|
|
161
|
+
) : null}
|
|
162
|
+
|
|
145
163
|
{loading ? <ActivityIndicator /> : null}
|
|
146
164
|
|
|
147
165
|
{!loading && error ? (
|
|
@@ -169,3 +187,4 @@ const NewsWidget = forwardRef(function NewsWidget(
|
|
|
169
187
|
});
|
|
170
188
|
|
|
171
189
|
export default NewsWidget;
|
|
190
|
+
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"exports": {
|
|
12
12
|
".": "./lib/index.js",
|
|
13
13
|
"./components": "./lib/components.js",
|
|
14
|
+
"./debug": "./lib/debug.js",
|
|
14
15
|
"./globals": "./lib/screen-registry.js",
|
|
15
16
|
"./hero-screen-registry": "./lib/hero-screen-registry.js",
|
|
16
17
|
"./lite": "./lib/hero-screen-registry.js",
|
|
@@ -34,5 +35,5 @@
|
|
|
34
35
|
},
|
|
35
36
|
"sideEffects": false,
|
|
36
37
|
"type": "module",
|
|
37
|
-
"version": "0.0.
|
|
38
|
+
"version": "0.0.15"
|
|
38
39
|
}
|