@stone-js/starters 0.8.12 → 0.8.14
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 +6 -5
- package/basic-react-declarative/tests/Application.spec.ts +4 -3
- package/basic-react-imperative/package.json +6 -5
- package/basic-react-imperative/tests/Application.spec.ts +4 -3
- package/basic-react-native-declarative/README.md +15 -22
- package/basic-react-native-declarative/package.json +7 -7
- package/basic-react-native-declarative/tests/HomeScreen.spec.ts +36 -0
- package/basic-react-native-declarative/tests/{Application.spec.ts → navigation.spec.ts} +10 -7
- package/basic-react-native-declarative/vitest.config.ts +6 -0
- package/basic-react-native-imperative/README.md +15 -22
- package/basic-react-native-imperative/package.json +7 -7
- package/basic-react-native-imperative/tests/HomeScreen.spec.ts +36 -0
- package/basic-react-native-imperative/tests/{Application.spec.ts → navigation.spec.ts} +10 -7
- package/basic-react-native-imperative/vitest.config.ts +6 -0
- package/basic-service-declarative/package.json +5 -4
- package/basic-service-declarative/tests/Application.spec.ts +2 -1
- package/basic-service-imperative/package.json +5 -4
- package/basic-service-imperative/tests/Application.spec.ts +2 -1
- package/continuum-showcase/package.json +9 -9
- package/continuum-showcase/tests/application.spec.ts +2 -1
- package/full-react-declarative/package.json +10 -10
- package/full-react-declarative/tests/application.spec.ts +2 -1
- package/full-react-imperative/package.json +10 -10
- package/full-react-imperative/tests/application.spec.ts +2 -1
- package/full-service-declarative/package.json +10 -10
- package/full-service-declarative/tests/application.spec.ts +2 -1
- package/full-service-imperative/package.json +10 -10
- package/full-service-imperative/tests/application.spec.ts +2 -1
- package/monorepo-declarative/LICENSE +21 -0
- package/monorepo-declarative/README.md +91 -0
- package/monorepo-declarative/apps/mobile/App.tsx +26 -0
- package/monorepo-declarative/apps/mobile/LICENSE +21 -0
- package/monorepo-declarative/apps/mobile/app/Application.ts +24 -0
- package/monorepo-declarative/apps/mobile/app/PortalMark.tsx +32 -0
- package/monorepo-declarative/apps/mobile/app/TaskListScreen.tsx +67 -0
- package/monorepo-declarative/apps/mobile/app/TaskListView.tsx +98 -0
- package/monorepo-declarative/apps/mobile/app/theme.ts +66 -0
- package/monorepo-declarative/apps/mobile/app.json +32 -0
- package/monorepo-declarative/apps/mobile/assets/android-icon-background.png +0 -0
- package/monorepo-declarative/apps/mobile/assets/android-icon-foreground.png +0 -0
- package/monorepo-declarative/apps/mobile/assets/android-icon-monochrome.png +0 -0
- package/monorepo-declarative/apps/mobile/assets/favicon.png +0 -0
- package/monorepo-declarative/apps/mobile/assets/icon.png +0 -0
- package/monorepo-declarative/apps/mobile/assets/logo-dark.png +0 -0
- package/monorepo-declarative/apps/mobile/assets/logo-light.png +0 -0
- package/monorepo-declarative/apps/mobile/assets/splash-icon.png +0 -0
- package/monorepo-declarative/apps/mobile/babel.config.js +16 -0
- package/monorepo-declarative/apps/mobile/index.ts +30 -0
- package/monorepo-declarative/apps/mobile/metro.config.js +13 -0
- package/monorepo-declarative/apps/mobile/package.json +47 -0
- package/monorepo-declarative/apps/mobile/tests/TaskListScreen.spec.ts +54 -0
- package/monorepo-declarative/apps/mobile/tests/stubs/react-native.ts +42 -0
- package/monorepo-declarative/apps/mobile/tsconfig.json +15 -0
- package/monorepo-declarative/apps/mobile/vitest.config.ts +35 -0
- package/monorepo-declarative/apps/web/LICENSE +21 -0
- package/monorepo-declarative/apps/web/app/Application.ts +23 -0
- package/monorepo-declarative/apps/web/app/TaskListPage.tsx +95 -0
- package/monorepo-declarative/apps/web/assets/css/index.css +220 -0
- package/monorepo-declarative/apps/web/package.json +40 -0
- package/monorepo-declarative/apps/web/public/favicon.svg +16 -0
- package/monorepo-declarative/apps/web/public/logo.svg +16 -0
- package/monorepo-declarative/apps/web/tests/TaskListPage.spec.ts +37 -0
- package/monorepo-declarative/apps/web/tsconfig.json +33 -0
- package/monorepo-declarative/package.json +28 -0
- package/monorepo-declarative/packages/domain/LICENSE +21 -0
- package/monorepo-declarative/packages/domain/package.json +32 -0
- package/monorepo-declarative/packages/domain/src/Task.ts +16 -0
- package/monorepo-declarative/packages/domain/src/TaskService.ts +75 -0
- package/monorepo-declarative/packages/domain/src/index.ts +33 -0
- package/monorepo-declarative/packages/domain/tests/TaskService.spec.ts +73 -0
- package/monorepo-declarative/packages/domain/tsconfig.build.json +4 -0
- package/monorepo-declarative/packages/domain/tsconfig.json +16 -0
- package/monorepo-declarative/packages/domain/vitest.config.ts +15 -0
- package/monorepo-declarative/pnpm-lock.yaml +7613 -0
- package/monorepo-declarative/pnpm-workspace.yaml +4 -0
- package/package.json +16 -1
- package/standard-react-declarative/package.json +9 -9
- package/standard-react-declarative/tests/application.spec.ts +2 -1
- package/standard-react-imperative/package.json +9 -9
- package/standard-react-imperative/tests/application.spec.ts +2 -1
- package/standard-service-declarative/package.json +7 -7
- package/standard-service-declarative/tests/Application.spec.ts +2 -1
- package/standard-service-imperative/package.json +7 -7
- package/standard-service-imperative/tests/Application.spec.ts +2 -1
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@libsql/client": "^0.14.0",
|
|
25
|
-
"@stone-js/core": "^0.8.
|
|
26
|
-
"@stone-js/env": "^0.8.
|
|
27
|
-
"@stone-js/filesystem": "^0.8.
|
|
28
|
-
"@stone-js/http-core": "^0.8.
|
|
29
|
-
"@stone-js/node-cli-adapter": "^0.8.
|
|
30
|
-
"@stone-js/node-http-adapter": "^0.8.
|
|
31
|
-
"@stone-js/pipeline": "^0.8.
|
|
32
|
-
"@stone-js/router": "^0.8.
|
|
25
|
+
"@stone-js/core": "^0.8.14",
|
|
26
|
+
"@stone-js/env": "^0.8.14",
|
|
27
|
+
"@stone-js/filesystem": "^0.8.14",
|
|
28
|
+
"@stone-js/http-core": "^0.8.14",
|
|
29
|
+
"@stone-js/node-cli-adapter": "^0.8.14",
|
|
30
|
+
"@stone-js/node-http-adapter": "^0.8.14",
|
|
31
|
+
"@stone-js/pipeline": "^0.8.14",
|
|
32
|
+
"@stone-js/router": "^0.8.14",
|
|
33
33
|
"bcryptjs": "^3.0.2",
|
|
34
34
|
"drizzle-orm": "^0.45.2",
|
|
35
35
|
"jsonwebtoken": "^9.0.2"
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
39
39
|
"@babel/preset-env": "^7.26.9",
|
|
40
40
|
"@babel/preset-typescript": "^7.27.0",
|
|
41
|
-
"@stone-js/cli": "^0.8.
|
|
42
|
-
"@stone-js/testing": "^0.8.
|
|
41
|
+
"@stone-js/cli": "^0.8.14",
|
|
42
|
+
"@stone-js/testing": "^0.8.14",
|
|
43
43
|
"@types/jsonwebtoken": "^9.0.8",
|
|
44
44
|
"@types/node": "^24.0.3",
|
|
45
45
|
"@vitest/coverage-v8": "^3.0.9",
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@libsql/client": "^0.14.0",
|
|
25
|
-
"@stone-js/core": "^0.8.
|
|
26
|
-
"@stone-js/env": "^0.8.
|
|
27
|
-
"@stone-js/filesystem": "^0.8.
|
|
28
|
-
"@stone-js/http-core": "^0.8.
|
|
29
|
-
"@stone-js/node-cli-adapter": "^0.8.
|
|
30
|
-
"@stone-js/node-http-adapter": "^0.8.
|
|
31
|
-
"@stone-js/pipeline": "^0.8.
|
|
32
|
-
"@stone-js/router": "^0.8.
|
|
25
|
+
"@stone-js/core": "^0.8.14",
|
|
26
|
+
"@stone-js/env": "^0.8.14",
|
|
27
|
+
"@stone-js/filesystem": "^0.8.14",
|
|
28
|
+
"@stone-js/http-core": "^0.8.14",
|
|
29
|
+
"@stone-js/node-cli-adapter": "^0.8.14",
|
|
30
|
+
"@stone-js/node-http-adapter": "^0.8.14",
|
|
31
|
+
"@stone-js/pipeline": "^0.8.14",
|
|
32
|
+
"@stone-js/router": "^0.8.14",
|
|
33
33
|
"bcryptjs": "^3.0.2",
|
|
34
34
|
"drizzle-orm": "^0.45.2",
|
|
35
35
|
"jsonwebtoken": "^9.0.2"
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
39
39
|
"@babel/preset-env": "^7.26.9",
|
|
40
40
|
"@babel/preset-typescript": "^7.27.0",
|
|
41
|
-
"@stone-js/cli": "^0.8.
|
|
42
|
-
"@stone-js/testing": "^0.8.
|
|
41
|
+
"@stone-js/cli": "^0.8.14",
|
|
42
|
+
"@stone-js/testing": "^0.8.14",
|
|
43
43
|
"@types/jsonwebtoken": "^9.0.8",
|
|
44
44
|
"@types/node": "^24.0.3",
|
|
45
45
|
"@vitest/coverage-v8": "^3.0.9",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Stone Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Stone.js - One domain, two applications
|
|
2
|
+
|
|
3
|
+
A monorepo where the interesting file is the one that runs everywhere.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
acme/
|
|
7
|
+
├── packages/domain/ @acme/domain the entities and the behaviour
|
|
8
|
+
├── apps/web/ @acme/web a browser application
|
|
9
|
+
└── apps/mobile/ @acme/mobile a React Native application
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`packages/domain` imports `@stone-js/core` and nothing else about a platform: no `Request`, no
|
|
13
|
+
`Response`, no `window`, no `View`. Both applications depend on it, and neither has a copy of it.
|
|
14
|
+
|
|
15
|
+
## The two applications, side by side
|
|
16
|
+
|
|
17
|
+
Open both and read them together. This is the whole demonstration:
|
|
18
|
+
|
|
19
|
+
| | `apps/web/app/Application.ts` | `apps/mobile/app/Application.ts` |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| Router | `@Routing()` | `@Routing()` |
|
|
22
|
+
| Where events come from | `@Browser()` | `@ReactNative()` |
|
|
23
|
+
| What a route becomes | `@UseReact()` | `@UseReactNative()` |
|
|
24
|
+
| The domain | `[domainBlueprint]` | `[domainBlueprint]` |
|
|
25
|
+
|
|
26
|
+
Two decorators differ. Everything else is the same file.
|
|
27
|
+
|
|
28
|
+
Then open `apps/web/app/TaskListPage.tsx` next to `apps/mobile/app/TaskListScreen.tsx`. `handle` and
|
|
29
|
+
`head` are identical, line for line, because answering a route is not a platform question. Only
|
|
30
|
+
`render` differs, and only in what it draws with: `div` and `ul` on one side, `View` and `ScrollView`
|
|
31
|
+
on the other.
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pnpm install
|
|
37
|
+
pnpm --filter @acme/domain build
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The domain is built first because both applications consume its `dist`. `pnpm run build` does both in
|
|
41
|
+
order.
|
|
42
|
+
|
|
43
|
+
## Run them
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
pnpm run dev:web # http://localhost:8080
|
|
47
|
+
pnpm run dev:mobile # Expo: press i for iOS, a for Android
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Test them
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
pnpm run test # every package
|
|
54
|
+
pnpm --filter @acme/domain test # the domain alone, in milliseconds
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Three suites, three levels, and the cheapest one is the one that matters most:
|
|
58
|
+
|
|
59
|
+
- **`packages/domain`** boots nothing. Plain objects in, plain objects out. This is where the
|
|
60
|
+
behaviour is tested, because this is where the behaviour is.
|
|
61
|
+
- **`apps/web`** boots the real kernel through `createTestApp()` and reads the HTML that came back.
|
|
62
|
+
- **`apps/mobile`** boots the real kernel, adapter and renderer under Node, sends a deep link, and
|
|
63
|
+
asserts what landed on the navigation stack. No device, no simulator.
|
|
64
|
+
|
|
65
|
+
The web and mobile suites assert the same domain behaviour through two different contexts, which is
|
|
66
|
+
the claim this repository exists to make.
|
|
67
|
+
|
|
68
|
+
## Why React is pinned at the root
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
"pnpm": { "overrides": { "react": "19.2.3", "react-dom": "19.2.3" } }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Expo pins React to an exact version, and a workspace with one React and a different React DOM fails
|
|
75
|
+
at run time with "Incompatible React versions". Pinning both at the root is the one piece of
|
|
76
|
+
configuration a web-and-mobile monorepo genuinely needs; without it the two applications drift apart
|
|
77
|
+
the first time either one updates.
|
|
78
|
+
|
|
79
|
+
## Adding to the domain
|
|
80
|
+
|
|
81
|
+
Put it in `packages/domain/src`, export it from `src/index.ts`, and register it in `domainBlueprint`
|
|
82
|
+
if an application should resolve it from the container. Both applications see it on the next build,
|
|
83
|
+
and neither one changes.
|
|
84
|
+
|
|
85
|
+
Relative imports inside the domain carry their `.js` extension, because it is published as ESM with
|
|
86
|
+
`moduleResolution: NodeNext`. That is what a consumer on modern Node needs in order to resolve them.
|
|
87
|
+
|
|
88
|
+
## Learn more
|
|
89
|
+
|
|
90
|
+
- [Stone.js documentation](https://stonejs.dev/docs)
|
|
91
|
+
- [The Continuum Architecture](https://evens-stone.github.io/continuum-manifesto/manifesto)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { JSX } from 'react'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import { StatusBar } from 'expo-status-bar'
|
|
4
|
+
import { useStoneTheme } from './app/theme'
|
|
5
|
+
import { StoneNativeApp } from '@stone-js/use-react-native'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The root component.
|
|
9
|
+
*
|
|
10
|
+
* `StoneNativeApp` shows the screen on top of the stack, which is all a first run needs, and it
|
|
11
|
+
* needs nothing installed to do it. When the application grows a real navigator, this is the
|
|
12
|
+
* component it replaces: the stack is public state, so `@react-navigation/native-stack` drives
|
|
13
|
+
* itself from the same object through `useScreens()`. The README shows that wiring.
|
|
14
|
+
*
|
|
15
|
+
* @returns The application.
|
|
16
|
+
*/
|
|
17
|
+
export default function App (): JSX.Element {
|
|
18
|
+
const theme = useStoneTheme()
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<View style={{ flex: 1, backgroundColor: theme.ink }}>
|
|
22
|
+
<StatusBar style='auto' />
|
|
23
|
+
<StoneNativeApp />
|
|
24
|
+
</View>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-present 650 Industries, Inc. (aka Expo)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Routing } from '@stone-js/router'
|
|
2
|
+
import { LogLevel, StoneApp } from '@stone-js/core'
|
|
3
|
+
import { domainBlueprint } from '@acme/domain'
|
|
4
|
+
import { ReactNative } from '@stone-js/react-native-adapter'
|
|
5
|
+
import { UseReactNative } from '@stone-js/use-react-native'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The mobile application.
|
|
9
|
+
*
|
|
10
|
+
* Four decorators and one blueprint. `@ReactNative()` says where events come from, deep links
|
|
11
|
+
* included, `@UseReactNative()` says what a resolved route becomes, and `domainBlueprint` brings in
|
|
12
|
+
* the shared domain, which is the only line here that has anything to do with what the application
|
|
13
|
+
* is *about*.
|
|
14
|
+
*
|
|
15
|
+
* Compare with `apps/web/app/Application.ts`: the same file with two decorators swapped. That is the
|
|
16
|
+
* whole difference between a website and a native application in this repository.
|
|
17
|
+
*
|
|
18
|
+
* Screens live in their own files, one route each. See `app/TaskListScreen.tsx`.
|
|
19
|
+
*/
|
|
20
|
+
@Routing()
|
|
21
|
+
@ReactNative()
|
|
22
|
+
@UseReactNative()
|
|
23
|
+
@StoneApp({ name: 'acme-mobile', logger: { level: LogLevel.INFO } }, [domainBlueprint])
|
|
24
|
+
export class Application {}
|
|
@@ -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,67 @@
|
|
|
1
|
+
import { JSX } from 'react'
|
|
2
|
+
import { Promiseable } from '@stone-js/core'
|
|
3
|
+
import { TaskListView } from './TaskListView'
|
|
4
|
+
import { Task, TaskService } from '@acme/domain'
|
|
5
|
+
import { HeadContext, IPage, Page, PageHeadContext, PageRenderContext, ReactIncomingEvent } from '@stone-js/use-react-native'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The task list, as a native screen.
|
|
9
|
+
*
|
|
10
|
+
* Read `handle` and `head` next to `apps/web/app/TaskListPage.tsx`: they are the same, line for line,
|
|
11
|
+
* because answering a route is not a platform question. Only `render` differs, and only because a
|
|
12
|
+
* browser draws with `div` and a phone draws with `View`.
|
|
13
|
+
*/
|
|
14
|
+
@Page('/')
|
|
15
|
+
export class TaskListScreen implements IPage<ReactIncomingEvent> {
|
|
16
|
+
private readonly tasks: TaskService
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param tasks - The shared domain service, under the alias its blueprint registered.
|
|
20
|
+
*/
|
|
21
|
+
constructor ({ tasks }: { tasks: TaskService }) {
|
|
22
|
+
this.tasks = tasks
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Answer the route.
|
|
27
|
+
*
|
|
28
|
+
* @param event - The incoming event.
|
|
29
|
+
* @returns The screen's data.
|
|
30
|
+
*/
|
|
31
|
+
handle (event: ReactIncomingEvent): TaskListData {
|
|
32
|
+
const toggle = event.get<string>('toggle')
|
|
33
|
+
|
|
34
|
+
if (toggle !== undefined) { this.tasks.toggle(toggle) }
|
|
35
|
+
|
|
36
|
+
return { tasks: this.tasks.all(), remaining: this.tasks.remaining() }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Name the screen.
|
|
41
|
+
*
|
|
42
|
+
* @returns The head context.
|
|
43
|
+
*/
|
|
44
|
+
head ({ data }: PageHeadContext<TaskListData>): Promiseable<HeadContext> {
|
|
45
|
+
return {
|
|
46
|
+
title: `${data?.remaining ?? 0} left · Acme tasks`,
|
|
47
|
+
description: 'One domain, two applications. This is the native one.'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Draw the screen.
|
|
53
|
+
*
|
|
54
|
+
* @returns The rendered screen.
|
|
55
|
+
*/
|
|
56
|
+
render ({ data }: PageRenderContext<TaskListData>): JSX.Element {
|
|
57
|
+
return <TaskListView tasks={data?.tasks ?? []} remaining={data?.remaining ?? 0} />
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* What the screen hands to its renderer.
|
|
63
|
+
*/
|
|
64
|
+
export interface TaskListData {
|
|
65
|
+
tasks: Task[]
|
|
66
|
+
remaining: number
|
|
67
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { JSX } from 'react'
|
|
2
|
+
import { Task } from '@acme/domain'
|
|
3
|
+
import { PortalMark } from './PortalMark'
|
|
4
|
+
import { useStoneTheme } from './theme'
|
|
5
|
+
import { useNavigate } from '@stone-js/use-react-native'
|
|
6
|
+
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What the task list looks like on a phone.
|
|
10
|
+
*
|
|
11
|
+
* The web application's page draws the same data with `div`, `ul` and `a`. This one uses `View`,
|
|
12
|
+
* `ScrollView` and `Pressable`, and goes through `useNavigate` instead of an `href`: navigation is
|
|
13
|
+
* the router's business either way, so a screen never renders another screen itself.
|
|
14
|
+
*
|
|
15
|
+
* @param props.tasks - The tasks to show.
|
|
16
|
+
* @param props.remaining - How many are left.
|
|
17
|
+
* @returns The screen.
|
|
18
|
+
*/
|
|
19
|
+
export function TaskListView ({ tasks, remaining }: { tasks: Task[], remaining: number }): JSX.Element {
|
|
20
|
+
const theme = useStoneTheme()
|
|
21
|
+
const navigate = useNavigate()
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<View style={[styles.screen, { backgroundColor: theme.ink }]}>
|
|
25
|
+
<View style={[styles.glow, { backgroundColor: theme.inkGlow }]} pointerEvents='none' />
|
|
26
|
+
|
|
27
|
+
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false}>
|
|
28
|
+
<PortalMark size={88} />
|
|
29
|
+
|
|
30
|
+
<Text style={[styles.eyebrow, { color: theme.creamDim }]}>ONE DOMAIN, TWO APPS</Text>
|
|
31
|
+
<Text style={[styles.title, { color: theme.ember2 }]}>Acme tasks</Text>
|
|
32
|
+
<Text style={[styles.lead, { color: theme.cream }]}>{remaining} left to do</Text>
|
|
33
|
+
|
|
34
|
+
<View style={styles.tasks}>
|
|
35
|
+
{tasks.map((task) => (
|
|
36
|
+
<Pressable
|
|
37
|
+
key={task.id}
|
|
38
|
+
accessibilityRole='button'
|
|
39
|
+
accessibilityState={{ checked: task.done }}
|
|
40
|
+
onPress={() => navigate(`/?toggle=${task.id}`, 'replace')}
|
|
41
|
+
style={[styles.task, { borderColor: theme.line, backgroundColor: theme.card }]}
|
|
42
|
+
>
|
|
43
|
+
<Text style={[styles.box, { color: task.done ? theme.creamDim : theme.ember2 }]}>
|
|
44
|
+
{task.done ? '●' : '○'}
|
|
45
|
+
</Text>
|
|
46
|
+
<Text style={[styles.taskTitle, { color: task.done ? theme.creamDim : theme.cream }]}>
|
|
47
|
+
{task.title}
|
|
48
|
+
</Text>
|
|
49
|
+
</Pressable>
|
|
50
|
+
))}
|
|
51
|
+
</View>
|
|
52
|
+
|
|
53
|
+
<Text style={[styles.brand, { color: theme.creamDim }]}>
|
|
54
|
+
<Text style={{ color: theme.ember2 }}>●</Text> the same{' '}
|
|
55
|
+
<Text style={[styles.strong, { color: theme.ember1 }]}>@acme/domain</Text> runs the web app
|
|
56
|
+
</Text>
|
|
57
|
+
</ScrollView>
|
|
58
|
+
</View>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const styles = StyleSheet.create({
|
|
63
|
+
screen: { flex: 1, overflow: 'hidden' },
|
|
64
|
+
// The web app's ember glow, as the only shape a phone can draw it with.
|
|
65
|
+
glow: {
|
|
66
|
+
position: 'absolute',
|
|
67
|
+
top: '2%',
|
|
68
|
+
alignSelf: 'center',
|
|
69
|
+
width: 380,
|
|
70
|
+
height: 380,
|
|
71
|
+
borderRadius: 190,
|
|
72
|
+
opacity: 0.5
|
|
73
|
+
},
|
|
74
|
+
content: {
|
|
75
|
+
flexGrow: 1,
|
|
76
|
+
alignItems: 'center',
|
|
77
|
+
justifyContent: 'center',
|
|
78
|
+
paddingHorizontal: 24,
|
|
79
|
+
paddingVertical: 72
|
|
80
|
+
},
|
|
81
|
+
eyebrow: { marginTop: 20, fontSize: 11, fontWeight: '600', letterSpacing: 3 },
|
|
82
|
+
title: { marginTop: 6, fontSize: 44, fontWeight: '800', letterSpacing: -1.4 },
|
|
83
|
+
lead: { marginTop: 12, fontSize: 18, fontWeight: '600' },
|
|
84
|
+
tasks: { marginTop: 26, width: '100%', maxWidth: 420, gap: 8 },
|
|
85
|
+
task: {
|
|
86
|
+
flexDirection: 'row',
|
|
87
|
+
alignItems: 'center',
|
|
88
|
+
gap: 12,
|
|
89
|
+
borderWidth: 1,
|
|
90
|
+
borderRadius: 12,
|
|
91
|
+
paddingVertical: 14,
|
|
92
|
+
paddingHorizontal: 16
|
|
93
|
+
},
|
|
94
|
+
box: { fontSize: 13 },
|
|
95
|
+
taskTitle: { flex: 1, fontSize: 16, fontWeight: '600' },
|
|
96
|
+
strong: { fontWeight: '700' },
|
|
97
|
+
brand: { marginTop: 32, fontSize: 13, textAlign: 'center' }
|
|
98
|
+
})
|
|
@@ -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": "Acme tasks",
|
|
4
|
+
"slug": "acme-mobile",
|
|
5
|
+
"scheme": "acme",
|
|
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
|
|
@@ -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)
|