@stone-js/starters 0.8.13 → 0.8.15
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 +36 -0
- package/monorepo-declarative/apps/mobile/tests/navigation.spec.ts +62 -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 +41 -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 +41 -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 +38 -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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@acme/mobile",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "The mobile application: the shared domain, running on a phone.",
|
|
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
|
+
"@acme/domain": "workspace:*",
|
|
25
|
+
"@stone-js/browser-core": "^0.8.13",
|
|
26
|
+
"@stone-js/core": "^0.8.13",
|
|
27
|
+
"@stone-js/react-native-adapter": "^0.8.13",
|
|
28
|
+
"@stone-js/router": "^0.8.13",
|
|
29
|
+
"@stone-js/use-react-native": "^0.8.13",
|
|
30
|
+
"expo": "~57.0.14",
|
|
31
|
+
"expo-status-bar": "~57.0.1",
|
|
32
|
+
"fast-text-encoding": "^1.0.6",
|
|
33
|
+
"react": "19.2.3",
|
|
34
|
+
"react-native": "0.86.2",
|
|
35
|
+
"react-native-url-polyfill": "^3.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@babel/plugin-proposal-decorators": "^7.28.0",
|
|
39
|
+
"@stone-js/cli": "^0.8.13",
|
|
40
|
+
"@stone-js/testing": "^0.8.13",
|
|
41
|
+
"@types/react": "~19.2.2",
|
|
42
|
+
"@vitest/coverage-v8": "^3.0.9",
|
|
43
|
+
"babel-preset-expo": "^57.0.7",
|
|
44
|
+
"typescript": "~6.0.3",
|
|
45
|
+
"vitest": "^3.0.9"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createTestApp } from '@stone-js/testing'
|
|
2
|
+
import { makeIncomingBrowserEvent } from '@stone-js/testing/browser'
|
|
3
|
+
import { REACT_NATIVE_PLATFORM } from '@stone-js/react-native-adapter'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The mobile application, booted in memory and asked the same questions the web one is asked.
|
|
7
|
+
*
|
|
8
|
+
* Read it next to `apps/web/tests/TaskListPage.spec.ts`. Same domain, same assertions, same counts,
|
|
9
|
+
* through two different contexts. That is the claim this repository exists to demonstrate, and it is
|
|
10
|
+
* visible in the tests rather than only in the source.
|
|
11
|
+
*
|
|
12
|
+
* `createTestApp()` discovers `app/**` and substitutes the adapter, which is the architecture rather
|
|
13
|
+
* than a concession to testing: the domain is written once and the context is chosen at run time, so
|
|
14
|
+
* under Node the context is a test adapter instead of React Native. What a native application does
|
|
15
|
+
* differently, its navigation stack, is `tests/navigation.spec.ts`.
|
|
16
|
+
*/
|
|
17
|
+
describe('TaskListScreen', () => {
|
|
18
|
+
it('resolves the task list from the shared domain', async () => {
|
|
19
|
+
const app = await createTestApp({ platform: REACT_NATIVE_PLATFORM })
|
|
20
|
+
|
|
21
|
+
const response = await app.send(makeIncomingBrowserEvent({ url: 'acme://app/' }))
|
|
22
|
+
|
|
23
|
+
expect(response.statusCode).toBe(200)
|
|
24
|
+
// The head is what a navigator shows in its header, and it carries the count the domain computed.
|
|
25
|
+
expect((response.content as any).head.title).toBe('1 left · Acme tasks')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('toggles a task through the same domain the web app uses', async () => {
|
|
29
|
+
const app = await createTestApp({ platform: REACT_NATIVE_PLATFORM })
|
|
30
|
+
|
|
31
|
+
const response = await app.send(makeIncomingBrowserEvent({ url: 'acme://app/?toggle=3' }))
|
|
32
|
+
|
|
33
|
+
expect(response.statusCode).toBe(200)
|
|
34
|
+
expect((response.content as any).head.title).toBe('0 left · Acme tasks')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { stoneApp } from '@stone-js/core'
|
|
2
|
+
import { Application } from '../app/Application'
|
|
3
|
+
import { ScreenStack } from '@stone-js/use-react-native'
|
|
4
|
+
import { TaskListScreen } from '../app/TaskListScreen'
|
|
5
|
+
import { NavigationSource } from '@stone-js/react-native-adapter'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Navigation, with the real adapter this time.
|
|
9
|
+
*
|
|
10
|
+
* `TaskListScreen.spec.ts` asks what a route resolves to, which every platform answers the same way.
|
|
11
|
+
* This asks the native question: what lands on the navigation stack, and in what order. So nothing is
|
|
12
|
+
* substituted. The kernel, the router, the React Native adapter and the renderer are the ones a phone
|
|
13
|
+
* runs; what a device adds is a screen to draw on.
|
|
14
|
+
*
|
|
15
|
+
* Two things are supplied rather than discovered, both through documented configuration: the
|
|
16
|
+
* navigation source, so the test can send a deep link, and the screen stack, so it can read what
|
|
17
|
+
* landed on it.
|
|
18
|
+
*/
|
|
19
|
+
describe('Navigation', () => {
|
|
20
|
+
const screens = ScreenStack.create()
|
|
21
|
+
const navigation = NavigationSource.create({ baseUrl: 'acme://app' })
|
|
22
|
+
|
|
23
|
+
// Booted once for the whole suite, as on a device: the default blueprints are shared module
|
|
24
|
+
// objects, so booting the same modules twice in one process would accumulate route definitions.
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
await stoneApp({
|
|
27
|
+
modules: [
|
|
28
|
+
Application,
|
|
29
|
+
TaskListScreen,
|
|
30
|
+
{ stone: { reactNative: { navigationSource: navigation }, useReactNative: { screenStack: screens } } }
|
|
31
|
+
]
|
|
32
|
+
}).run()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const settle = async (): Promise<void> => { await new Promise((resolve) => setImmediate(resolve)) }
|
|
36
|
+
|
|
37
|
+
it('puts the task list on the stack at launch, counted by the shared domain', () => {
|
|
38
|
+
expect(screens.size()).toBe(1)
|
|
39
|
+
expect(screens.top()?.path).toBe('/')
|
|
40
|
+
expect(screens.top()?.title).toBe('1 left · Acme tasks')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('routes a deep link through the same domain the web app uses', async () => {
|
|
44
|
+
navigation.navigate('acme://app/?toggle=3')
|
|
45
|
+
await settle()
|
|
46
|
+
|
|
47
|
+
expect(screens.top()?.title).toBe('0 left · Acme tasks')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('re-resolving the current route replaces it instead of stacking a duplicate', () => {
|
|
51
|
+
// A user walking back must not find two identical screens, so the stack swaps the top.
|
|
52
|
+
expect(screens.size()).toBe(1)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('surfaces an unknown route instead of crashing', async () => {
|
|
56
|
+
navigation.navigate('acme://app/nowhere')
|
|
57
|
+
await settle()
|
|
58
|
+
|
|
59
|
+
// An error page is still a screen: the application stays up and shows something.
|
|
60
|
+
expect(screens.top()?.path).not.toBe('/')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `react-native` is under a Node test runner: absent.
|
|
3
|
+
*
|
|
4
|
+
* The screen imports `View`, `Text` and friends at module scope, so the specifier has to resolve to
|
|
5
|
+
* something for the application to even load. It resolves to this: the primitives as inert
|
|
6
|
+
* identifiers, which is enough because a resolved screen is a React element that has been *built*,
|
|
7
|
+
* not mounted. React never calls these components here, so there is nothing for them to do.
|
|
8
|
+
*
|
|
9
|
+
* That is the line this test draws, and it is the honest one. What runs under Node is the domain:
|
|
10
|
+
* the kernel, the router, the loader, the head. What a `View` looks like is React Native's business,
|
|
11
|
+
* and the way to see it is to run the application, either on a device or in a browser tab through
|
|
12
|
+
* `npx expo start --web`.
|
|
13
|
+
*/
|
|
14
|
+
const component = (name: string): string => name
|
|
15
|
+
|
|
16
|
+
export const View = component('View')
|
|
17
|
+
export const Text = component('Text')
|
|
18
|
+
export const Image = component('Image')
|
|
19
|
+
export const Pressable = component('Pressable')
|
|
20
|
+
export const ScrollView = component('ScrollView')
|
|
21
|
+
|
|
22
|
+
export const StyleSheet = {
|
|
23
|
+
create: <T>(styles: T): T => styles,
|
|
24
|
+
flatten: (style: unknown): unknown => style
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const Platform = {
|
|
28
|
+
OS: 'ios',
|
|
29
|
+
select: <T>(spec: Record<string, T>): T | undefined => spec.ios ?? spec.default
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Linking = {
|
|
33
|
+
openURL: async (): Promise<void> => {},
|
|
34
|
+
getInitialURL: async (): Promise<string | null> => null,
|
|
35
|
+
addEventListener: () => ({ remove: () => {} })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const useColorScheme = (): string | null => null
|
|
39
|
+
|
|
40
|
+
export default {
|
|
41
|
+
View, Text, Image, Pressable, ScrollView, StyleSheet, Platform, Linking, useColorScheme
|
|
42
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { defineConfig } from 'vitest/config'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
// Under Node there is no React Native, and the screen imports its primitives at module scope, so
|
|
6
|
+
// the specifier has to resolve somewhere. `tests/stubs/react-native.ts` explains what it stands
|
|
7
|
+
// for and where the line is drawn.
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
'react-native': fileURLToPath(new URL('./tests/stubs/react-native.ts', import.meta.url))
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
// `experimentalDecorators` is enabled in tsconfig.json for tsc TYPING only (the framework types
|
|
14
|
+
// its decorators with the legacy signatures). At RUNTIME Stone.js requires standard 2023-11
|
|
15
|
+
// decorators, so esbuild must ignore that flag and emit the standard semantics, `Symbol.metadata`
|
|
16
|
+
// included (esbuild 0.25+ implements them correctly on its own).
|
|
17
|
+
esbuild: {
|
|
18
|
+
tsconfigRaw: {
|
|
19
|
+
compilerOptions: {
|
|
20
|
+
experimentalDecorators: false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
test: {
|
|
25
|
+
// `createTestApp()` discovers the application's modules by importing them at run time. Vitest
|
|
26
|
+
// externalises dependencies from `node_modules`, and an externalised module's `import()` is
|
|
27
|
+
// Node's, which cannot load a `.ts` file. Inlining the package puts those imports back through
|
|
28
|
+
// Vite's transform. `stone test` does this for you; a project running Vitest directly, as an
|
|
29
|
+
// Expo project does, states it here.
|
|
30
|
+
server: { deps: { inline: ['@stone-js/testing'] } },
|
|
31
|
+
globals: true,
|
|
32
|
+
environment: 'node',
|
|
33
|
+
include: ['./tests/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
|
34
|
+
coverage: {
|
|
35
|
+
provider: 'v8',
|
|
36
|
+
include: ['app/**/*.{ts,tsx}'],
|
|
37
|
+
reporter: ['text', 'html'],
|
|
38
|
+
reportsDirectory: './coverage'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
})
|
|
@@ -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,23 @@
|
|
|
1
|
+
import { domainBlueprint } from '@acme/domain'
|
|
2
|
+
import { Routing } from '@stone-js/router'
|
|
3
|
+
import { UseReact } from '@stone-js/use-react'
|
|
4
|
+
import { LogLevel, StoneApp } from '@stone-js/core'
|
|
5
|
+
import { Browser } from '@stone-js/browser-adapter'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The web application.
|
|
9
|
+
*
|
|
10
|
+
* Four decorators and one blueprint. `@Browser()` says where events come from, `@UseReact()` says
|
|
11
|
+
* what a resolved route becomes, and `domainBlueprint` brings in the shared domain, which is the
|
|
12
|
+
* only line here that has anything to do with what the application is *about*.
|
|
13
|
+
*
|
|
14
|
+
* Compare with `apps/mobile/app/Application.ts`: the same file with two decorators swapped. That is
|
|
15
|
+
* the whole difference between a website and a native application in this repository.
|
|
16
|
+
*
|
|
17
|
+
* Pages live in their own files, one route each. See `app/TaskListPage.tsx`.
|
|
18
|
+
*/
|
|
19
|
+
@Routing()
|
|
20
|
+
@Browser()
|
|
21
|
+
@UseReact()
|
|
22
|
+
@StoneApp({ name: 'acme-web', logger: { level: LogLevel.INFO } }, [domainBlueprint])
|
|
23
|
+
export class Application {}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { JSX } from 'react'
|
|
2
|
+
import { Promiseable } from '@stone-js/core'
|
|
3
|
+
import { Task, TaskService } from '@acme/domain'
|
|
4
|
+
import { HeadContext, IPage, Page, PageHeadContext, PageRenderContext, ReactIncomingEvent } from '@stone-js/use-react'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The task list, as a web page.
|
|
8
|
+
*
|
|
9
|
+
* Read `handle` and `head` next to `apps/mobile/app/TaskListScreen.tsx`: they are the same, line for
|
|
10
|
+
* line, because answering a route is not a platform question. Only `render` differs, and only because
|
|
11
|
+
* a browser draws with `div` and a phone draws with `View`.
|
|
12
|
+
*/
|
|
13
|
+
@Page('/')
|
|
14
|
+
export class TaskListPage implements IPage<ReactIncomingEvent> {
|
|
15
|
+
private readonly tasks: TaskService
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param tasks - The shared domain service, under the alias its blueprint registered.
|
|
19
|
+
*/
|
|
20
|
+
constructor ({ tasks }: { tasks: TaskService }) {
|
|
21
|
+
this.tasks = tasks
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Answer the route.
|
|
26
|
+
*
|
|
27
|
+
* @param event - The incoming event.
|
|
28
|
+
* @returns The page's data.
|
|
29
|
+
*/
|
|
30
|
+
handle (event: ReactIncomingEvent): TaskListData {
|
|
31
|
+
const toggle = event.get<string>('toggle')
|
|
32
|
+
|
|
33
|
+
if (toggle !== undefined) { this.tasks.toggle(toggle) }
|
|
34
|
+
|
|
35
|
+
return { tasks: this.tasks.all(), remaining: this.tasks.remaining() }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Name the page.
|
|
40
|
+
*
|
|
41
|
+
* @returns The head context.
|
|
42
|
+
*/
|
|
43
|
+
head ({ data }: PageHeadContext<TaskListData>): Promiseable<HeadContext> {
|
|
44
|
+
return {
|
|
45
|
+
title: `${data?.remaining ?? 0} left · Acme tasks`,
|
|
46
|
+
description: 'One domain, two applications. This is the web one.'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Draw the page.
|
|
52
|
+
*
|
|
53
|
+
* @returns The rendered page.
|
|
54
|
+
*/
|
|
55
|
+
render ({ data }: PageRenderContext<TaskListData>): JSX.Element {
|
|
56
|
+
return (
|
|
57
|
+
<main className='stone-welcome'>
|
|
58
|
+
<div className='glow' aria-hidden='true' />
|
|
59
|
+
<section className='hero'>
|
|
60
|
+
<img className='mark' src='/logo.svg' alt='Stone.js' width={104} height={104} />
|
|
61
|
+
<p className='eyebrow'>ONE DOMAIN, TWO APPS</p>
|
|
62
|
+
<h1 className='title'>Acme tasks</h1>
|
|
63
|
+
<p className='lead'>{data?.remaining} left to do</p>
|
|
64
|
+
|
|
65
|
+
<ul className='tasks'>
|
|
66
|
+
{data?.tasks.map((task) => (
|
|
67
|
+
<li key={task.id} className={task.done ? 'task done' : 'task'}>
|
|
68
|
+
<a href={`/?toggle=${task.id}`}>
|
|
69
|
+
<span className='box' aria-hidden='true'>{task.done ? '●' : '○'}</span>
|
|
70
|
+
{task.title}
|
|
71
|
+
</a>
|
|
72
|
+
</li>
|
|
73
|
+
))}
|
|
74
|
+
</ul>
|
|
75
|
+
|
|
76
|
+
<nav className='links'>
|
|
77
|
+
<a href='https://stonejs.dev/docs' target='_blank' rel='noreferrer noopener'>Documentation</a>
|
|
78
|
+
<span className='edit'>Edit <b>apps/web/app/TaskListPage.tsx</b></span>
|
|
79
|
+
</nav>
|
|
80
|
+
</section>
|
|
81
|
+
<footer className='brand'>
|
|
82
|
+
<span className='dot'>●</span> the same <b>@acme/domain</b> runs the mobile app
|
|
83
|
+
</footer>
|
|
84
|
+
</main>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* What the page hands to its renderer.
|
|
91
|
+
*/
|
|
92
|
+
export interface TaskListData {
|
|
93
|
+
tasks: Task[]
|
|
94
|
+
remaining: number
|
|
95
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stone.js starter — brand welcome screen.
|
|
3
|
+
* Identity: "Obsidienne & Braise" — obsidian ground, ember (braise) accent, the Portal mark.
|
|
4
|
+
* Theme-aware: dark by default, light under prefers-color-scheme: light.
|
|
5
|
+
*/
|
|
6
|
+
:root {
|
|
7
|
+
--ink: #14100E;
|
|
8
|
+
--ink-2: #1E1815;
|
|
9
|
+
--cream: #F4EFE6;
|
|
10
|
+
--cream-dim: #B8AE9E;
|
|
11
|
+
--ember-1: #FFC46B;
|
|
12
|
+
--ember-2: #FF5A1F;
|
|
13
|
+
--line: rgba(244, 239, 230, 0.10);
|
|
14
|
+
--card: rgba(244, 239, 230, 0.04);
|
|
15
|
+
--bg: radial-gradient(1200px 600px at 50% -10%, #241C17 0%, var(--ink) 55%);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@media (prefers-color-scheme: light) {
|
|
19
|
+
:root {
|
|
20
|
+
--ink: #F4EFE6;
|
|
21
|
+
--ink-2: #ffffff;
|
|
22
|
+
--cream: #1B1613;
|
|
23
|
+
--cream-dim: #6B6154;
|
|
24
|
+
--line: rgba(27, 22, 19, 0.10);
|
|
25
|
+
--card: rgba(27, 22, 19, 0.03);
|
|
26
|
+
--bg: radial-gradient(1200px 600px at 50% -10%, #FFFFFF 0%, #F1E9DC 60%);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
* { box-sizing: border-box; }
|
|
31
|
+
|
|
32
|
+
body {
|
|
33
|
+
margin: 0;
|
|
34
|
+
min-height: 100vh;
|
|
35
|
+
color: var(--cream);
|
|
36
|
+
background: var(--ink);
|
|
37
|
+
font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
38
|
+
-webkit-font-smoothing: antialiased;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.stone-welcome {
|
|
42
|
+
position: relative;
|
|
43
|
+
min-height: 100vh;
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
padding: 48px 24px;
|
|
49
|
+
background: var(--bg);
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Ember glow behind the mark. */
|
|
54
|
+
.stone-welcome .glow {
|
|
55
|
+
position: absolute;
|
|
56
|
+
top: 8%;
|
|
57
|
+
left: 50%;
|
|
58
|
+
width: 520px;
|
|
59
|
+
height: 520px;
|
|
60
|
+
transform: translateX(-50%);
|
|
61
|
+
background: radial-gradient(circle, rgba(255, 90, 31, 0.22) 0%, rgba(255, 90, 31, 0) 68%);
|
|
62
|
+
filter: blur(8px);
|
|
63
|
+
pointer-events: none;
|
|
64
|
+
z-index: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.stone-welcome .hero {
|
|
68
|
+
position: relative;
|
|
69
|
+
z-index: 1;
|
|
70
|
+
width: 100%;
|
|
71
|
+
max-width: 680px;
|
|
72
|
+
text-align: center;
|
|
73
|
+
animation: rise 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.stone-welcome .mark {
|
|
77
|
+
width: 104px;
|
|
78
|
+
height: 104px;
|
|
79
|
+
filter: drop-shadow(0 8px 28px rgba(255, 90, 31, 0.35));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.stone-welcome .eyebrow {
|
|
83
|
+
margin: 22px 0 0;
|
|
84
|
+
font-size: 12px;
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
letter-spacing: 0.28em;
|
|
87
|
+
text-transform: uppercase;
|
|
88
|
+
color: var(--cream-dim);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.stone-welcome .title {
|
|
92
|
+
margin: 8px 0 0;
|
|
93
|
+
font-size: clamp(44px, 8vw, 76px);
|
|
94
|
+
font-weight: 800;
|
|
95
|
+
letter-spacing: -0.03em;
|
|
96
|
+
line-height: 1.02;
|
|
97
|
+
background: linear-gradient(92deg, var(--ember-1), var(--ember-2));
|
|
98
|
+
-webkit-background-clip: text;
|
|
99
|
+
background-clip: text;
|
|
100
|
+
color: transparent;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.stone-welcome .lead {
|
|
104
|
+
margin: 18px auto 0;
|
|
105
|
+
font-size: 20px;
|
|
106
|
+
font-weight: 600;
|
|
107
|
+
color: var(--cream);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.stone-welcome .tagline {
|
|
111
|
+
margin: 10px auto 0;
|
|
112
|
+
max-width: 46ch;
|
|
113
|
+
font-size: 16px;
|
|
114
|
+
line-height: 1.6;
|
|
115
|
+
color: var(--cream-dim);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.stone-welcome .links {
|
|
119
|
+
margin-top: 34px;
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-wrap: wrap;
|
|
122
|
+
gap: 12px;
|
|
123
|
+
justify-content: center;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.stone-welcome .links a,
|
|
127
|
+
.stone-welcome .links .edit {
|
|
128
|
+
display: inline-flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: 8px;
|
|
131
|
+
padding: 11px 18px;
|
|
132
|
+
border: 1px solid var(--line);
|
|
133
|
+
border-radius: 12px;
|
|
134
|
+
background: var(--card);
|
|
135
|
+
color: var(--cream);
|
|
136
|
+
font-size: 14px;
|
|
137
|
+
font-weight: 600;
|
|
138
|
+
text-decoration: none;
|
|
139
|
+
transition: border-color 0.2s, transform 0.2s, background 0.2s;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.stone-welcome .links a:hover {
|
|
143
|
+
transform: translateY(-2px);
|
|
144
|
+
border-color: rgba(255, 90, 31, 0.6);
|
|
145
|
+
background: rgba(255, 90, 31, 0.08);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.stone-welcome .links .edit {
|
|
149
|
+
font-family: ui-monospace, 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
|
150
|
+
color: var(--cream-dim);
|
|
151
|
+
}
|
|
152
|
+
.stone-welcome .links .edit b { color: var(--ember-1); font-weight: 700; }
|
|
153
|
+
|
|
154
|
+
.stone-welcome .brand {
|
|
155
|
+
position: relative;
|
|
156
|
+
z-index: 1;
|
|
157
|
+
margin-top: 56px;
|
|
158
|
+
font-size: 13px;
|
|
159
|
+
letter-spacing: 0.02em;
|
|
160
|
+
color: var(--cream-dim);
|
|
161
|
+
}
|
|
162
|
+
.stone-welcome .brand .dot { color: var(--ember-2); }
|
|
163
|
+
|
|
164
|
+
@keyframes rise {
|
|
165
|
+
from { opacity: 0; transform: translateY(14px); }
|
|
166
|
+
to { opacity: 1; transform: translateY(0); }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@media (prefers-reduced-motion: reduce) {
|
|
170
|
+
.stone-welcome .hero { animation: none; }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* The task list. */
|
|
174
|
+
.stone-welcome .tasks {
|
|
175
|
+
margin: 26px 0 0;
|
|
176
|
+
padding: 0;
|
|
177
|
+
list-style: none;
|
|
178
|
+
display: flex;
|
|
179
|
+
flex-direction: column;
|
|
180
|
+
gap: 8px;
|
|
181
|
+
text-align: left;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.stone-welcome .task a {
|
|
185
|
+
display: flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
gap: 12px;
|
|
188
|
+
padding: 13px 16px;
|
|
189
|
+
border: 1px solid var(--line);
|
|
190
|
+
border-radius: 12px;
|
|
191
|
+
background: var(--card);
|
|
192
|
+
color: var(--cream);
|
|
193
|
+
font-size: 16px;
|
|
194
|
+
font-weight: 600;
|
|
195
|
+
text-decoration: none;
|
|
196
|
+
transition: border-color 0.2s, background 0.2s;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.stone-welcome .task a:hover {
|
|
200
|
+
border-color: rgba(255, 90, 31, 0.6);
|
|
201
|
+
background: rgba(255, 90, 31, 0.08);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.stone-welcome .task .box {
|
|
205
|
+
color: var(--ember-2);
|
|
206
|
+
font-size: 13px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.stone-welcome .task.done a {
|
|
210
|
+
color: var(--cream-dim);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.stone-welcome .task.done .box {
|
|
214
|
+
color: var(--cream-dim);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.stone-welcome .brand b {
|
|
218
|
+
color: var(--ember-1);
|
|
219
|
+
font-weight: 700;
|
|
220
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@acme/web",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "The web application: the shared domain, served in a browser.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20.0.0"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "stone dev",
|
|
13
|
+
"build": "stone build",
|
|
14
|
+
"preview": "stone preview",
|
|
15
|
+
"build:ssr": "stone build --ssr",
|
|
16
|
+
"test": "stone test",
|
|
17
|
+
"test:cvg": "stone test --coverage"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@acme/domain": "workspace:*",
|
|
21
|
+
"@stone-js/browser-adapter": "^0.8.13",
|
|
22
|
+
"@stone-js/core": "^0.8.13",
|
|
23
|
+
"@stone-js/router": "^0.8.13",
|
|
24
|
+
"@stone-js/use-react": "^0.8.13",
|
|
25
|
+
"react": "^19.0.0",
|
|
26
|
+
"react-dom": "^19.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@babel/plugin-proposal-decorators": "^7.25.9",
|
|
30
|
+
"@babel/preset-env": "^7.26.9",
|
|
31
|
+
"@babel/preset-typescript": "^7.27.0",
|
|
32
|
+
"@stone-js/cli": "^0.8.13",
|
|
33
|
+
"@stone-js/http-core": "^0.8.13",
|
|
34
|
+
"@stone-js/testing": "^0.8.13",
|
|
35
|
+
"@types/node": "^24.0.7",
|
|
36
|
+
"@types/react": "^19.0.7",
|
|
37
|
+
"@types/react-dom": "^19.0.3",
|
|
38
|
+
"@vitest/coverage-v8": "^3.0.9",
|
|
39
|
+
"vitest": "^3.0.9"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="Stone.js">
|
|
2
|
+
<title>Stone.js</title>
|
|
3
|
+
<style>
|
|
4
|
+
.ink { stroke: #1B1613; }
|
|
5
|
+
@media (prefers-color-scheme: dark) { .ink { stroke: #EFE8DC; } }
|
|
6
|
+
</style>
|
|
7
|
+
<defs>
|
|
8
|
+
<linearGradient id="braise" gradientUnits="userSpaceOnUse" x1="22" y1="14" x2="74" y2="26.1">
|
|
9
|
+
<stop offset="0" stop-color="#FFC46B" />
|
|
10
|
+
<stop offset="1" stop-color="#FF5A1F" />
|
|
11
|
+
</linearGradient>
|
|
12
|
+
</defs>
|
|
13
|
+
<path class="ink" d="M 79.96 36.4 A 34 34 0 0 1 53.9 81.5" fill="none" stroke-width="11" stroke-linecap="round" opacity=".85" />
|
|
14
|
+
<path class="ink" d="M 42.1 81.5 A 34 34 0 0 1 16.05 36.4" fill="none" stroke-width="11" stroke-linecap="round" opacity=".85" />
|
|
15
|
+
<path d="M 22 26.1 A 34 34 0 0 1 74 26.1" fill="none" stroke="url(#braise)" stroke-width="11" stroke-linecap="round" />
|
|
16
|
+
</svg>
|