@stone-js/starters 0.8.11 → 0.8.13

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.
Files changed (78) hide show
  1. package/basic-react-declarative/package.json +5 -5
  2. package/basic-react-imperative/package.json +5 -5
  3. package/basic-react-native-declarative/App.tsx +16 -182
  4. package/basic-react-native-declarative/README.md +130 -29
  5. package/basic-react-native-declarative/app/Application.ts +17 -6
  6. package/basic-react-native-declarative/app/HomeScreen.tsx +80 -0
  7. package/basic-react-native-declarative/app/PortalMark.tsx +32 -0
  8. package/basic-react-native-declarative/app/WelcomeView.tsx +146 -0
  9. package/basic-react-native-declarative/app/theme.ts +66 -0
  10. package/basic-react-native-declarative/app.json +10 -3
  11. package/basic-react-native-declarative/assets/android-icon-background.png +0 -0
  12. package/basic-react-native-declarative/assets/android-icon-foreground.png +0 -0
  13. package/basic-react-native-declarative/assets/android-icon-monochrome.png +0 -0
  14. package/basic-react-native-declarative/assets/favicon.png +0 -0
  15. package/basic-react-native-declarative/assets/icon.png +0 -0
  16. package/basic-react-native-declarative/assets/logo-dark.png +0 -0
  17. package/basic-react-native-declarative/assets/logo-light.png +0 -0
  18. package/basic-react-native-declarative/assets/splash-icon.png +0 -0
  19. package/basic-react-native-declarative/index.ts +16 -8
  20. package/basic-react-native-declarative/metro.config.js +9 -3
  21. package/basic-react-native-declarative/package.json +9 -4
  22. package/basic-react-native-declarative/tests/Application.spec.ts +41 -43
  23. package/basic-react-native-declarative/tests/stubs/react-native.ts +42 -0
  24. package/basic-react-native-declarative/tsconfig.json +0 -1
  25. package/basic-react-native-declarative/vitest.config.ts +13 -5
  26. package/basic-react-native-imperative/App.tsx +26 -0
  27. package/basic-react-native-imperative/LICENSE +21 -0
  28. package/basic-react-native-imperative/README.md +150 -0
  29. package/basic-react-native-imperative/app/Application.ts +25 -0
  30. package/basic-react-native-imperative/app/HomeScreen.tsx +84 -0
  31. package/basic-react-native-imperative/app/PortalMark.tsx +32 -0
  32. package/basic-react-native-imperative/app/WelcomeView.tsx +146 -0
  33. package/basic-react-native-imperative/app/theme.ts +66 -0
  34. package/basic-react-native-imperative/app.json +32 -0
  35. package/basic-react-native-imperative/assets/android-icon-background.png +0 -0
  36. package/basic-react-native-imperative/assets/android-icon-foreground.png +0 -0
  37. package/basic-react-native-imperative/assets/android-icon-monochrome.png +0 -0
  38. package/basic-react-native-imperative/assets/favicon.png +0 -0
  39. package/basic-react-native-imperative/assets/icon.png +0 -0
  40. package/basic-react-native-imperative/assets/logo-dark.png +0 -0
  41. package/basic-react-native-imperative/assets/logo-light.png +0 -0
  42. package/basic-react-native-imperative/assets/splash-icon.png +0 -0
  43. package/basic-react-native-imperative/babel.config.js +16 -0
  44. package/basic-react-native-imperative/index.ts +30 -0
  45. package/basic-react-native-imperative/metro.config.js +13 -0
  46. package/basic-react-native-imperative/package.json +46 -0
  47. package/basic-react-native-imperative/tests/Application.spec.ts +63 -0
  48. package/basic-react-native-imperative/tests/stubs/react-native.ts +42 -0
  49. package/basic-react-native-imperative/tsconfig.json +15 -0
  50. package/basic-react-native-imperative/vitest.config.ts +35 -0
  51. package/basic-service-declarative/package.json +4 -4
  52. package/basic-service-imperative/package.json +4 -4
  53. package/continuum-showcase/package.json +9 -9
  54. package/full-react-declarative/app/layout/AppLayout/AppLayout.tsx +1 -1
  55. package/full-react-declarative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
  56. package/full-react-declarative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
  57. package/full-react-declarative/package.json +10 -10
  58. package/full-react-imperative/app/layout/AppLayout/AppLayout.tsx +1 -1
  59. package/full-react-imperative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
  60. package/full-react-imperative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
  61. package/full-react-imperative/app/layout/SettingsLayout/SettingsLayout.tsx +1 -1
  62. package/full-react-imperative/package.json +10 -10
  63. package/full-service-declarative/package.json +10 -10
  64. package/full-service-imperative/package.json +10 -10
  65. package/package.json +29 -15
  66. package/standard-react-declarative/package.json +9 -9
  67. package/standard-react-imperative/package.json +9 -9
  68. package/standard-service-declarative/package.json +7 -7
  69. package/standard-service-imperative/package.json +7 -7
  70. package/basic-react-native-declarative/adapter/NativeAdapter.ts +0 -108
  71. package/basic-react-native-declarative/adapter/NativeErrorHandler.ts +0 -53
  72. package/basic-react-native-declarative/adapter/NativeEventSource.ts +0 -58
  73. package/basic-react-native-declarative/adapter/RawResponseWrapper.ts +0 -30
  74. package/basic-react-native-declarative/adapter/declarations.ts +0 -40
  75. package/basic-react-native-declarative/adapter/middleware.ts +0 -73
  76. package/basic-react-native-declarative/adapter/nativeAdapterBlueprint.ts +0 -51
  77. package/basic-react-native-declarative/adapter/renderSink.ts +0 -52
  78. package/basic-react-native-declarative/app/WelcomeController.ts +0 -66
