@opndev/react-native-events 0.0.10 → 0.0.12

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,42 @@
1
1
  Revision history for @opndev/opndev-react-native-events
2
2
 
3
+ 0.0.12 2026-06-25 23:24:45Z
4
+
5
+ * Add top bar to package
6
+ * Add more HeroScreen types
7
+
8
+ 0.0.11 2026-06-25 04:12:51Z
9
+
10
+ * registry not registery :/
11
+ * Add HeroScreen variants: Parallax, Fixed and Overlay. Overlay is the
12
+ default.
13
+ * Move consuming units to screen-registry, so consuming apps can just use:
14
+
15
+ import { createScreens } from
16
+ '@opndev/react-native-events/screen-registry';
17
+ import { ThemedText } from '@/components/themed-text';
18
+ import { styles } from '@/styles/tabs';
19
+
20
+ const CategoryText =
21
+ (props) => <ThemedText type="subtitle" {...props} />;
22
+
23
+ export const {
24
+ heroImage,
25
+ FoodMenuScreen,
26
+ FoodVendorScreen,
27
+ HeroScreen,
28
+ HeroScreenParallax,
29
+ HeroScreenFixed,
30
+ NewsItemScreen,
31
+ NewsListScreen,
32
+ QRCodeScreen,
33
+ Tile,
34
+ } = createScreens({
35
+ TextComponent: ThemedText,
36
+ CategoryTextComponent: CategoryText,
37
+ styles,
38
+ });
39
+
3
40
  0.0.10 2026-06-21 17:41:27Z
4
41
 
5
42
  * Rename @opndev/opndev-react-native-events to @opndev/react-native-events,
