@stone-js/starters 0.8.10 → 0.8.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/basic-react-declarative/package.json +5 -5
- package/basic-react-imperative/package.json +5 -5
- package/basic-react-native-declarative/App.tsx +16 -182
- package/basic-react-native-declarative/README.md +136 -21
- package/basic-react-native-declarative/app/Application.ts +17 -6
- package/basic-react-native-declarative/app/HomeScreen.tsx +80 -0
- package/basic-react-native-declarative/app/PortalMark.tsx +32 -0
- package/basic-react-native-declarative/app/WelcomeView.tsx +146 -0
- package/basic-react-native-declarative/app/theme.ts +66 -0
- package/basic-react-native-declarative/app.json +10 -3
- package/basic-react-native-declarative/assets/android-icon-background.png +0 -0
- package/basic-react-native-declarative/assets/android-icon-foreground.png +0 -0
- package/basic-react-native-declarative/assets/android-icon-monochrome.png +0 -0
- package/basic-react-native-declarative/assets/favicon.png +0 -0
- package/basic-react-native-declarative/assets/icon.png +0 -0
- package/basic-react-native-declarative/assets/logo-dark.png +0 -0
- package/basic-react-native-declarative/assets/logo-light.png +0 -0
- package/basic-react-native-declarative/assets/splash-icon.png +0 -0
- package/basic-react-native-declarative/index.ts +16 -8
- package/basic-react-native-declarative/metro.config.js +9 -3
- package/basic-react-native-declarative/package.json +9 -4
- package/basic-react-native-declarative/tests/Application.spec.ts +41 -43
- package/basic-react-native-declarative/tests/stubs/react-native.ts +42 -0
- package/basic-react-native-declarative/tsconfig.json +0 -1
- package/basic-react-native-declarative/vitest.config.ts +13 -5
- package/basic-react-native-imperative/App.tsx +26 -0
- package/basic-react-native-imperative/LICENSE +21 -0
- package/basic-react-native-imperative/README.md +150 -0
- package/basic-react-native-imperative/app/Application.ts +25 -0
- package/basic-react-native-imperative/app/HomeScreen.tsx +84 -0
- package/basic-react-native-imperative/app/PortalMark.tsx +32 -0
- package/basic-react-native-imperative/app/WelcomeView.tsx +146 -0
- package/basic-react-native-imperative/app/theme.ts +66 -0
- package/basic-react-native-imperative/app.json +32 -0
- package/basic-react-native-imperative/assets/android-icon-background.png +0 -0
- package/basic-react-native-imperative/assets/android-icon-foreground.png +0 -0
- package/basic-react-native-imperative/assets/android-icon-monochrome.png +0 -0
- package/basic-react-native-imperative/assets/favicon.png +0 -0
- package/basic-react-native-imperative/assets/icon.png +0 -0
- package/basic-react-native-imperative/assets/logo-dark.png +0 -0
- package/basic-react-native-imperative/assets/logo-light.png +0 -0
- package/basic-react-native-imperative/assets/splash-icon.png +0 -0
- package/basic-react-native-imperative/babel.config.js +16 -0
- package/basic-react-native-imperative/index.ts +30 -0
- package/basic-react-native-imperative/metro.config.js +13 -0
- package/basic-react-native-imperative/package.json +46 -0
- package/basic-react-native-imperative/tests/Application.spec.ts +63 -0
- package/basic-react-native-imperative/tests/stubs/react-native.ts +42 -0
- package/basic-react-native-imperative/tsconfig.json +15 -0
- package/basic-react-native-imperative/vitest.config.ts +35 -0
- package/basic-service-declarative/package.json +4 -4
- package/basic-service-imperative/package.json +4 -4
- package/continuum-showcase/package.json +9 -9
- package/full-react-declarative/app/layout/AppLayout/AppLayout.tsx +1 -1
- package/full-react-declarative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
- package/full-react-declarative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
- package/full-react-declarative/package.json +10 -10
- package/full-react-imperative/app/layout/AppLayout/AppLayout.tsx +1 -1
- package/full-react-imperative/app/layout/ErrorLayout/ErrorLayout.tsx +1 -1
- package/full-react-imperative/app/layout/SecurityLayout/SecurityLayout.tsx +1 -1
- package/full-react-imperative/app/layout/SettingsLayout/SettingsLayout.tsx +1 -1
- package/full-react-imperative/package.json +10 -10
- package/full-service-declarative/package.json +10 -10
- package/full-service-imperative/package.json +10 -10
- package/package.json +29 -15
- package/standard-react-declarative/package.json +9 -9
- package/standard-react-imperative/package.json +9 -9
- package/standard-service-declarative/package.json +7 -7
- package/standard-service-imperative/package.json +7 -7
- package/basic-react-native-declarative/adapter/NativeAdapter.ts +0 -108
- package/basic-react-native-declarative/adapter/NativeErrorHandler.ts +0 -53
- package/basic-react-native-declarative/adapter/NativeEventSource.ts +0 -58
- package/basic-react-native-declarative/adapter/RawResponseWrapper.ts +0 -30
- package/basic-react-native-declarative/adapter/declarations.ts +0 -40
- package/basic-react-native-declarative/adapter/middleware.ts +0 -73
- package/basic-react-native-declarative/adapter/nativeAdapterBlueprint.ts +0 -51
- package/basic-react-native-declarative/adapter/renderSink.ts +0 -52
- 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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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.12",
|
|
25
|
+
"@stone-js/core": "^0.8.12",
|
|
26
|
+
"@stone-js/react-native-adapter": "^0.8.12",
|
|
27
|
+
"@stone-js/router": "^0.8.12",
|
|
28
|
+
"@stone-js/use-react-native": "^0.8.12",
|
|
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.12",
|
|
39
|
+
"@stone-js/testing": "^0.8.12",
|
|
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
|
+
}
|