@@ -0,0 +1,150 @@
1
+ # Stone.js - Basic React Native starter
2
+
3
+ Stone.js's basic starter to create a React Native (Expo) app using the imperative API.
4
+
5
+ The same welcome screen the web starters show, on a phone. Same mark, same palette, same words,
6
+ because it is the same domain: `app/HomeScreen.tsx` is a page, with `handle`, `head` and `render`,
7
+ and only `render` knows it is native.
8
+
9
+ ## Project Setup
10
+
11
+ ```sh
12
+ npm install
13
+ ```
14
+
15
+ ### Run it
16
+
17
+ ```sh
18
+ npm start # Expo dev server: press i for iOS, a for Android
19
+ npm run ios # straight to the iOS simulator
20
+ npm run android # straight to an Android emulator
21
+ ```
22
+
23
+ ### Run it in a browser
24
+
25
+ The fastest loop is often a browser tab: Fast Refresh, and the same code then runs on a device
26
+ untouched. It is opt-in, because the web target needs two dependencies a native-only app has no
27
+ reason to carry.
28
+
29
+ ```sh
30
+ npx expo install react-dom react-native-web
31
+ npm run web
32
+ ```
33
+
34
+ Use it for the domain, the navigation and most of the interface. Use a device before believing
35
+ anything about the parts that are actually native: `react-native-web` covers the core primitives,
36
+ not the camera, secure storage or a native gesture handler.
37
+
38
+ ### Bundle for production
39
+
40
+ ```sh
41
+ npm run export # a Hermes bytecode bundle, per platform
42
+ ```
43
+
44
+ An installable build is `npx expo run:ios` or an [EAS build](https://docs.expo.dev/build/setup/):
45
+ both need a native toolchain, and they are better commands than a wrapper would be.
46
+
47
+ ### Run tests
48
+
49
+ ```sh
50
+ npm run test
51
+ npm run test:cvg
52
+ ```
53
+
54
+ The tests boot the real kernel, router, adapter and renderer under Node, send a deep link, and
55
+ assert what landed on the navigation stack. No device, no simulator, no Metro.
56
+
57
+ ### Type-check
58
+
59
+ ```sh
60
+ npm run typecheck
61
+ ```
62
+
63
+ ## What is where
64
+
65
+ | File | What it does |
66
+ |---|---|
67
+ | `app/Application.ts` | Enables the router, the adapter and the renderer. One call, two blueprints. |
68
+ | `app/HomeScreen.tsx` | The page factory and its `definePage` blueprint. Adding a screen is adding a file like it. |
69
+ | `app/WelcomeView.tsx` | What the screen draws, in React Native components. |
70
+ | `app/theme.ts` | The Stone.js palette, following the device's light or dark appearance. |
71
+ | `index.ts` | Polyfills, boot, and `registerRootComponent`. |
72
+ | `metro.config.js` | `withStone`: collects `app/` into `.stone/modules.ts` before Metro bundles. |
73
+
74
+ ## Nothing lists your screens
75
+
76
+ A web application never lists its pages, and this one does not either. `withStone` collects
77
+ everything under `app/` and writes `.stone/modules.ts` whenever Metro starts, so `expo start`,
78
+ `expo run:ios` and an EAS build all get it without anyone remembering to ask.
79
+
80
+ It runs when Metro starts, not while it runs: **adding** a screen to a running dev server means
81
+ restarting it. **Editing** one needs nothing, Fast Refresh was never involved.
82
+
83
+ `.stone/` is generated. It is in `.gitignore`, and there is no reason to open it.
84
+
85
+ ## Navigating
86
+
87
+ ```tsx
88
+ import { useNavigate, useGoBack } from '@stone-js/use-react-native'
89
+
90
+ const navigate = useNavigate()
91
+
92
+ navigate('/tasks/42') // push a screen
93
+ navigate('/tasks/42', 'replace') // swap the current one
94
+ navigate('/sign-in', 'reset') // start again, leaving no history
95
+ ```
96
+
97
+ Navigation goes through the router, so a screen never renders another screen itself. `useGoBack()`
98
+ returns `{ goBack, canGoBack }`: wire `goBack` to your header button and to Android's hardware back
99
+ button, and let the platform leave the application when `canGoBack` is false.
100
+
101
+ Deep links arrive the same way. This starter's scheme is `stone`, so `stone://app/?name=Ada` reaches
102
+ the page that owns `/`, with `name` readable through `event.get('name')`. Change `scheme` in
103
+ `app.json` to your own.
104
+
105
+ ## Using a native navigator
106
+
107
+ `App.tsx` renders `StoneNativeApp`, which shows the screen on top of the stack. That is the floor,
108
+ not the ceiling: it is what makes the first run work with nothing installed. The platform's
109
+ transitions, the swipe-back gesture, and a screen keeping its own state while another covers it are
110
+ things only a native navigator gives you.
111
+
112
+ The stack is public state, so a navigator drives itself from it:
113
+
114
+ ```tsx
115
+ import { useScreens } from '@stone-js/use-react-native'
116
+ import { NavigationContainer } from '@react-navigation/native'
117
+ import { createNativeStackNavigator } from '@react-navigation/native-stack'
118
+
119
+ const Stack = createNativeStackNavigator()
120
+
121
+ export default function App () {
122
+ const screens = useScreens()
123
+
124
+ return (
125
+ <NavigationContainer>
126
+ <Stack.Navigator>
127
+ {screens.map((screen) => (
128
+ <Stack.Screen key={screen.key} name={screen.key} options={{ title: screen.title }}>
129
+ {() => screen.element}
130
+ </Stack.Screen>
131
+ ))}
132
+ </Stack.Navigator>
133
+ </NavigationContainer>
134
+ )
135
+ }
136
+ ```
137
+
138
+ Nothing about your pages changes.
139
+
140
+ ## The declarative twin
141
+
142
+ The same application, written with decorators instead of values, is
143
+ [`basic-react-native-declarative`](../basic-react-native-declarative). Neither paradigm wraps the
144
+ other; both write to the same manifest.
145
+
146
+ ## Learn more
147
+
148
+ - [Stone.js documentation](https://stonejs.dev/docs)
149
+ - [`@stone-js/use-react-native`](https://www.npmjs.com/package/@stone-js/use-react-native)
150
+ - [`@stone-js/react-native-adapter`](https://www.npmjs.com/package/@stone-js/react-native-adapter)
@@ -0,0 +1,25 @@
1
+ import { LogLevel } from '@stone-js/core'
2
+ import { routerBlueprint } from '@stone-js/router'
3
+ import { defineStoneReactNativeApp } from '@stone-js/use-react-native'
4
+ import { reactNativeAdapterBlueprint } from '@stone-js/react-native-adapter'
5
+
6
+ /**
7
+ * Application
8
+ *
9
+ * This is the main application entry point.
10
+ *
11
+ * The same four things the declarative starter says with decorators, said with values instead:
12
+ * `defineStoneReactNativeApp` enables the renderer, and the blueprints it is handed enable the
13
+ * router and the React Native adapter. Neither paradigm is a wrapper around the other; both write
14
+ * to the same manifest.
15
+ *
16
+ * The web starter's Application is this same call with one blueprint swapped: the browser adapter's
17
+ * for the native one. That is the promise, in four lines: the domain is written once, and what
18
+ * changes is the context it runs in.
19
+ *
20
+ * Screens live in their own files, one route each. See `app/HomeScreen.tsx`.
21
+ */
22
+ export const Application = defineStoneReactNativeApp(
23
+ { logger: { level: LogLevel.INFO } },
24
+ [routerBlueprint, reactNativeAdapterBlueprint]
25
+ )
@@ -0,0 +1,84 @@
1
+ import { JSX } from 'react'
2
+ import { WelcomeView } from './WelcomeView'
3
+ import { ILogger, Promiseable } from '@stone-js/core'
4
+ import { definePage, HeadContext, IPage, PageHeadContext, PageRenderContext, ReactIncomingEvent } from '@stone-js/use-react-native'
5
+
6
+ /**
7
+ * The home screen, as a factory.
8
+ *
9
+ * A page, exactly as on the web: it answers with `handle`, names itself with `head` and draws itself
10
+ * with `render`. The only native thing here is what `render` returns, and it returns React Native
11
+ * components instead of DOM ones.
12
+ *
13
+ * The factory receives the container's services as its argument, which is how this paradigm gets
14
+ * dependency injection without a constructor.
15
+ *
16
+ * @param options - The injected services.
17
+ * @returns The page.
18
+ */
19
+ export const HomeScreen = ({ logger }: AppOptions): IPage<ReactIncomingEvent> => {
20
+ return {
21
+ /**
22
+ * Handle incoming events.
23
+ *
24
+ * A deep link carries its parameters here: `stone://app/?name=Ada` arrives as an event, and
25
+ * `event.get` reads from it the same way an HTTP request would be read.
26
+ *
27
+ * @param event - Incoming event.
28
+ * @returns The screen's data.
29
+ */
30
+ handle (event: ReactIncomingEvent): ResponseData {
31
+ const message = `Hello ${String(event.get<string>('name', 'World'))}!`
32
+
33
+ logger.info(message)
34
+
35
+ return { message }
36
+ },
37
+
38
+ /**
39
+ * Set the screen's head.
40
+ *
41
+ * A phone has no meta tags, so the title is what a navigator shows in its header. The same
42
+ * `head` a web page declares, read by a different renderer.
43
+ *
44
+ * @returns The head context.
45
+ */
46
+ head ({ event }: PageHeadContext): Promiseable<HeadContext> {
47
+ return {
48
+ title: `${String(event.get<string>('name', 'World'))} · Welcome to Stone.js`,
49
+ description: 'A universal Stone.js application. Write your domain once; Stone.js is the context that runs it anywhere.'
50
+ }
51
+ },
52
+
53
+ /**
54
+ * Render the screen.
55
+ *
56
+ * @returns The rendered screen.
57
+ */
58
+ render ({ data }: PageRenderContext<ResponseData>): JSX.Element {
59
+ return <WelcomeView message={data?.message} />
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * HomeScreen Blueprint
66
+ *
67
+ * Binds the page to its route. Adding a screen is adding a file like this one: nothing lists it,
68
+ * because `withStone` collects everything under `app/` before Metro bundles.
69
+ */
70
+ export const HomeScreenBlueprint = definePage(HomeScreen, { path: '/' })
71
+
72
+ /**
73
+ * Application options.
74
+ */
75
+ interface AppOptions {
76
+ logger: ILogger
77
+ }
78
+
79
+ /**
80
+ * Response data
81
+ */
82
+ export interface ResponseData {
83
+ message: string
84
+ }
@@ -0,0 +1,32 @@
1
+ import { JSX } from 'react'
2
+ import { Image, useColorScheme } from 'react-native'
3
+
4
+ /**
5
+ * Le Portail, the Stone.js mark.
6
+ *
7
+ * The same three arcs as `public/logo.svg` in the web starters: two stone segments rising, and the
8
+ * keystone lit in ember. Two files rather than one, because the segments have to contrast with the
9
+ * ground the screen paints, exactly as the brand ships a dark-ground and a light-ground Portal.
10
+ *
11
+ * Shipped as an image on purpose. Drawing it would mean `react-native-svg`, a native module, and
12
+ * a starter's first job is to run on a device the moment it is installed, with nothing to link
13
+ * and no version to match against Expo Go's.
14
+ *
15
+ * @param props.size - Side length in points.
16
+ * @returns The mark.
17
+ */
18
+ export function PortalMark ({ size = 104 }: { size?: number }): JSX.Element {
19
+ const scheme = useColorScheme()
20
+
21
+ return (
22
+ <Image
23
+ accessibilityRole='image'
24
+ accessibilityLabel='Stone.js'
25
+ source={scheme === 'light'
26
+ ? require('../assets/logo-light.png')
27
+ : require('../assets/logo-dark.png')}
28
+ style={{ width: size, height: size }}
29
+ resizeMode='contain'
30
+ />
31
+ )
32
+ }
@@ -0,0 +1,146 @@
1
+ import { JSX } from 'react'
2
+ import { PortalMark } from './PortalMark'
3
+ import { useStoneTheme } from './theme'
4
+ import { Linking, Platform, Pressable, StyleSheet, Text, View } from 'react-native'
5
+
6
+ const DOCS = 'https://stonejs.dev/docs'
7
+ const GITHUB = 'https://github.com/stone-foundation/stone-js-framework'
8
+
9
+ /**
10
+ * The welcome screen.
11
+ *
12
+ * The native twin of the web starters' welcome page: the same mark, the same palette, the same
13
+ * words. What differs is only what a phone does differently, which is the whole point of the
14
+ * framework: `View` instead of `div`, a `StyleSheet` instead of a stylesheet, `Linking` instead of
15
+ * an anchor, and a glow drawn as a circle because there is no `radial-gradient`.
16
+ *
17
+ * @param props.message - What the handler returned.
18
+ * @returns The screen.
19
+ */
20
+ export function WelcomeView ({ message }: { message?: string }): JSX.Element {
21
+ const theme = useStoneTheme()
22
+
23
+ const open = (url: string): void => { void Linking.openURL(url) }
24
+
25
+ return (
26
+ <View style={[styles.screen, { backgroundColor: theme.ink }]}>
27
+ <View style={[styles.glow, { backgroundColor: theme.inkGlow }]} pointerEvents='none' />
28
+
29
+ <View style={styles.hero}>
30
+ <PortalMark size={104} />
31
+
32
+ <Text style={[styles.eyebrow, { color: theme.creamDim }]}>WELCOME TO</Text>
33
+ <Text style={[styles.title, { color: theme.ember2 }]}>Stone.js</Text>
34
+ <Text style={[styles.lead, { color: theme.cream }]}>{message}</Text>
35
+ <Text style={[styles.tagline, { color: theme.creamDim }]}>
36
+ Your app is running. Write your domain once, Stone.js is the context that runs it
37
+ anywhere: server, serverless, browser, CLI and the edge.
38
+ </Text>
39
+
40
+ <View style={styles.links}>
41
+ <Pressable
42
+ accessibilityRole='link'
43
+ onPress={() => open(DOCS)}
44
+ style={[styles.pill, { borderColor: theme.line, backgroundColor: theme.card }]}
45
+ >
46
+ <Text style={[styles.pillText, { color: theme.cream }]}>Documentation</Text>
47
+ </Pressable>
48
+ <Pressable
49
+ accessibilityRole='link'
50
+ onPress={() => open(GITHUB)}
51
+ style={[styles.pill, { borderColor: theme.line, backgroundColor: theme.card }]}
52
+ >
53
+ <Text style={[styles.pillText, { color: theme.cream }]}>GitHub</Text>
54
+ </Pressable>
55
+ </View>
56
+
57
+ <Text style={[styles.edit, { color: theme.creamDim }]}>
58
+ Edit <Text style={[styles.editFile, { color: theme.ember1 }]}>app/HomeScreen.tsx</Text>
59
+ </Text>
60
+ </View>
61
+
62
+ <Text style={[styles.brand, { color: theme.creamDim }]}>
63
+ <Text style={{ color: theme.ember2 }}>●</Text> Stone.js · the continuum framework
64
+ </Text>
65
+ </View>
66
+ )
67
+ }
68
+
69
+ const styles = StyleSheet.create({
70
+ screen: {
71
+ flex: 1,
72
+ alignItems: 'center',
73
+ justifyContent: 'center',
74
+ paddingHorizontal: 24,
75
+ paddingVertical: 48,
76
+ overflow: 'hidden'
77
+ },
78
+ // The web starter's ember glow, as the only shape a phone can draw it with.
79
+ glow: {
80
+ position: 'absolute',
81
+ top: '4%',
82
+ width: 420,
83
+ height: 420,
84
+ borderRadius: 210,
85
+ opacity: 0.55
86
+ },
87
+ hero: {
88
+ width: '100%',
89
+ maxWidth: 420,
90
+ alignItems: 'center'
91
+ },
92
+ eyebrow: {
93
+ marginTop: 22,
94
+ fontSize: 12,
95
+ fontWeight: '600',
96
+ letterSpacing: 3.4
97
+ },
98
+ title: {
99
+ marginTop: 6,
100
+ fontSize: 52,
101
+ fontWeight: '800',
102
+ letterSpacing: -1.6
103
+ },
104
+ lead: {
105
+ marginTop: 16,
106
+ fontSize: 20,
107
+ fontWeight: '600',
108
+ textAlign: 'center'
109
+ },
110
+ tagline: {
111
+ marginTop: 10,
112
+ fontSize: 15,
113
+ lineHeight: 24,
114
+ textAlign: 'center'
115
+ },
116
+ links: {
117
+ marginTop: 30,
118
+ flexDirection: 'row',
119
+ flexWrap: 'wrap',
120
+ justifyContent: 'center',
121
+ gap: 12
122
+ },
123
+ pill: {
124
+ borderWidth: 1,
125
+ borderRadius: 12,
126
+ paddingVertical: 11,
127
+ paddingHorizontal: 18
128
+ },
129
+ pillText: {
130
+ fontSize: 14,
131
+ fontWeight: '600'
132
+ },
133
+ edit: {
134
+ marginTop: 18,
135
+ fontSize: 13,
136
+ fontFamily: Platform.select({ ios: 'Menlo', android: 'monospace', default: 'monospace' })
137
+ },
138
+ editFile: {
139
+ fontWeight: '700'
140
+ },
141
+ brand: {
142
+ position: 'absolute',
143
+ bottom: 32,
144
+ fontSize: 13
145
+ }
146
+ })
@@ -0,0 +1,66 @@
1
+ import { useColorScheme } from 'react-native'
2
+
3
+ /**
4
+ * The Stone.js brand, "Obsidienne & Braise", as values a StyleSheet can use.
5
+ *
6
+ * The same palette the web starters paint in CSS custom properties. A native application has no
7
+ * cascade, so the tokens are plain values and the screen reads them through `useStoneTheme`.
8
+ */
9
+ export interface StoneTheme {
10
+ /** The page ground. */
11
+ ink: string
12
+ /** The glow at the top of the ground, behind the mark. */
13
+ inkGlow: string
14
+ /** Primary text. */
15
+ cream: string
16
+ /** Secondary text. */
17
+ creamDim: string
18
+ /** The ember gradient, light end. Also the accent for small text. */
19
+ ember1: string
20
+ /** The ember gradient, dark end. */
21
+ ember2: string
22
+ /** Hairlines and card borders. */
23
+ line: string
24
+ /** Card and pill surfaces. */
25
+ card: string
26
+ /** The mark's two arcs. */
27
+ mark: string
28
+ }
29
+
30
+ const dark: StoneTheme = {
31
+ ink: '#14110F',
32
+ inkGlow: '#241C17',
33
+ cream: '#F4EFE6',
34
+ creamDim: '#A69A8C',
35
+ ember1: '#FFC46B',
36
+ ember2: '#FF5A1F',
37
+ line: 'rgba(244, 239, 230, 0.10)',
38
+ card: 'rgba(244, 239, 230, 0.05)',
39
+ mark: '#E2D9CC'
40
+ }
41
+
42
+ const light: StoneTheme = {
43
+ ink: '#F4EFE6',
44
+ inkGlow: '#FFFFFF',
45
+ cream: '#1B1613',
46
+ creamDim: '#6B6154',
47
+ // On a light ground the ember needs the darker end to stay readable, per the brand's
48
+ // contrast notes: `braise.clair` for large elements, `braise.texte` for small text.
49
+ ember1: '#E04E10',
50
+ ember2: '#A83A08',
51
+ line: 'rgba(27, 22, 19, 0.12)',
52
+ card: 'rgba(27, 22, 19, 0.04)',
53
+ mark: '#1B1613'
54
+ }
55
+
56
+ /**
57
+ * The palette matching the device's appearance.
58
+ *
59
+ * Dark is the default, as in the web starters: `useColorScheme` returns `null` when the platform
60
+ * expresses no preference, and the brand's ground is obsidian.
61
+ *
62
+ * @returns The active theme.
63
+ */
64
+ export function useStoneTheme (): StoneTheme {
65
+ return useColorScheme() === 'light' ? light : dark
66
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "expo": {
3
+ "name": "Stone.js",
4
+ "slug": "basic-react-native-imperative",
5
+ "scheme": "stone",
6
+ "version": "1.0.0",
7
+ "orientation": "portrait",
8
+ "icon": "./assets/icon.png",
9
+ "userInterfaceStyle": "automatic",
10
+ "backgroundColor": "#14110F",
11
+ "splash": {
12
+ "image": "./assets/splash-icon.png",
13
+ "resizeMode": "contain",
14
+ "backgroundColor": "#14110F"
15
+ },
16
+ "ios": {
17
+ "supportsTablet": true
18
+ },
19
+ "android": {
20
+ "adaptiveIcon": {
21
+ "backgroundColor": "#14110F",
22
+ "foregroundImage": "./assets/android-icon-foreground.png",
23
+ "backgroundImage": "./assets/android-icon-background.png",
24
+ "monochromeImage": "./assets/android-icon-monochrome.png"
25
+ },
26
+ "predictiveBackGestureEnabled": false
27
+ },
28
+ "web": {
29
+ "favicon": "./assets/favicon.png"
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Stone.js runs on TC39 stage-3 decorators (2023-11) with `Symbol.metadata`.
3
+ *
4
+ * `babel-preset-expo` applies `@babel/plugin-proposal-decorators` itself (in
5
+ * legacy mode by default): the decorator semantics are configured through the
6
+ * preset's `decorators` option, never by adding the plugin separately, so the
7
+ * preset keeps its ordering guarantees against the class-field transforms.
8
+ */
9
+ module.exports = function (api) {
10
+ api.cache(true)
11
+ return {
12
+ presets: [
13
+ ['babel-preset-expo', { decorators: { version: '2023-11' } }]
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Platform polyfills come first, before anything from Stone.js loads.
3
+ *
4
+ * - React Native's built-in `URL` is a stub (no `pathname`, no `searchParams`), and the router
5
+ * reads `pathname` on every event, so the polyfill is required rather than nice to have.
6
+ * - `TextEncoder` is installed only when the engine does not provide it (older Hermes).
7
+ */
8
+ import 'react-native-url-polyfill/auto'
9
+
10
+ if (typeof globalThis.TextEncoder === 'undefined') {
11
+ require('fast-text-encoding')
12
+ }
13
+
14
+ import { stoneApp } from '@stone-js/core'
15
+ import { registerRootComponent } from 'expo'
16
+ import App from './App'
17
+
18
+ /**
19
+ * Every module under `app/`, collected at build time.
20
+ *
21
+ * Generated by `withStone` in `metro.config.js`, because collecting modules is a bundler question
22
+ * and Metro has no `import.meta.glob`. Never edit it, never commit it.
23
+ */
24
+ import { modules } from './.stone/modules'
25
+
26
+ // Boot the framework, then hand the root component to Expo. `registerRootComponent` works the same
27
+ // in Expo Go and in a native build.
28
+ void stoneApp({ modules }).run()
29
+
30
+ registerRootComponent(App)
@@ -0,0 +1,13 @@
1
+ const { getDefaultConfig } = require('expo/metro-config')
2
+ const { withStone } = require('@stone-js/use-react-native/metro')
3
+
4
+ /**
5
+ * `withStone` collects every module under `app/` and writes `.stone/modules.ts` before Metro
6
+ * bundles, because Metro resolves imports statically and has no `import.meta.glob`. It hooks in
7
+ * here rather than in a command on purpose: Metro loads this file whatever started it, so
8
+ * `expo start`, `expo run:ios` and an EAS build all get the generation without anyone asking.
9
+ *
10
+ * Pinning the project root also keeps Metro scoped to this starter, even when it lives inside a
11
+ * larger workspace, as it does in the Stone.js monorepo.
12
+ */
13
+ module.exports = withStone(getDefaultConfig(__dirname), __dirname)
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "basic-react-native-imperative",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "description": "Stone.js's basic starter to create a React Native (Expo) app using the imperative API.",
6
+ "author": "Mr. Stone <evensstone@gmail.com>",
7
+ "license": "MIT",
8
+ "main": "index.ts",
9
+ "engines": {
10
+ "node": ">=20.0.0"
11
+ },
12
+ "scripts": {
13
+ "start": "expo start",
14
+ "android": "expo start --android",
15
+ "ios": "expo start --ios",
16
+ "web": "expo start --web",
17
+ "export": "expo export",
18
+ "modules": "node -e \"require('@stone-js/use-react-native/metro').writeManifest(process.cwd())\"",
19
+ "typecheck": "npm run modules && tsc --noEmit",
20
+ "test": "vitest run",
21
+ "test:cvg": "npm run test -- --coverage"
22
+ },
23
+ "dependencies": {
24
+ "@stone-js/browser-core": "^0.8.13",
25
+ "@stone-js/core": "^0.8.13",
26
+ "@stone-js/react-native-adapter": "^0.8.13",
27
+ "@stone-js/router": "^0.8.13",
28
+ "@stone-js/use-react-native": "^0.8.13",
29
+ "expo": "~57.0.14",
30
+ "expo-status-bar": "~57.0.1",
31
+ "fast-text-encoding": "^1.0.6",
32
+ "react": "19.2.3",
33
+ "react-native": "0.86.2",
34
+ "react-native-url-polyfill": "^3.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@babel/plugin-proposal-decorators": "^7.28.0",
38
+ "@stone-js/cli": "^0.8.13",
39
+ "@stone-js/testing": "^0.8.13",
40
+ "@types/react": "~19.2.2",
41
+ "@vitest/coverage-v8": "^3.0.9",
42
+ "babel-preset-expo": "^57.0.7",
43
+ "typescript": "~6.0.3",
44
+ "vitest": "^3.0.9"
45
+ }
46
+ }