@@ -66,9 +66,6 @@ export default class QRCodeAction {
66
66
  ? this.buildPayload(values)
67
67
  : values;
68
68
 
69
- console.log(this.endpoint);
70
- console.log(payload);
71
-
72
69
  const res = await fetch(this.endpoint.url, {
73
70
  method: this.endpoint.method || 'POST',
74
71
  headers: {
@@ -0,0 +1,97 @@
1
+ // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
4
+ import { cloneElement, isValidElement } from 'react';
5
+ import { View, StyleSheet, useWindowDimensions } from 'react-native';
6
+
7
+ export const DEFAULT_TOP_BAR_HEIGHT = 56;
8
+ export const DEFAULT_MAX_WIDTH_PERCENT = 33;
9
+
10
+ const styles = StyleSheet.create({
11
+ bar: {
12
+ flexDirection: 'row',
13
+ alignItems: 'flex-end',
14
+ width: '100%',
15
+ },
16
+ });
17
+
18
+ /**
19
+ * Injects the bar's own height (unless the caller already set an
20
+ * explicit height or width) and a pixel maxWidth clamp into a logo
21
+ * element (e.g. ScaledLogo). The clamp is passed as a `maxWidth`
22
+ * PROP, not a style percentage — ScaledLogo re-derives height from
23
+ * the clamped width itself, so nothing ever letterboxes.
24
+ */
25
+ function applyBarStyling(element, { height, maxWidthPixels }) {
26
+ if (!isValidElement(element)) return element;
27
+
28
+ const props = {};
29
+
30
+ if (element.props.height == null && element.props.width == null) {
31
+ props.height = height;
32
+ }
33
+
34
+ if (maxWidthPixels != null && element.props.maxWidth == null) {
35
+ props.maxWidth = maxWidthPixels;
36
+ }
37
+
38
+ return cloneElement(element, props);
39
+ }
40
+
41
+ /**
42
+ * AppScreenTopBar
43
+ *
44
+ * Purely presentational — the logo / sponsor-logo row. It knows
45
+ * nothing about safe areas, positioning, or stacking order; all of
46
+ * that is owned by AppScreen, which renders this as its `topBar`.
47
+ *
48
+ * `logo` / `sponsorLogo` automatically receive a `height` matching
49
+ * the bar's own height, and a pixel `maxWidth` clamp (default 33%
50
+ * of the screen width) that ScaledLogo applies aspect-ratio-aware —
51
+ * so nothing to keep in sync manually, and no letterboxing if a
52
+ * logo would otherwise render too wide on a narrow screen. Pass an
53
+ * explicit `height` or `width` on the logo element yourself to opt
54
+ * out of the auto-sizing.
55
+ *
56
+ * With no `sponsorLogo`, `logo` is centered rather than left-aligned
57
+ * — there's nothing to space it apart from.
58
+ *
59
+ * @param {object} props
60
+ * @param {React.ReactNode} [props.logo]
61
+ * @param {React.ReactNode} [props.sponsorLogo]
62
+ * @param {number} [props.height] Defaults to DEFAULT_TOP_BAR_HEIGHT (56). If you override this, pass the same value to AppScreen's `topBarHeight` so the reserved offset still matches.
63
+ * @param {number} [props.maxWidthPercent] Defaults to 33 (% of screen width). Pass null to disable the clamp entirely.
64
+ * @param {number} [props.horizontalPadding] Space from the screen edges. Defaults to 24.
65
+ * @param {string} [props.backgroundColor]
66
+ * @param {object} [props.style]
67
+ *
68
+ * @returns {JSX.Element}
69
+ */
70
+ export default function AppScreenTopBar({
71
+ logo,
72
+ sponsorLogo,
73
+ height = DEFAULT_TOP_BAR_HEIGHT,
74
+ maxWidthPercent = DEFAULT_MAX_WIDTH_PERCENT,
75
+ horizontalPadding = 24,
76
+ verticalPadding = 8,
77
+ backgroundColor,
78
+ style,
79
+ }) {
80
+ const { width: windowWidth } = useWindowDimensions();
81
+ const maxWidthPixels = maxWidthPercent != null ? (windowWidth * maxWidthPercent) / 100 : null;
82
+ const justifyContent = sponsorLogo ? 'space-between' : 'center';
83
+
84
+ return (
85
+ <View
86
+ style={[
87
+ styles.bar,
88
+ { height, justifyContent, paddingHorizontal: horizontalPadding, paddingBottom: verticalPadding },
89
+ backgroundColor ? { backgroundColor } : null,
90
+ style,
91
+ ]}
92
+ >
93
+ {applyBarStyling(logo, { height, maxWidthPixels })}
94
+ {sponsorLogo ? applyBarStyling(sponsorLogo, { height, maxWidthPixels }) : null}
95
+ </View>
96
+ );
97
+ }
@@ -0,0 +1,78 @@
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 { View, StyleSheet } from 'react-native';
6
+ import { useSafeAreaTopInset } from '../hooks/use-safe-area-top-inset';
7
+ import { AppScreenOffsetContext } from '../hooks/use-app-screen-offset';
8
+ import { DEFAULT_TOP_BAR_HEIGHT } from './app-screen-top-bar';
9
+
10
+ const defaultStyle = StyleSheet.create({
11
+ container: {
12
+ flex: 1,
13
+ },
14
+ topBarWrap: {
15
+ position: 'absolute',
16
+ top: 0,
17
+ left: 0,
18
+ width: '100%',
19
+ zIndex: 10,
20
+ },
21
+ });
22
+
23
+ /**
24
+ * AppScreen
25
+ *
26
+ * The mandatory root wrapper for every screen in the app. Owns
27
+ * safe-area handling as a baseline — wrap every screen in this,
28
+ * even ones with no visible top bar. Optionally floats a `topBar`
29
+ * (e.g. AppScreenTopBar) above everything, and publishes the
30
+ * combined offset via AppScreenOffsetContext so any nested screen
31
+ * (HeroScreen, HeroScreenParallax, HeroScreenFixed,
32
+ * HeroScreenOverlay, Screen) automatically starts its own
33
+ * content/header below it — no per-screen wiring needed.
34
+ *
35
+ * In practice, you only need ONE AppScreen, at the root of your
36
+ * navigator (e.g. wrapping <Slot/> in app/_layout.tsx), rather
37
+ * than wrapping every individual screen file — context propagates
38
+ * to the whole tree underneath it.
39
+ *
40
+ * Screens rendered WITHOUT an AppScreen ancestor get an offset of
41
+ * 0 (no safe-area protection) — that's intentional now that
42
+ * AppScreen is the single owner of this concern, not a fallback
43
+ * gap. Make sure the root wrap is always in place.
44
+ *
45
+ * @param {object} props
46
+ * @param {React.ReactNode} props.children The screen tree (HeroScreen*, Screen, a navigator, etc.)
47
+ * @param {React.ReactNode} [props.topBar] e.g. <AppScreenTopBar .../>. Omit for no branding bar.
48
+ * @param {number} [props.topBarHeight] Visible height of `topBar`. Defaults to AppScreenTopBar's own default height (56) — only override if you also customized AppScreenTopBar's height.
49
+ *
50
+ * @returns {JSX.Element}
51
+ */
52
+ export default function AppScreen({
53
+ children,
54
+ topBar,
55
+ topBarHeight = DEFAULT_TOP_BAR_HEIGHT,
56
+ }) {
57
+ const insetTop = useSafeAreaTopInset();
58
+ const offset = topBar ? insetTop + topBarHeight : insetTop;
59
+
60
+ return (
61
+ <AppScreenOffsetContext.Provider value={offset}>
62
+ <View style={defaultStyle.container}>
63
+ {children}
64
+
65
+ {topBar ? (
66
+ <View
67
+ style={[
68
+ defaultStyle.topBarWrap,
69
+ { height: offset, paddingTop: insetTop },
70
+ ]}
71
+ >
72
+ {topBar}
73
+ </View>
74
+ ) : null}
75
+ </View>
76
+ </AppScreenOffsetContext.Provider>
77
+ );
78
+ }
@@ -0,0 +1,63 @@
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 { View, ScrollView, StyleSheet } from 'react-native';
6
+ import { useTopOffset } from './hero-screen-header';
7
+
8
+ const defaultStyle = StyleSheet.create({
9
+ container: {
10
+ flex: 1,
11
+ },
12
+ content: {
13
+ flex: 1,
14
+ },
15
+ });
16
+
17
+ /**
18
+ * BaseScreen
19
+ *
20
+ * Plain screen wrapper for content that doesn't have a hero header
21
+ * image (e.g. NewsListScreen, FoodMenuScreen). Encapsulates the
22
+ * same background / safe-area / scroll-container concerns as the
23
+ * HeroScreen variants, minus the header. Automatically starts
24
+ * content below an enclosing AppScreen's bar when present (via
25
+ * useTopOffset), same as the Hero variants do.
26
+ *
27
+ * @param {object} props
28
+ * @param {React.ReactNode} props.children
29
+ * @param {string} [props.backgroundColor]
30
+ * @param {React.ComponentType} [props.ContentComponent] Defaults to ScrollView; pass a list component (FlatList/SectionList) if children render as list items
31
+ * @param {object} [props.containerStyle]
32
+ * @param {object} [props.contentStyle]
33
+ * @param {boolean} [props.useSafeArea] Pad content by the top offset (safe-area inset, or AppScreen bar height if present). Defaults true.
34
+ *
35
+ * @returns {JSX.Element}
36
+ */
37
+ export default function BaseScreen({
38
+ children,
39
+ backgroundColor,
40
+ ContentComponent = ScrollView,
41
+ containerStyle,
42
+ contentStyle,
43
+ useSafeArea = true,
44
+ }) {
45
+ const topOffset = useTopOffset();
46
+
47
+ return (
48
+ <View
49
+ style={[
50
+ defaultStyle.container,
51
+ backgroundColor ? { backgroundColor } : null,
52
+ containerStyle,
53
+ ]}
54
+ >
55
+ <ContentComponent
56
+ style={[defaultStyle.content, contentStyle]}
57
+ contentContainerStyle={useSafeArea ? { paddingTop: topOffset } : null}
58
+ >
59
+ {children}
60
+ </ContentComponent>
61
+ </View>
62
+ );
63
+ }
@@ -0,0 +1,105 @@
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 { View, ScrollView, StyleSheet } from 'react-native';
6
+ import {
7
+ renderHeaderImage,
8
+ HeaderOverlay,
9
+ useHeroHeaderHeight,
10
+ heroContentStyle,
11
+ } from './hero-screen-header';
12
+
13
+ const defaultStyle = StyleSheet.create({
14
+ container: {
15
+ flex: 1,
16
+ },
17
+ headerWrap: {
18
+ width: '100%',
19
+ position: 'absolute',
20
+ top: 0,
21
+ left: 0,
22
+ },
23
+ imageInner: {
24
+ position: 'absolute',
25
+ left: 0,
26
+ width: '100%',
27
+ },
28
+ });
29
+
30
+ /**
31
+ * HeroScreenFixed
32
+ *
33
+ * Header image stays pinned in place; only the content below it
34
+ * scrolls. The image never overlaps the status bar — a solid
35
+ * headerBackgroundColor strip (sized to the safe-area top inset,
36
+ * via useHeroHeaderHeight) sits above it. Body content is wrapped
37
+ * the same way ParallaxScrollView wraps its children — padding: 32,
38
+ * gap: 16 — so spacing matches the parallax variant exactly.
39
+ *
40
+ * @param {object} props
41
+ * @param {React.ReactNode} props.children
42
+ * @param {object} [props.headerImage]
43
+ * @param {React.ReactNode} [props.headerOverlay]
44
+ * @param {string} [props.backgroundColor]
45
+ * @param {string} [props.headerBackgroundColor]
46
+ * @param {number} [props.headerHeight] Visible image height (excludes the safe-area inset)
47
+ * @param {React.ComponentType} [props.ContentComponent]
48
+ * @param {object} [props.containerStyle]
49
+ * @param {object} [props.headerStyle]
50
+ * @param {object} [props.contentStyle]
51
+ *
52
+ * @returns {JSX.Element}
53
+ */
54
+ export default function HeroScreenFixed({
55
+ children,
56
+ headerImage,
57
+ headerOverlay,
58
+ backgroundColor,
59
+ headerBackgroundColor,
60
+ headerHeight = 220,
61
+ ContentComponent = ScrollView,
62
+ containerStyle,
63
+ headerStyle,
64
+ contentStyle,
65
+ }) {
66
+ const { insetTop, totalHeaderHeight } = useHeroHeaderHeight(headerHeight);
67
+
68
+ return (
69
+ <View
70
+ style={[
71
+ defaultStyle.container,
72
+ backgroundColor ? { backgroundColor } : null,
73
+ containerStyle,
74
+ ]}
75
+ >
76
+ <View
77
+ style={[
78
+ defaultStyle.headerWrap,
79
+ { height: totalHeaderHeight },
80
+ headerBackgroundColor ? { backgroundColor: headerBackgroundColor } : null,
81
+ headerStyle,
82
+ ]}
83
+ >
84
+ <View
85
+ style={[
86
+ defaultStyle.imageInner,
87
+ { top: insetTop, height: headerHeight },
88
+ ]}
89
+ >
90
+ {renderHeaderImage(headerImage)}
91
+ </View>
92
+ </View>
93
+
94
+ <ContentComponent
95
+ style={{ flex: 1 }}
96
+ contentContainerStyle={{ paddingTop: totalHeaderHeight }}
97
+ >
98
+ <View style={[heroContentStyle, contentStyle]}>
99
+ <HeaderOverlay headerOverlay={headerOverlay} />
100
+ {children}
101
+ </View>
102
+ </ContentComponent>
103
+ </View>
104
+ );
105
+ }
@@ -0,0 +1,66 @@
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 { View, Image, StyleSheet } from 'react-native';
6
+ import { useAppScreenOffset } from '../hooks/use-app-screen-offset.js';
7
+
8
+ const styles = StyleSheet.create({
9
+ headerImage: { width: '100%', height: '100%' },
10
+ titleWrap: { paddingHorizontal: 8, paddingTop: 8, paddingBottom: 8 },
11
+ // Matches ParallaxScrollView's own `content` style — single source of
12
+ // truth so every Hero variant lines up on body padding/gap.
13
+ heroContent: { padding: 32, gap: 16 },
14
+ });
15
+
16
+ export function renderHeaderImage(headerImage) {
17
+ if (!headerImage) return null;
18
+ const fit = headerImage.fit || 'cover';
19
+ const style = headerImage.style || styles.headerImage;
20
+ return <Image source={headerImage.source} style={style} resizeMode={fit} />;
21
+ }
22
+
23
+ export function HeaderOverlay({ headerOverlay }) {
24
+ if (!headerOverlay) return null;
25
+ return <View style={styles.titleWrap}>{headerOverlay}</View>;
26
+ }
27
+
28
+ /**
29
+ * useTopOffset
30
+ *
31
+ * How much vertical space must be reserved at the very top of a
32
+ * screen before its own content begins. AppScreen is now the sole
33
+ * owner of this number — it's always the safe-area inset, plus a
34
+ * top bar's height if AppScreen has one. There's no fallback to a
35
+ * direct safe-area call here anymore: if a screen has no AppScreen
36
+ * ancestor, this is 0, by design.
37
+ *
38
+ * @returns {number}
39
+ */
40
+ export function useTopOffset() {
41
+ return useAppScreenOffset();
42
+ }
43
+
44
+ /**
45
+ * useHeroHeaderHeight
46
+ *
47
+ * Shared header-height calculation for every HeroScreen variant
48
+ * (Parallax, Fixed, Overlay), so the space above the header
49
+ * behaves identically across all three and automatically starts
50
+ * below an AppScreen's top bar when present.
51
+ *
52
+ * @param {number} [headerHeight] Visible image height, as passed by the caller (excludes the top offset)
53
+ * @returns {{ topOffset: number, headerHeight: number, totalHeaderHeight: number }}
54
+ */
55
+ export function useHeroHeaderHeight(headerHeight = 220) {
56
+ const topOffset = useTopOffset();
57
+
58
+ return {
59
+ topOffset,
60
+ headerHeight,
61
+ totalHeaderHeight: topOffset + headerHeight,
62
+ };
63
+ }
64
+
65
+ export { styles as heroImageStyles };
66
+ export const heroContentStyle = styles.heroContent;
@@ -0,0 +1,50 @@
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 { View, StyleSheet } from 'react-native';
6
+ import ParallaxScrollView from './parallax-scroll-view';
7
+ import { renderHeaderImage, HeaderOverlay } from './hero-screen-header';
8
+
9
+ const defaultStyle = StyleSheet.create({
10
+ headerImageWrap: {
11
+ width: '100%',
12
+ height: 220,
13
+ bottom: 0,
14
+ left: 0,
15
+ position: 'absolute',
16
+ },
17
+ });
18
+
19
+ export default function HeroScreen({
20
+ children,
21
+ headerImage,
22
+ headerOverlay,
23
+ backgroundColor,
24
+ headerBackgroundColor,
25
+ headerHeight,
26
+ ContentComponent,
27
+ containerStyle,
28
+ headerStyle,
29
+ contentStyle,
30
+ }) {
31
+ return (
32
+ <ParallaxScrollView
33
+ backgroundColor={backgroundColor}
34
+ headerBackgroundColor={headerBackgroundColor}
35
+ headerHeight={headerHeight}
36
+ ContentComponent={ContentComponent}
37
+ containerStyle={containerStyle}
38
+ headerStyle={headerStyle}
39
+ contentStyle={contentStyle}
40
+ headerImage={
41
+ <View style={defaultStyle.headerImageWrap}>
42
+ {renderHeaderImage(headerImage)}
43
+ </View>
44
+ }
45
+ >
46
+ <HeaderOverlay headerOverlay={headerOverlay} />
47
+ {children}
48
+ </ParallaxScrollView>
49
+ );
50
+ }
@@ -2,108 +2,111 @@
2
2
  //
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
4
4
 
5
- import { View, StyleSheet, Image } from 'react-native';
6
- import ParallaxScrollView from './parallax-scroll-view';
5
+ import { View, ScrollView, StyleSheet } from 'react-native';
6
+ import {
7
+ renderHeaderImage,
8
+ HeaderOverlay,
9
+ useHeroHeaderHeight,
10
+ heroContentStyle,
11
+ } from './hero-screen-header';
7
12
 
8
13
  const defaultStyle = StyleSheet.create({
9
- headerImageWrap: {
14
+ container: {
15
+ flex: 1,
16
+ },
17
+ headerWrap: {
10
18
  width: '100%',
11
- height: 220,
12
- bottom: 0,
13
- left: 0,
14
19
  position: 'absolute',
20
+ top: 0,
21
+ left: 0,
22
+ zIndex: 1,
15
23
  },
16
- headerImage: {
24
+ imageInner: {
25
+ position: 'absolute',
26
+ left: 0,
17
27
  width: '100%',
18
- height: '100%',
19
- },
20
- titleWrap: {
21
- paddingHorizontal: 8,
22
- paddingTop: 8,
23
- paddingBottom: 8,
24
28
  },
25
29
  });
26
30
 
27
31
  /**
28
- * HeroScreen
32
+ * HeroScreenOverlay
29
33
  *
30
- * Thin wrapper around ParallaxScrollView that:
31
- * - renders a full-bleed header image
32
- * - renders optional title/header content below the image
33
- * - renders children below that
34
+ * Header image stays pinned on top; content scrolls underneath it
35
+ * (visible content gets covered by the image as the user scrolls
36
+ * up). The image never overlaps the status bar — a solid
37
+ * headerBackgroundColor strip (sized to the safe-area top inset,
38
+ * via useHeroHeaderHeight) sits above it. Body content is wrapped
39
+ * the same way ParallaxScrollView wraps its children — padding: 32,
40
+ * gap: 8 — so spacing matches the parallax variant exactly. The
41
+ * spacer that reserves room for the header stays outside that
42
+ * padded wrapper, same as Parallax keeps its header outside its
43
+ * own padded content area.
34
44
  *
35
45
  * @param {object} props
36
46
  * @param {React.ReactNode} props.children
37
47
  * @param {object} [props.headerImage]
38
- * Header image config:
39
- * - source: React Native image source
40
- * - style: optional image style
41
- * - fit: optional resize mode, defaults to 'cover'
42
48
  * @param {React.ReactNode} [props.headerOverlay]
43
- * Optional content rendered below the header image.
44
49
  * @param {string} [props.backgroundColor]
45
50
  * @param {string} [props.headerBackgroundColor]
46
- * @param {number} [props.headerHeight]
47
- * @param {React.ComponentType<{style?: any, children?: React.ReactNode}>}
48
- * [props.ContentComponent]
51
+ * @param {number} [props.headerHeight] Visible image height (excludes the safe-area inset)
52
+ * @param {React.ComponentType} [props.ContentComponent]
49
53
  * @param {object} [props.containerStyle]
50
54
  * @param {object} [props.headerStyle]
51
55
  * @param {object} [props.contentStyle]
52
56
  *
53
57
  * @returns {JSX.Element}
54
58
  */
55
- export default function HeroScreen({
59
+ export default function HeroScreenOverlay({
56
60
  children,
57
61
  headerImage,
58
62
  headerOverlay,
59
-
60
63
  backgroundColor,
61
64
  headerBackgroundColor,
62
- headerHeight,
63
-
64
- ContentComponent,
65
+ headerHeight = 220,
66
+ ContentComponent = ScrollView,
65
67
  containerStyle,
66
68
  headerStyle,
67
69
  contentStyle,
68
70
  }) {
69
- let hi;
70
-
71
- if (headerImage) {
72
- const fit = headerImage.fit || 'cover';
73
- const style = headerImage.style || defaultStyle.headerImage;
74
-
75
- hi = (
76
- <Image
77
- source={headerImage.source}
78
- style={style}
79
- resizeMode={fit}
80
- />
81
- );
82
- }
71
+ const { topOffset, totalHeaderHeight } = useHeroHeaderHeight(headerHeight);
83
72
 
84
73
  return (
85
- <ParallaxScrollView
86
- backgroundColor={backgroundColor}
87
- headerBackgroundColor={headerBackgroundColor}
88
- headerHeight={headerHeight}
89
- ContentComponent={ContentComponent}
90
- containerStyle={containerStyle}
91
- headerStyle={headerStyle}
92
- contentStyle={contentStyle}
93
- headerImage={
94
- <View style={defaultStyle.headerImageWrap}>
95
- {hi}
96
- </View>
97
- }
74
+ <View
75
+ style={[
76
+ defaultStyle.container,
77
+ backgroundColor ? { backgroundColor } : null,
78
+ containerStyle,
79
+ ]}
98
80
  >
99
- {headerOverlay ? (
100
- <View style={defaultStyle.titleWrap}>
101
- {headerOverlay}
81
+ <ContentComponent
82
+ style={{ flex: 1 }}
83
+ contentContainerStyle={{ paddingTop: 0 }}
84
+ >
85
+ <View style={{ height: totalHeaderHeight }} />
86
+
87
+ <View style={[heroContentStyle, contentStyle]}>
88
+ <HeaderOverlay headerOverlay={headerOverlay} />
89
+ {children}
102
90
  </View>
103
- ) : null}
91
+ </ContentComponent>
104
92
 
105
- {children}
106
- </ParallaxScrollView>
93
+ <View
94
+ style={[
95
+ defaultStyle.headerWrap,
96
+ { height: totalHeaderHeight },
97
+ headerBackgroundColor ? { backgroundColor: headerBackgroundColor } : null,
98
+ headerStyle,
99
+ ]}
100
+ >
101
+ <View
102
+ style={[
103
+ defaultStyle.imageInner,
104
+ { top: topOffset, height: headerHeight },
105
+ ]}
106
+ >
107
+ {renderHeaderImage(headerImage)}
108
+ </View>
109
+ </View>
110
+ </View>
107
111
  );
108
112
  }
109
-
@@ -0,0 +1,142 @@
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 { StyleSheet, View } from 'react-native';
6
+ import Animated, {
7
+ interpolate,
8
+ useAnimatedRef,
9
+ useAnimatedStyle,
10
+ useScrollOffset,
11
+ } from 'react-native-reanimated';
12
+ import { useTopOffset, heroContentStyle } from './hero-screen-header';
13
+
14
+ const DEFAULT_HEADER_HEIGHT = 250;
15
+
16
+ const defaultStyle = StyleSheet.create({
17
+ container: {
18
+ flex: 1,
19
+ },
20
+ header: {
21
+ overflow: 'hidden',
22
+ zIndex: 1,
23
+ },
24
+ imageInner: {
25
+ position: 'absolute',
26
+ left: 0,
27
+ width: '100%',
28
+ },
29
+ });
30
+
31
+ /**
32
+ * A scroll view with a parallax header image and a pluggable content wrapper.
33
+ *
34
+ * The component is intentionally theme-agnostic. Consumers are expected to pass
35
+ * resolved colors and, if needed, a custom content wrapper component.
36
+ *
37
+ * The header reserves space above the image via useTopOffset() — the
38
+ * safe-area inset, plus an AppScreen top bar's height if one is
39
+ * present — so the image itself always renders at exactly
40
+ * `headerHeight`, with `headerBackgroundColor` as backdrop behind
41
+ * whatever's reserved above it. Same contract as HeroScreenFixed /
42
+ * HeroScreenOverlay.
43
+ *
44
+ * @param {object} props
45
+ * @param {React.ReactNode} props.children
46
+ * Content rendered below the parallax header.
47
+ * @param {React.ReactElement} props.headerImage
48
+ * Element rendered inside the header area, usually an image.
49
+ * @param {string} [props.backgroundColor='#fff']
50
+ * Background color for the scroll view container.
51
+ * @param {string} [props.headerBackgroundColor]
52
+ * Background color shown behind the header image.
53
+ * @param {number} [props.headerHeight=250]
54
+ * Visible height of the parallax header image (excludes the top offset).
55
+ * @param {React.ComponentType<{style?: any, children?: React.ReactNode}>}
56
+ * [props.ContentComponent=View]
57
+ * Wrapper used for the content area below the header.
58
+ * @param {import('react-native').StyleProp<import('react-native').ViewStyle>}
59
+ * [props.containerStyle]
60
+ * Optional style overrides for the outer scroll view container.
61
+ * @param {import('react-native').StyleProp<import('react-native').ViewStyle>}
62
+ * [props.headerStyle]
63
+ * Optional style overrides for the header container.
64
+ * @param {import('react-native').StyleProp<import('react-native').ViewStyle>}
65
+ * [props.contentStyle]
66
+ * Optional style overrides for the content wrapper.
67
+ * @returns {JSX.Element}
68
+ */
69
+ export default function ParallaxScrollView({
70
+ children,
71
+ headerImage,
72
+ headerBackgroundColor,
73
+ backgroundColor = '#fff',
74
+ colorScheme = 'light',
75
+ headerHeight = DEFAULT_HEADER_HEIGHT,
76
+ containerStyle,
77
+ headerStyle,
78
+ contentStyle,
79
+ ContentComponent = View,
80
+ }) {
81
+ const scrollRef = useAnimatedRef();
82
+ const scrollOffset = useScrollOffset(scrollRef);
83
+ const topOffset = useTopOffset();
84
+ const effectiveHeaderHeight = headerHeight + topOffset;
85
+
86
+ const headerAnimatedStyle = useAnimatedStyle(() => {
87
+ return {
88
+ transform: [
89
+ {
90
+ translateY: interpolate(
91
+ scrollOffset.value,
92
+ [-headerHeight, 0, headerHeight],
93
+ [-headerHeight / 2, 0, headerHeight * 0.75]
94
+ ),
95
+ },
96
+ {
97
+ scale: interpolate(
98
+ scrollOffset.value,
99
+ [-headerHeight, 0, headerHeight],
100
+ [2, 1, 1]
101
+ ),
102
+ },
103
+ ],
104
+ };
105
+ });
106
+
107
+ return (
108
+ <Animated.ScrollView
109
+ ref={scrollRef}
110
+ style={[
111
+ defaultStyle.container,
112
+ { backgroundColor },
113
+ containerStyle,
114
+ ]}
115
+ scrollEventThrottle={16}
116
+ >
117
+ <Animated.View
118
+ style={[
119
+ defaultStyle.header,
120
+ {
121
+ height: effectiveHeaderHeight,
122
+ backgroundColor: headerBackgroundColor,
123
+ },
124
+ headerAnimatedStyle,
125
+ headerStyle,
126
+ ]}
127
+ >
128
+ <View
129
+ style={[
130
+ defaultStyle.imageInner,
131
+ { top: topOffset, height: headerHeight },
132
+ ]}
133
+ >
134
+ {headerImage}
135
+ </View>
136
+ </Animated.View>
137
+ <ContentComponent style={[heroContentStyle, contentStyle]}>
138
+ {children}
139
+ </ContentComponent>
140
+ </Animated.ScrollView>
141
+ );
142
+ }
@@ -24,8 +24,8 @@ const defaultStyle = StyleSheet.create({
24
24
  zIndex: 1,
25
25
  },
26
26
  content: {
27
- padding: 32,
28
- gap: 16,
27
+ padding: 16,
28
+ gap: 8,
29
29
  },
30
30
  });
31
31
 
@@ -179,6 +179,7 @@ export default function QRCodeForm({
179
179
  const responseError = getResponseError(response);
180
180
  const visibleError = error || responseError;
181
181
  const hasResult = Boolean(response && !responseError);
182
+ const showResponseErrorCard = Boolean(response && responseError);
182
183
  const token = hasResult ? action.getToken(response) : undefined;
183
184
 
184
185
  const renderArgs = {
@@ -297,7 +298,7 @@ export default function QRCodeForm({
297
298
  * @returns {JSX.Element|null}
298
299
  */
299
300
  function renderResult() {
300
- if (!hasResult) {
301
+ if (!hasResult && !showResponseErrorCard) {
301
302
  return null;
302
303
  }
303
304
 
@@ -312,10 +313,10 @@ export default function QRCodeForm({
312
313
  ]}
313
314
  >
314
315
  {renderAboveQRCode ? renderAboveQRCode(renderArgs) : null}
315
- {renderQRCode()}
316
- {renderBelowQRCode ? renderBelowQRCode(renderArgs) : null}
317
- {renderRefreshButton()}
318
- {renderResetLink()}
316
+ {hasResult ? renderQRCode() : null}
317
+ {hasResult && renderBelowQRCode ? renderBelowQRCode(renderArgs) : null}
318
+ {hasResult ? renderRefreshButton() : null}
319
+ {hasResult ? renderResetLink() : renderResetButton()}
319
320
  </View>
320
321
  );
321
322
  }
@@ -390,10 +391,41 @@ export default function QRCodeForm({
390
391
  }
391
392
 
392
393
  /**
394
+ * Button version of reset, used in place of the link when showing
395
+ * the error-state card (response.error), so it matches the
396
+ * success card's button-style affordance instead of a text link.
397
+ *
398
+ * @returns {JSX.Element}
399
+ */
400
+ function renderResetButton() {
401
+ return (
402
+ <Pressable
403
+ style={[
404
+ defaultStyle.button,
405
+ defaultStyle.wideButton,
406
+ secondaryButtonBackgroundColor
407
+ ? { backgroundColor: secondaryButtonBackgroundColor }
408
+ : null,
409
+ secondaryButtonStyle,
410
+ ]}
411
+ onPress={reset}
412
+ >
413
+ <TextComponent style={secondaryButtonTextStyle}>
414
+ {resetLabel}
415
+ </TextComponent>
416
+ </Pressable>
417
+ );
418
+ }
419
+
420
+ /**
421
+ * Only handles the network/throw-style error (from the catch block
422
+ * in fetchQr). Response-level errors (response.error) are now
423
+ * rendered inside renderResult() as a styled card instead.
424
+ *
393
425
  * @returns {JSX.Element|null}
394
426
  */
395
427
  function renderError() {
396
- if (!visibleError) {
428
+ if (!error) {
397
429
  return null;
398
430
  }
399
431
 
@@ -412,10 +444,8 @@ export default function QRCodeForm({
412
444
  errorTextStyle,
413
445
  ]}
414
446
  >
415
- {visibleError}
447
+ {error}
416
448
  </TextComponent>
417
-
418
- {responseError ? renderResetLink() : null}
419
449
  </View>
420
450
  );
421
451
  }
@@ -0,0 +1,82 @@
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 { Image } from 'react-native';
6
+
7
+ /**
8
+ * ScaledLogo
9
+ *
10
+ * Fits a logo image to a fixed height OR width while preserving its
11
+ * real aspect ratio — pass whichever dimension you actually care
12
+ * about, and the other is computed automatically. If both are
13
+ * given, both are used as-is (no aspect-ratio computation).
14
+ *
15
+ * `maxWidth` (in pixels, not a style percentage) is applied AFTER
16
+ * the natural size is computed, and — critically — height is
17
+ * re-derived from the clamped width so the box always matches the
18
+ * aspect ratio. A style-based `maxWidth: '33%'` clamp can't do this:
19
+ * it shrinks width but leaves height untouched, which (with
20
+ * resizeMode="contain") leaves the visible artwork letterboxed
21
+ * inside a too-tall box. Passing `maxWidth` as a prop here avoids
22
+ * that entirely.
23
+ *
24
+ * Deliberately computes explicit width/height rather than relying
25
+ * on the `aspectRatio` style property — Image has special intrinsic
26
+ * sizing behaviour that can override `aspectRatio` and fall back to
27
+ * the image's native pixel size when only one explicit dimension is
28
+ * given via style. Providing both width and height directly
29
+ * sidesteps that entirely.
30
+ *
31
+ * Works out-of-the-box for local assets (require('...')), since RN
32
+ * can read their real dimensions synchronously via
33
+ * Image.resolveAssetSource. For remote ({ uri: ... }) sources, pass
34
+ * an explicit `aspectRatio` instead — those dimensions aren't known
35
+ * until the image actually loads.
36
+ *
37
+ * @param {object} props
38
+ * @param {any} props.source Image source (require(...) or { uri }).
39
+ * @param {number} [props.height] Computed from `width` + aspect ratio if omitted.
40
+ * @param {number} [props.width] Computed from `height` + aspect ratio if omitted. If neither height nor width is given, defaults to height=32.
41
+ * @param {number} [props.maxWidth] Pixel clamp applied after natural sizing; height is re-derived from the clamped width, never letterboxed.
42
+ * @param {number} [props.aspectRatio] Required for remote ({ uri }) sources; ignored for local assets, where it's read automatically.
43
+ * @param {object} [props.style]
44
+ *
45
+ * @returns {JSX.Element}
46
+ */
47
+ export default function ScaledLogo({ source, height, width, maxWidth, aspectRatio, style }) {
48
+ let resolvedAspectRatio = aspectRatio;
49
+
50
+ if (typeof source === 'number') {
51
+ // Local asset — RN can resolve real dimensions synchronously.
52
+ const { width: naturalWidth, height: naturalHeight } = Image.resolveAssetSource(source);
53
+ resolvedAspectRatio = naturalWidth / naturalHeight;
54
+ }
55
+
56
+ let finalWidth = width;
57
+ let finalHeight = height;
58
+
59
+ if (finalWidth != null && finalHeight == null) {
60
+ finalHeight = resolvedAspectRatio ? finalWidth / resolvedAspectRatio : undefined;
61
+ } else if (finalHeight != null && finalWidth == null) {
62
+ finalWidth = resolvedAspectRatio ? finalHeight * resolvedAspectRatio : undefined;
63
+ } else if (finalWidth == null && finalHeight == null) {
64
+ finalHeight = 32;
65
+ finalWidth = resolvedAspectRatio ? finalHeight * resolvedAspectRatio : undefined;
66
+ }
67
+ // If both width and height were explicitly given, use both as-is.
68
+
69
+ // Clamp width if needed, then re-derive height so nothing letterboxes.
70
+ if (maxWidth != null && finalWidth != null && finalWidth > maxWidth) {
71
+ finalWidth = maxWidth;
72
+ finalHeight = resolvedAspectRatio ? finalWidth / resolvedAspectRatio : finalHeight;
73
+ }
74
+
75
+ return (
76
+ <Image
77
+ source={source}
78
+ resizeMode="contain"
79
+ style={[{ height: finalHeight, width: finalWidth }, style]}
80
+ />
81
+ );
82
+ }
package/lib/components.js CHANGED
@@ -10,6 +10,7 @@ export { default as TileBase } from './components/tile-base.jsx';
10
10
  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
+ export { default as ScaledLogo } from './components/scaled-logo.jsx';
13
14
 
14
15
  export { openUrl, openApp, openExternal } from './utils/launch.js';
15
16
 
@@ -0,0 +1,24 @@
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 { createContext, useContext } from 'react';
6
+
7
+ /**
8
+ * AppScreenOffsetContext
9
+ *
10
+ * Provided by AppScreen with the total height of its persistent
11
+ * top bar (safe-area inset + visible bar height). Defaults to 0,
12
+ * so any screen rendered WITHOUT an AppScreen ancestor behaves
13
+ * exactly as before — this is additive, not a breaking change.
14
+ */
15
+ export const AppScreenOffsetContext = createContext(0);
16
+
17
+ /**
18
+ * useAppScreenOffset
19
+ *
20
+ * @returns {number} Height of the enclosing AppScreen's bar, or 0 if none.
21
+ */
22
+ export function useAppScreenOffset() {
23
+ return useContext(AppScreenOffsetContext);
24
+ }
@@ -0,0 +1,20 @@
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 { useSafeAreaInsets } from 'react-native-safe-area-context';
6
+
7
+ /**
8
+ * useSafeAreaTopInset
9
+ *
10
+ * Single source of truth for "how much space does the status bar /
11
+ * notch take up". Used by both HeroScreen variants (via
12
+ * useHeroHeaderHeight) and plain Screen, so every screen in the
13
+ * package agrees on the same number.
14
+ *
15
+ * @returns {number}
16
+ */
17
+ export function useSafeAreaTopInset() {
18
+ const insets = useSafeAreaInsets();
19
+ return insets.top;
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.10";
5
+ const VERSION = "0.0.12";
6
6
 
7
7
  // TODO: @opndev/util?
8
8
  export { formatPrice } from './utils/format-price.js';
@@ -0,0 +1,129 @@
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
+
7
+ import HeroScreen from './components/hero-screen';
8
+ import HeroScreenParallax from './components/hero-screen-parallax';
9
+ import HeroScreenFixed from './components/hero-screen-fixed';
10
+ import BaseScreen from './components/base-screen';
11
+ import AppScreen from './components/app-screen';
12
+ import AppScreenTopBar from './components/app-screen-top-bar';
13
+ import Tile from './components/tile';
14
+
15
+ import FoodMenuScreen from './screens/food-menu-screen';
16
+ import FoodVendorScreen from './screens/food-vendor-screen';
17
+ import NewsItemScreen from './screens/news-item-screen';
18
+ import NewsListScreen from './screens/news-list-screen';
19
+ import QRCodeScreen from './screens/qr-code-screen';
20
+
21
+ /**
22
+ * createScreens
23
+ *
24
+ * @param {object} [options]
25
+ * @param {React.ComponentType} [options.TextComponent]
26
+ * @param {React.ComponentType} [options.CategoryTextComponent]
27
+ * @param {object} [options.styles]
28
+ * App-provided style object. Used as the default source for
29
+ * heroImage() when a caller doesn't pass an explicit style/fit.
30
+ * Expected (optional) keys: heroImage, heroImageFit.
31
+ *
32
+ * @returns {object} Named screens + helpers, ready to re-export.
33
+ */
34
+ export function createScreens({
35
+ TextComponent,
36
+ CategoryTextComponent,
37
+ styles = {},
38
+ } = {}) {
39
+ /**
40
+ * heroImage
41
+ *
42
+ * @param {any} source
43
+ * @param {object} [options]
44
+ * @param {object} [options.style]
45
+ * @param {string} [options.fit] // 'cover', 'contain', etc
46
+ */
47
+ function heroImage(source, options = {}) {
48
+ return {
49
+ source,
50
+ style: options.style || styles.heroImage,
51
+ fit: options.fit || styles.heroImageFit,
52
+ };
53
+ }
54
+
55
+ return {
56
+ heroImage,
57
+
58
+ HeroScreen: (props) => (
59
+ <HeroScreen {...props} />
60
+ ),
61
+
62
+ HeroScreenParallax: (props) => (
63
+ <HeroScreenParallax {...props} />
64
+ ),
65
+
66
+ HeroScreenFixed: (props) => (
67
+ <HeroScreenFixed {...props} />
68
+ ),
69
+
70
+ HeroScreenOverlay: (props) => (
71
+ <HeroScreenOverlay {...props} />
72
+ ),
73
+
74
+ Screen: (props) => (
75
+ <Screen {...props} />
76
+ ),
77
+
78
+ AppScreen: (props) => (
79
+ <AppScreen {...props} />
80
+ ),
81
+
82
+ AppScreenTopBar: (props) => (
83
+ <AppScreenTopBar {...props} />
84
+ ),
85
+
86
+ FoodMenuScreen: (props) => (
87
+ <FoodMenuScreen
88
+ {...props}
89
+ TextComponent={TextComponent}
90
+ CategoryTextComponent={CategoryTextComponent}
91
+ />
92
+ ),
93
+
94
+ FoodVendorScreen: (props) => (
95
+ <FoodVendorScreen
96
+ {...props}
97
+ TextComponent={TextComponent}
98
+ />
99
+ ),
100
+
101
+ QRCodeScreen: (props) => (
102
+ <QRCodeScreen
103
+ {...props}
104
+ TextComponent={TextComponent}
105
+ />
106
+ ),
107
+
108
+ NewsListScreen: (props) => (
109
+ <NewsListScreen
110
+ {...props}
111
+ TextComponent={TextComponent}
112
+ />
113
+ ),
114
+
115
+ NewsItemScreen: (props) => (
116
+ <NewsItemScreen
117
+ {...props}
118
+ TextComponent={TextComponent}
119
+ />
120
+ ),
121
+
122
+ Tile: (props) => (
123
+ <Tile
124
+ {...props}
125
+ TextComponent={TextComponent}
126
+ />
127
+ ),
128
+ };
129
+ }
package/package.json CHANGED
@@ -11,12 +11,13 @@
11
11
  "exports": {
12
12
  ".": "./lib/index.js",
13
13
  "./components": "./lib/components.js",
14
- "./globals": "./lib/screen-registery.js",
15
- "./hero-screen-registry": "./lib/hero-screen-registery.js",
16
- "./lite": "./lib/hero-screen-registery.js",
14
+ "./globals": "./lib/screen-registry.js",
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
+ "./lite": "./lib/hero-screen-registry.js",
17
18
  "./notifications": "./lib/notifications.js",
18
19
  "./notifications-fcm": "./lib/notifications/fcm.js",
19
- "./screen-registery": "./lib/screen-registery.js",
20
+ "./screen-registry": "./lib/screen-registry.js",
20
21
  "./screens": "./lib/screens.js"
21
22
  },
22
23
  "keywords": [],
@@ -33,5 +34,5 @@
33
34
  },
34
35
  "sideEffects": false,
35
36
  "type": "module",
36
- "version": "0.0.10"
37
+ "version": "0.0.12"
37
38
  }
@@ -1,68 +0,0 @@
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
-
7
- import HeroScreen from './components/hero-screen';
8
- import Tile from './components/tile';
9
-
10
- import FoodMenuScreen from './screens/food-menu-screen';
11
- import FoodVendorScreen from './screens/food-vendor-screen';
12
- import NewsItemScreen from './screens/news-item-screen';
13
- import NewsListScreen from './screens/news-list-screen';
14
- import QRCodeScreen from './screens/qr-code-screen';
15
-
16
- export function createScreens({
17
- TextComponent,
18
- CategoryTextComponent,
19
- } = {}) {
20
- return {
21
- HeroScreen: (props) => (
22
- <HeroScreen {...props} />
23
- ),
24
-
25
- FoodMenuScreen: (props) => (
26
- <FoodMenuScreen
27
- {...props}
28
- TextComponent={TextComponent}
29
- CategoryTextComponent={CategoryTextComponent}
30
- />
31
- ),
32
-
33
- FoodVendorScreen: (props) => (
34
- <FoodVendorScreen
35
- {...props}
36
- TextComponent={TextComponent}
37
- />
38
- ),
39
-
40
- QRCodeScreen: (props) => (
41
- <QRCodeScreen
42
- {...props}
43
- TextComponent={TextComponent}
44
- />
45
- ),
46
-
47
- NewsListScreen: (props) => (
48
- <NewsListScreen
49
- {...props}
50
- TextComponent={TextComponent}
51
- />
52
- ),
53
-
54
- NewsItemScreen: (props) => (
55
- <NewsItemScreen
56
- {...props}
57
- TextComponent={TextComponent}
58
- />
59
- ),
60
-
61
- Tile: (props) => (
62
- <Tile
63
- {...props}
64
- TextComponent={TextComponent}
65
- />
66
- ),
67
- };
68
- }