@opndev/react-native-events 0.0.10 → 0.0.11

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,37 @@
1
1
  Revision history for @opndev/opndev-react-native-events
2
2
 
3
+ 0.0.11 2026-06-25 04:12:51Z
4
+
5
+ * registry not registery :/
6
+ * Add HeroScreen variants: Parallax, Fixed and Overlay. Overlay is the
7
+ default.
8
+ * Move consuming units to screen-registry, so consuming apps can just use:
9
+
10
+ import { createScreens } from
11
+ '@opndev/react-native-events/screen-registry';
12
+ import { ThemedText } from '@/components/themed-text';
13
+ import { styles } from '@/styles/tabs';
14
+
15
+ const CategoryText =
16
+ (props) => <ThemedText type="subtitle" {...props} />;
17
+
18
+ export const {
19
+ heroImage,
20
+ FoodMenuScreen,
21
+ FoodVendorScreen,
22
+ HeroScreen,
23
+ HeroScreenParallax,
24
+ HeroScreenFixed,
25
+ NewsItemScreen,
26
+ NewsListScreen,
27
+ QRCodeScreen,
28
+ Tile,
29
+ } = createScreens({
30
+ TextComponent: ThemedText,
31
+ CategoryTextComponent: CategoryText,
32
+ styles,
33
+ });
34
+
3
35
  0.0.10 2026-06-21 17:41:27Z
4
36
 
5
37
  * Rename @opndev/opndev-react-native-events to @opndev/react-native-events,
