@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
|
@@ -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,54 @@
|
|
|
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
|
+
* The mobile application, booted under Node and asked real questions.
|
|
9
|
+
*
|
|
10
|
+
* Nothing is mocked. The kernel, the router, the adapter and the renderer are the ones a phone runs,
|
|
11
|
+
* and the domain is the very same `@acme/domain` the web application uses. What a device adds is a
|
|
12
|
+
* screen to draw on, and this asserts what the framework resolved before drawing.
|
|
13
|
+
*
|
|
14
|
+
* Read it next to `apps/web/tests/TaskListPage.spec.ts`: same domain, same questions, same answers.
|
|
15
|
+
*/
|
|
16
|
+
describe('TaskListScreen', () => {
|
|
17
|
+
const screens = ScreenStack.create()
|
|
18
|
+
const navigation = NavigationSource.create({ baseUrl: 'acme://app' })
|
|
19
|
+
|
|
20
|
+
// Booted once for the whole suite, as on a device: the default blueprints are shared module
|
|
21
|
+
// objects, so booting the same modules twice in one process would accumulate route definitions.
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
await stoneApp({
|
|
24
|
+
modules: [
|
|
25
|
+
Application,
|
|
26
|
+
TaskListScreen,
|
|
27
|
+
{ stone: { reactNative: { navigationSource: navigation }, useReactNative: { screenStack: screens } } }
|
|
28
|
+
]
|
|
29
|
+
}).run()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const settle = async (): Promise<void> => { await new Promise((resolve) => setImmediate(resolve)) }
|
|
33
|
+
|
|
34
|
+
it('puts the task list on the stack at launch, counted by the shared domain', () => {
|
|
35
|
+
expect(screens.size()).toBe(1)
|
|
36
|
+
expect(screens.top()?.path).toBe('/')
|
|
37
|
+
expect(screens.top()?.title).toBe('1 left · Acme tasks')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('toggles a task through the same domain the web app uses', async () => {
|
|
41
|
+
navigation.navigate('acme://app/?toggle=3')
|
|
42
|
+
await settle()
|
|
43
|
+
|
|
44
|
+
expect(screens.top()?.title).toBe('0 left · Acme tasks')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('surfaces an unknown route instead of crashing', async () => {
|
|
48
|
+
navigation.navigate('acme://app/nowhere')
|
|
49
|
+
await settle()
|
|
50
|
+
|
|
51
|
+
// An error page is still a screen: the application stays up and shows something.
|
|
52
|
+
expect(screens.top()?.path).not.toBe('/')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -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,35 @@
|
|
|
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
|
+
globals: true,
|
|
26
|
+
environment: 'node',
|
|
27
|
+
include: ['./tests/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
|
|
28
|
+
coverage: {
|
|
29
|
+
provider: 'v8',
|
|
30
|
+
include: ['app/**/*.{ts,tsx}'],
|
|
31
|
+
reporter: ['text', 'html'],
|
|
32
|
+
reportsDirectory: './coverage'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
})
|
|
@@ -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,40 @@
|
|
|
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/testing": "^0.8.13",
|
|
34
|
+
"@types/node": "^24.0.7",
|
|
35
|
+
"@types/react": "^19.0.7",
|
|
36
|
+
"@types/react-dom": "^19.0.3",
|
|
37
|
+
"@vitest/coverage-v8": "^3.0.9",
|
|
38
|
+
"vitest": "^3.0.9"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -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>
|
|
@@ -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>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createTestApp, makeIncomingHttpEvent } from '@stone-js/testing'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The web application, booted in memory and asked a real question.
|
|
5
|
+
*
|
|
6
|
+
* Nothing is mocked and nothing is listed: `createTestApp()` discovers `app/**`, and the event goes
|
|
7
|
+
* through the same kernel production runs, shared domain included.
|
|
8
|
+
*
|
|
9
|
+
* Read this next to `apps/mobile/tests/TaskListScreen.spec.ts`. Same domain, same assertions about
|
|
10
|
+
* what it resolved; what differs is only how each platform delivers the result.
|
|
11
|
+
*/
|
|
12
|
+
describe('TaskListPage', () => {
|
|
13
|
+
it('renders the task list from the shared domain', async () => {
|
|
14
|
+
const app = await createTestApp()
|
|
15
|
+
|
|
16
|
+
const response = await app.send(makeIncomingHttpEvent({ method: 'GET', url: '/' }))
|
|
17
|
+
|
|
18
|
+
expect(response.statusCode).toBe(200)
|
|
19
|
+
expect(response.html()).toContain('Run the same code on a phone')
|
|
20
|
+
expect(response.html()).toContain('Acme tasks')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('toggles a task through the domain, and says how many are left', async () => {
|
|
24
|
+
const app = await createTestApp()
|
|
25
|
+
|
|
26
|
+
// Asserted through the head rather than the body: React's server renderer splits the text
|
|
27
|
+
// around `{data.remaining}` into separate nodes, so the body reads `1<!-- --> left to do`.
|
|
28
|
+
// `head` builds one string, which is also what a browser tab and a navigator show.
|
|
29
|
+
const before = await app.send(makeIncomingHttpEvent({ method: 'GET', url: '/' }))
|
|
30
|
+
expect(before.html()).toContain('<title>1 left · Acme tasks</title>')
|
|
31
|
+
|
|
32
|
+
const after = await app.send(makeIncomingHttpEvent({ method: 'GET', url: '/?toggle=3' }))
|
|
33
|
+
|
|
34
|
+
expect(after.statusCode).toBe(200)
|
|
35
|
+
expect(after.html()).toContain('<title>0 left · Acme tasks</title>')
|
|
36
|
+
})
|
|
37
|
+
})
|