@@ -0,0 +1,56 @@
1
+ import { View, ScrollView, StyleSheet } from 'react-native';
2
+ import { useSafeAreaTopInset } from '../hooks/use-safe-area-top-inset.js';
3
+
4
+ const defaultStyle = StyleSheet.create({
5
+ container: {
6
+ flex: 1,
7
+ },
8
+ });
9
+
10
+ /**
11
+ * BaseScreen
12
+ *
13
+ * Plain screen wrapper for content that doesn't have a hero header
14
+ * image (e.g. NewsListScreen, FoodMenuScreen). Encapsulates the
15
+ * same background / safe-area / scroll-container concerns as the
16
+ * HeroScreen variants, minus the header, so non-hero screens still
17
+ * get consistent top-inset behaviour rather than each handling it
18
+ * (or forgetting to handle it) separately.
19
+ *
20
+ * @param {object} props
21
+ * @param {React.ReactNode} props.children
22
+ * @param {string} [props.backgroundColor]
23
+ * @param {React.ComponentType} [props.ContentComponent] Defaults to ScrollView; pass a list component (FlatList/SectionList) if children render as list items
24
+ * @param {object} [props.containerStyle]
25
+ * @param {object} [props.contentStyle]
26
+ * @param {boolean} [props.useSafeArea] Pad content by the top safe-area inset. Defaults true.
27
+ *
28
+ * @returns {JSX.Element}
29
+ */
30
+ export default function BaseScreen({
31
+ children,
32
+ backgroundColor,
33
+ ContentComponent = ScrollView,
34
+ containerStyle,
35
+ contentStyle,
36
+ useSafeArea = true,
37
+ }) {
38
+ const insetTop = useSafeAreaTopInset();
39
+
40
+ return (
41
+ <View
42
+ style={[
43
+ defaultStyle.container,
44
+ backgroundColor ? { backgroundColor } : null,
45
+ containerStyle,
46
+ ]}
47
+ >
48
+ <ContentComponent
49
+ style={[contentStyle]}
50
+ contentContainerStyle={useSafeArea ? { paddingTop: insetTop } : null}
51
+ >
52
+ {children}
53
+ </ContentComponent>
54
+ </View>
55
+ );
56
+ }
@@ -0,0 +1,96 @@
1
+ import { View, ScrollView, StyleSheet } from 'react-native';
2
+ import {
3
+ renderHeaderImage,
4
+ HeaderOverlay,
5
+ useHeroHeaderHeight
6
+ } from './hero-screen-header';
7
+
8
+ const defaultStyle = StyleSheet.create({
9
+ container: {
10
+ flex: 1,
11
+ },
12
+ headerWrap: {
13
+ width: '100%',
14
+ position: 'absolute',
15
+ top: 0,
16
+ left: 0,
17
+ },
18
+ imageInner: {
19
+ position: 'absolute',
20
+ left: 0,
21
+ width: '100%',
22
+ },
23
+ });
24
+
25
+ /**
26
+ * HeroScreenFixed
27
+ *
28
+ * Header image stays pinned in place; only the content below it
29
+ * scrolls. The image never overlaps the status bar — a solid
30
+ * headerBackgroundColor strip (sized to the safe-area top inset,
31
+ * via useHeroHeaderHeight) sits above it.
32
+ *
33
+ * @param {object} props
34
+ * @param {React.ReactNode} props.children
35
+ * @param {object} [props.headerImage]
36
+ * @param {React.ReactNode} [props.headerOverlay]
37
+ * @param {string} [props.backgroundColor]
38
+ * @param {string} [props.headerBackgroundColor]
39
+ * @param {number} [props.headerHeight] Visible image height (excludes the safe-area inset)
40
+ * @param {React.ComponentType} [props.ContentComponent]
41
+ * @param {object} [props.containerStyle]
42
+ * @param {object} [props.headerStyle]
43
+ * @param {object} [props.contentStyle]
44
+ *
45
+ * @returns {JSX.Element}
46
+ */
47
+ export default function HeroScreenFixed({
48
+ children,
49
+ headerImage,
50
+ headerOverlay,
51
+ backgroundColor,
52
+ headerBackgroundColor,
53
+ headerHeight = 220,
54
+ ContentComponent = ScrollView,
55
+ containerStyle,
56
+ headerStyle,
57
+ contentStyle,
58
+ }) {
59
+ const { insetTop, totalHeaderHeight } = useHeroHeaderHeight(headerHeight);
60
+
61
+ return (
62
+ <View
63
+ style={[
64
+ defaultStyle.container,
65
+ backgroundColor ? { backgroundColor } : null,
66
+ containerStyle,
67
+ ]}
68
+ >
69
+ <View
70
+ style={[
71
+ defaultStyle.headerWrap,
72
+ { height: totalHeaderHeight },
73
+ headerBackgroundColor ? { backgroundColor: headerBackgroundColor } : null,
74
+ headerStyle,
75
+ ]}
76
+ >
77
+ <View
78
+ style={[
79
+ defaultStyle.imageInner,
80
+ { top: insetTop, height: headerHeight },
81
+ ]}
82
+ >
83
+ {renderHeaderImage(headerImage)}
84
+ </View>
85
+ </View>
86
+
87
+ <ContentComponent
88
+ style={[contentStyle]}
89
+ contentContainerStyle={{ paddingTop: totalHeaderHeight }}
90
+ >
91
+ <HeaderOverlay headerOverlay={headerOverlay} />
92
+ {children}
93
+ </ContentComponent>
94
+ </View>
95
+ );
96
+ }
@@ -0,0 +1,41 @@
1
+ import { View, Image, StyleSheet } from 'react-native';
2
+ import { useSafeAreaTopInset } from '../hooks/use-safe-area-top-inset.js';
3
+
4
+ const styles = StyleSheet.create({
5
+ headerImage: { width: '100%', height: '100%' },
6
+ titleWrap: { paddingHorizontal: 8, paddingTop: 8, paddingBottom: 8 },
7
+ });
8
+
9
+ export function renderHeaderImage(headerImage) {
10
+ if (!headerImage) return null;
11
+ const fit = headerImage.fit || 'cover';
12
+ const style = headerImage.style || styles.headerImage;
13
+ return <Image source={headerImage.source} style={style} resizeMode={fit} />;
14
+ }
15
+
16
+ export function HeaderOverlay({ headerOverlay }) {
17
+ if (!headerOverlay) return null;
18
+ return <View style={styles.titleWrap}>{headerOverlay}</View>;
19
+ }
20
+
21
+ /**
22
+ * useHeroHeaderHeight
23
+ *
24
+ * Shared safe-area calculation for all HeroScreen variants, so the
25
+ * "space above the header" behaves identically across Parallax,
26
+ * Fixed, and Overlay rather than each computing it separately.
27
+ *
28
+ * @param {number} [headerHeight] Visible image height, as passed by the caller (excludes the inset)
29
+ * @returns {{ insetTop: number, headerHeight: number, totalHeaderHeight: number }}
30
+ */
31
+ export function useHeroHeaderHeight(headerHeight = 220) {
32
+ const insetTop = useSafeAreaTopInset();
33
+
34
+ return {
35
+ insetTop,
36
+ headerHeight,
37
+ totalHeaderHeight: insetTop + headerHeight,
38
+ };
39
+ }
40
+
41
+ export { styles as heroImageStyles };
@@ -0,0 +1,46 @@
1
+ import { View, StyleSheet } from 'react-native';
2
+ import ParallaxScrollView from './parallax-scroll-view';
3
+ import { renderHeaderImage, HeaderOverlay } from './hero-screen-header';
4
+
5
+ const defaultStyle = StyleSheet.create({
6
+ headerImageWrap: {
7
+ width: '100%',
8
+ height: 220,
9
+ bottom: 0,
10
+ left: 0,
11
+ position: 'absolute',
12
+ },
13
+ });
14
+
15
+ export default function HeroScreen({
16
+ children,
17
+ headerImage,
18
+ headerOverlay,
19
+ backgroundColor,
20
+ headerBackgroundColor,
21
+ headerHeight,
22
+ ContentComponent,
23
+ containerStyle,
24
+ headerStyle,
25
+ contentStyle,
26
+ }) {
27
+ return (
28
+ <ParallaxScrollView
29
+ backgroundColor={backgroundColor}
30
+ headerBackgroundColor={headerBackgroundColor}
31
+ headerHeight={headerHeight}
32
+ ContentComponent={ContentComponent}
33
+ containerStyle={containerStyle}
34
+ headerStyle={headerStyle}
35
+ contentStyle={contentStyle}
36
+ headerImage={
37
+ <View style={defaultStyle.headerImageWrap}>
38
+ {renderHeaderImage(headerImage)}
39
+ </View>
40
+ }
41
+ >
42
+ <HeaderOverlay headerOverlay={headerOverlay} />
43
+ {children}
44
+ </ParallaxScrollView>
45
+ );
46
+ }
@@ -2,108 +2,102 @@
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
+ } from './hero-screen-header';
7
11
 
8
12
  const defaultStyle = StyleSheet.create({
9
- headerImageWrap: {
13
+ container: {
14
+ flex: 1,
15
+ },
16
+ headerWrap: {
10
17
  width: '100%',
11
- height: 220,
12
- bottom: 0,
13
- left: 0,
14
18
  position: 'absolute',
19
+ top: 0,
20
+ left: 0,
21
+ zIndex: 1,
15
22
  },
16
- headerImage: {
23
+ imageInner: {
24
+ position: 'absolute',
25
+ left: 0,
17
26
  width: '100%',
18
- height: '100%',
19
- },
20
- titleWrap: {
21
- paddingHorizontal: 8,
22
- paddingTop: 8,
23
- paddingBottom: 8,
24
27
  },
25
28
  });
26
29
 
27
30
  /**
28
- * HeroScreen
31
+ * HeroScreenOverlay
29
32
  *
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
33
+ * Header image stays pinned on top; content scrolls underneath it
34
+ * (visible content gets covered by the image as the user scrolls
35
+ * up). 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.
34
38
  *
35
39
  * @param {object} props
36
40
  * @param {React.ReactNode} props.children
37
41
  * @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
42
  * @param {React.ReactNode} [props.headerOverlay]
43
- * Optional content rendered below the header image.
44
43
  * @param {string} [props.backgroundColor]
45
44
  * @param {string} [props.headerBackgroundColor]
46
- * @param {number} [props.headerHeight]
47
- * @param {React.ComponentType<{style?: any, children?: React.ReactNode}>}
48
- * [props.ContentComponent]
45
+ * @param {number} [props.headerHeight] Visible image height (excludes the safe-area inset)
46
+ * @param {React.ComponentType} [props.ContentComponent]
49
47
  * @param {object} [props.containerStyle]
50
48
  * @param {object} [props.headerStyle]
51
49
  * @param {object} [props.contentStyle]
52
50
  *
53
51
  * @returns {JSX.Element}
54
52
  */
55
- export default function HeroScreen({
53
+ export default function HeroScreenOverlay({
56
54
  children,
57
55
  headerImage,
58
56
  headerOverlay,
59
-
60
57
  backgroundColor,
61
58
  headerBackgroundColor,
62
- headerHeight,
63
-
64
- ContentComponent,
59
+ headerHeight = 220,
60
+ ContentComponent = ScrollView,
65
61
  containerStyle,
66
62
  headerStyle,
67
63
  contentStyle,
68
64
  }) {
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
- }
65
+ const { insetTop, totalHeaderHeight } = useHeroHeaderHeight(headerHeight);
83
66
 
84
67
  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
- }
68
+ <View
69
+ style={[
70
+ defaultStyle.container,
71
+ backgroundColor ? { backgroundColor } : null,
72
+ containerStyle,
73
+ ]}
98
74
  >
99
- {headerOverlay ? (
100
- <View style={defaultStyle.titleWrap}>
101
- {headerOverlay}
102
- </View>
103
- ) : null}
75
+ <ContentComponent
76
+ style={[contentStyle]}
77
+ contentContainerStyle={{ paddingTop: 0 }}
78
+ >
79
+ <View style={{ height: totalHeaderHeight }} />
80
+ <HeaderOverlay headerOverlay={headerOverlay} />
81
+ {children}
82
+ </ContentComponent>
104
83
 
105
- {children}
106
- </ParallaxScrollView>
84
+ <View
85
+ style={[
86
+ defaultStyle.headerWrap,
87
+ { height: totalHeaderHeight },
88
+ headerBackgroundColor ? { backgroundColor: headerBackgroundColor } : null,
89
+ headerStyle,
90
+ ]}
91
+ >
92
+ <View
93
+ style={[
94
+ defaultStyle.imageInner,
95
+ { top: insetTop, height: headerHeight },
96
+ ]}
97
+ >
98
+ {renderHeaderImage(headerImage)}
99
+ </View>
100
+ </View>
101
+ </View>
107
102
  );
108
103
  }
109
-
@@ -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,16 @@
1
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
2
+
3
+ /**
4
+ * useSafeAreaTopInset
5
+ *
6
+ * Single source of truth for "how much space does the status bar /
7
+ * notch take up". Used by both HeroScreen variants (via
8
+ * useHeroHeaderHeight) and plain Screen, so every screen in the
9
+ * package agrees on the same number.
10
+ *
11
+ * @returns {number}
12
+ */
13
+ export function useSafeAreaTopInset() {
14
+ const insets = useSafeAreaInsets();
15
+ return insets.top;
16
+ }
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.11";
6
6
 
7
7
  // TODO: @opndev/util?
8
8
  export { formatPrice } from './utils/format-price.js';
@@ -4,7 +4,10 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
+ import BaseScreen from './components/base-screen';
7
8
  import HeroScreen from './components/hero-screen';
9
+ import HeroScreenParallax from './components/hero-screen-parallax';
10
+ import HeroScreenFixed from './components/hero-screen-fixed';
8
11
  import Tile from './components/tile';
9
12
 
10
13
  import FoodMenuScreen from './screens/food-menu-screen';
@@ -13,15 +16,59 @@ import NewsItemScreen from './screens/news-item-screen';
13
16
  import NewsListScreen from './screens/news-list-screen';
14
17
  import QRCodeScreen from './screens/qr-code-screen';
15
18
 
19
+ /**
20
+ * createScreens
21
+ *
22
+ * @param {object} [options]
23
+ * @param {React.ComponentType} [options.TextComponent]
24
+ * @param {React.ComponentType} [options.CategoryTextComponent]
25
+ * @param {object} [options.styles]
26
+ * App-provided style object. Used as the default source for
27
+ * heroImage() when a caller doesn't pass an explicit style/fit.
28
+ * Expected (optional) keys: heroImage, heroImageFit.
29
+ *
30
+ * @returns {object} Named screens + helpers, ready to re-export.
31
+ */
16
32
  export function createScreens({
17
33
  TextComponent,
18
34
  CategoryTextComponent,
35
+ styles = {},
19
36
  } = {}) {
37
+ /**
38
+ * heroImage
39
+ *
40
+ * @param {any} source
41
+ * @param {object} [options]
42
+ * @param {object} [options.style]
43
+ * @param {string} [options.fit] // 'cover', 'contain', etc
44
+ */
45
+ function heroImage(source, options = {}) {
46
+ return {
47
+ source,
48
+ style: options.style || styles.heroImage,
49
+ fit: options.fit || styles.heroImageFit,
50
+ };
51
+ }
52
+
20
53
  return {
54
+ heroImage,
55
+
21
56
  HeroScreen: (props) => (
22
57
  <HeroScreen {...props} />
23
58
  ),
24
59
 
60
+ HeroScreenParallax: (props) => (
61
+ <HeroScreenParallax {...props} />
62
+ ),
63
+
64
+ HeroScreenFixed: (props) => (
65
+ <HeroScreenFixed {...props} />
66
+ ),
67
+
68
+ BaseScreen: (props) => (
69
+ <BaseScreen {...props} />
70
+ ),
71
+
25
72
  FoodMenuScreen: (props) => (
26
73
  <FoodMenuScreen
27
74
  {...props}
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.11"
37
38
  }