@finesoft/create-app 0.1.34 → 0.1.36
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/dist/index.mjs +22 -26
- package/package.json +14 -5
- package/templates/react/README.md +35 -0
- package/templates/react/index.html +2 -3
- package/templates/react/package.json +12 -8
- package/templates/react/src/App.tsx +7 -15
- package/templates/react/src/app-definition.ts +35 -0
- package/templates/react/src/components/Layout.tsx +5 -52
- package/templates/react/src/components/Loading.tsx +1 -1
- package/templates/react/src/components/Navigation.tsx +10 -37
- package/templates/react/src/components/ProductCard.tsx +5 -23
- package/templates/react/src/config.ts +2 -0
- package/templates/react/src/lib/controllers/about.ts +18 -13
- package/templates/react/src/lib/controllers/error.ts +11 -7
- package/templates/react/src/lib/controllers/home.ts +36 -41
- package/templates/react/src/lib/controllers/product-detail.ts +71 -40
- package/templates/react/src/lib/controllers/search.ts +33 -54
- package/templates/react/src/lib/data/products.ts +36 -0
- package/templates/react/src/lib/mappers/product.ts +3 -3
- package/templates/react/src/lib/models/product.ts +11 -1
- package/templates/react/src/lib/services/api-client.ts +7 -4
- package/templates/react/src/main.ts +34 -0
- package/templates/react/src/pages/About.tsx +2 -4
- package/templates/react/src/pages/Home.tsx +6 -15
- package/templates/react/src/pages/NotFound.tsx +4 -14
- package/templates/react/src/pages/ProductDetail.tsx +5 -23
- package/templates/react/src/pages/Search.tsx +5 -7
- package/templates/react/src/ssr.ts +11 -0
- package/templates/react/src/styles.css +71 -0
- package/templates/react/src/views.ts +12 -0
- package/templates/react/tsconfig.json +14 -9
- package/templates/react/vite.config.ts +9 -13
- package/templates/react-minimal/README.md +39 -0
- package/templates/react-minimal/index.html +2 -3
- package/templates/react-minimal/package.json +12 -8
- package/templates/react-minimal/src/App.tsx +40 -85
- package/templates/react-minimal/src/app-definition.ts +48 -0
- package/templates/react-minimal/src/config.ts +2 -0
- package/templates/react-minimal/src/lib/controllers/detail.ts +19 -18
- package/templates/react-minimal/src/lib/controllers/error.ts +15 -0
- package/templates/react-minimal/src/lib/controllers/home.ts +19 -24
- package/templates/react-minimal/src/lib/controllers/notes.ts +7 -5
- package/templates/react-minimal/src/lib/locale.ts +16 -0
- package/templates/react-minimal/src/lib/models/page.ts +27 -0
- package/templates/react-minimal/src/lib/navigation.ts +11 -0
- package/templates/react-minimal/src/locales/en-US.json +5 -0
- package/templates/react-minimal/src/locales/zh-Hans.json +5 -0
- package/templates/react-minimal/src/main.ts +43 -0
- package/templates/react-minimal/src/pages/Detail.tsx +20 -0
- package/templates/react-minimal/src/pages/Home.tsx +38 -0
- package/templates/react-minimal/src/pages/NotFound.tsx +11 -0
- package/templates/react-minimal/src/pages/Notes.tsx +21 -0
- package/templates/react-minimal/src/ssr.ts +11 -0
- package/templates/react-minimal/src/styles.css +74 -0
- package/templates/react-minimal/src/views.ts +6 -0
- package/templates/react-minimal/src/vite-env.d.ts +3 -0
- package/templates/react-minimal/tsconfig.json +14 -9
- package/templates/react-minimal/vite.config.ts +9 -7
- package/templates/svelte/README.md +35 -0
- package/templates/svelte/index.html +1 -2
- package/templates/svelte/package.json +9 -5
- package/templates/svelte/src/App.svelte +8 -80
- package/templates/svelte/src/app-definition.ts +35 -0
- package/templates/svelte/src/components/Layout.svelte +2 -2
- package/templates/svelte/src/components/Loading.svelte +1 -1
- package/templates/svelte/src/components/Navigation.svelte +5 -66
- package/templates/svelte/src/components/ProductCard.svelte +3 -24
- package/templates/svelte/src/config.ts +2 -0
- package/templates/svelte/src/lib/controllers/about.ts +18 -13
- package/templates/svelte/src/lib/controllers/error.ts +11 -7
- package/templates/svelte/src/lib/controllers/home.ts +36 -41
- package/templates/svelte/src/lib/controllers/product-detail.ts +71 -40
- package/templates/svelte/src/lib/controllers/search.ts +33 -54
- package/templates/svelte/src/lib/data/products.ts +36 -0
- package/templates/svelte/src/lib/mappers/product.ts +3 -3
- package/templates/svelte/src/lib/models/product.ts +12 -1
- package/templates/svelte/src/lib/services/api-client.ts +7 -4
- package/templates/svelte/src/main.ts +28 -24
- package/templates/svelte/src/pages/About.svelte +2 -2
- package/templates/svelte/src/pages/Home.svelte +3 -15
- package/templates/svelte/src/pages/NotFound.svelte +3 -12
- package/templates/svelte/src/pages/ProductDetail.svelte +4 -21
- package/templates/svelte/src/pages/Search.svelte +2 -10
- package/templates/svelte/src/ssr.ts +10 -9
- package/templates/svelte/src/styles.css +71 -0
- package/templates/svelte/src/views.ts +12 -0
- package/templates/svelte/svelte.config.js +2 -0
- package/templates/svelte/tsconfig.json +13 -8
- package/templates/svelte/vite.config.ts +9 -13
- package/templates/svelte-minimal/README.md +39 -0
- package/templates/svelte-minimal/index.html +1 -2
- package/templates/svelte-minimal/package.json +9 -5
- package/templates/svelte-minimal/src/App.svelte +49 -61
- package/templates/svelte-minimal/src/app-definition.ts +48 -0
- package/templates/svelte-minimal/src/config.ts +2 -0
- package/templates/svelte-minimal/src/lib/controllers/detail.ts +23 -0
- package/templates/svelte-minimal/src/lib/controllers/error.ts +15 -0
- package/templates/svelte-minimal/src/lib/controllers/home.ts +24 -23
- package/templates/svelte-minimal/src/lib/controllers/notes.ts +18 -0
- package/templates/svelte-minimal/src/lib/locale.ts +16 -0
- package/templates/svelte-minimal/src/lib/models/page.ts +27 -0
- package/templates/svelte-minimal/src/lib/navigation.ts +11 -0
- package/templates/svelte-minimal/src/locales/en-US.json +1 -3
- package/templates/svelte-minimal/src/locales/zh-Hans.json +1 -3
- package/templates/svelte-minimal/src/main.ts +37 -19
- package/templates/svelte-minimal/src/pages/Detail.svelte +11 -0
- package/templates/svelte-minimal/src/pages/Home.svelte +17 -21
- package/templates/svelte-minimal/src/pages/NotFound.svelte +10 -0
- package/templates/svelte-minimal/src/pages/Notes.svelte +11 -0
- package/templates/svelte-minimal/src/ssr.ts +9 -17
- package/templates/svelte-minimal/src/styles.css +74 -0
- package/templates/svelte-minimal/src/views.ts +6 -0
- package/templates/svelte-minimal/src/vite-env.d.ts +3 -0
- package/templates/svelte-minimal/svelte.config.js +2 -0
- package/templates/svelte-minimal/tsconfig.json +13 -8
- package/templates/svelte-minimal/vite.config.ts +9 -10
- package/templates/vue/README.md +35 -0
- package/templates/vue/index.html +1 -2
- package/templates/vue/package.json +9 -5
- package/templates/vue/src/App.vue +7 -22
- package/templates/vue/src/app-definition.ts +35 -0
- package/templates/vue/src/components/Layout.vue +4 -6
- package/templates/vue/src/components/Loading.vue +1 -1
- package/templates/vue/src/components/Navigation.vue +10 -43
- package/templates/vue/src/components/ProductCard.vue +4 -28
- package/templates/vue/src/config.ts +2 -0
- package/templates/vue/src/lib/controllers/about.ts +18 -13
- package/templates/vue/src/lib/controllers/error.ts +11 -7
- package/templates/vue/src/lib/controllers/home.ts +36 -41
- package/templates/vue/src/lib/controllers/product-detail.ts +71 -40
- package/templates/vue/src/lib/controllers/search.ts +33 -54
- package/templates/vue/src/lib/data/products.ts +36 -0
- package/templates/vue/src/lib/mappers/product.ts +3 -3
- package/templates/vue/src/lib/models/product.ts +12 -1
- package/templates/vue/src/lib/services/api-client.ts +7 -7
- package/templates/vue/src/main.ts +28 -43
- package/templates/vue/src/pages/About.vue +3 -5
- package/templates/vue/src/pages/Home.vue +5 -23
- package/templates/vue/src/pages/NotFound.vue +4 -13
- package/templates/vue/src/pages/ProductDetail.vue +5 -23
- package/templates/vue/src/pages/Search.vue +4 -18
- package/templates/vue/src/ssr.ts +5 -18
- package/templates/vue/src/styles.css +71 -0
- package/templates/vue/src/views.ts +12 -0
- package/templates/vue/tsconfig.json +14 -9
- package/templates/vue/vite.config.ts +9 -13
- package/templates/vue-minimal/README.md +39 -0
- package/templates/vue-minimal/index.html +1 -2
- package/templates/vue-minimal/package.json +9 -5
- package/templates/vue-minimal/src/App.vue +46 -43
- package/templates/vue-minimal/src/app-definition.ts +48 -0
- package/templates/vue-minimal/src/config.ts +2 -0
- package/templates/vue-minimal/src/lib/controllers/detail.ts +19 -18
- package/templates/vue-minimal/src/lib/controllers/error.ts +15 -0
- package/templates/vue-minimal/src/lib/controllers/home.ts +19 -24
- package/templates/vue-minimal/src/lib/controllers/notes.ts +7 -5
- package/templates/vue-minimal/src/lib/locale.ts +16 -0
- package/templates/vue-minimal/src/lib/models/page.ts +27 -0
- package/templates/vue-minimal/src/lib/navigation.ts +11 -0
- package/templates/vue-minimal/src/locales/en-US.json +5 -0
- package/templates/vue-minimal/src/locales/zh-Hans.json +5 -0
- package/templates/vue-minimal/src/main.ts +35 -70
- package/templates/vue-minimal/src/pages/Detail.vue +20 -0
- package/templates/vue-minimal/src/pages/Home.vue +34 -0
- package/templates/vue-minimal/src/pages/NotFound.vue +13 -0
- package/templates/vue-minimal/src/pages/Notes.vue +22 -0
- package/templates/vue-minimal/src/ssr.ts +7 -46
- package/templates/vue-minimal/src/styles.css +74 -0
- package/templates/vue-minimal/src/views.ts +6 -0
- package/templates/vue-minimal/src/vite-env.d.ts +3 -0
- package/templates/vue-minimal/tsconfig.json +14 -9
- package/templates/vue-minimal/vite.config.ts +9 -7
- package/templates/react/src/actions.ts +0 -14
- package/templates/react/src/bootstrap.ts +0 -36
- package/templates/react/src/components/PageRenderer.tsx +0 -41
- package/templates/react/src/main.tsx +0 -49
- package/templates/react/src/ssr.tsx +0 -23
- package/templates/react-minimal/src/bootstrap.ts +0 -39
- package/templates/react-minimal/src/main.tsx +0 -99
- package/templates/react-minimal/src/ssr.tsx +0 -51
- package/templates/react-minimal/src/views/DetailView.tsx +0 -28
- package/templates/react-minimal/src/views/HomeView.tsx +0 -27
- package/templates/react-minimal/src/views/NotesView.tsx +0 -11
- package/templates/svelte/src/actions.ts +0 -14
- package/templates/svelte/src/bootstrap.ts +0 -36
- package/templates/svelte/src/components/PageRenderer.svelte +0 -22
- package/templates/svelte/src/lib/framework-svelte.ts +0 -34
- package/templates/svelte/src/lib/render.ts +0 -15
- package/templates/svelte-minimal/src/bootstrap.ts +0 -6
- package/templates/svelte-minimal/src/lib/framework-svelte.ts +0 -34
- package/templates/svelte-minimal/src/lib/render.ts +0 -12
- package/templates/vue/src/actions.ts +0 -14
- package/templates/vue/src/bootstrap.ts +0 -42
- package/templates/vue/src/components/PageRenderer.vue +0 -26
- package/templates/vue-minimal/src/bootstrap.ts +0 -39
- package/templates/vue-minimal/src/views/DetailView.vue +0 -29
- package/templates/vue-minimal/src/views/HomeView.vue +0 -26
- package/templates/vue-minimal/src/views/NotesView.vue +0 -12
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color: #333;
|
|
3
|
+
font-family: Arial, sans-serif;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
body {
|
|
7
|
+
margin: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.app-shell {
|
|
11
|
+
min-height: 100vh;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.navigation {
|
|
15
|
+
display: flex;
|
|
16
|
+
gap: 1rem;
|
|
17
|
+
padding: 1rem;
|
|
18
|
+
border-bottom: 1px solid #eee;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.navigation-link {
|
|
22
|
+
color: #333;
|
|
23
|
+
text-decoration: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.navigation-link:hover,
|
|
27
|
+
.navigation-link.active {
|
|
28
|
+
color: #007bff;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.navigation-link.active {
|
|
32
|
+
font-weight: bold;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.app-main {
|
|
36
|
+
padding: 1rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.loading {
|
|
40
|
+
padding: 2rem;
|
|
41
|
+
color: #999;
|
|
42
|
+
text-align: center;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.shelf,
|
|
46
|
+
.results {
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-wrap: wrap;
|
|
49
|
+
gap: 1rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.shelf.horizontal {
|
|
53
|
+
overflow-x: auto;
|
|
54
|
+
flex-wrap: nowrap;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.product-card {
|
|
58
|
+
min-width: 200px;
|
|
59
|
+
padding: 1rem;
|
|
60
|
+
border: 1px solid #eee;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.price {
|
|
65
|
+
color: #007bff;
|
|
66
|
+
font-weight: bold;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.price-large {
|
|
70
|
+
font-size: 1.5rem;
|
|
71
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import About from "./pages/About.svelte";
|
|
2
|
+
import Home from "./pages/Home.svelte";
|
|
3
|
+
import NotFound from "./pages/NotFound.svelte";
|
|
4
|
+
import ProductDetail from "./pages/ProductDetail.svelte";
|
|
5
|
+
import Search from "./pages/Search.svelte";
|
|
6
|
+
export const views = {
|
|
7
|
+
home: Home,
|
|
8
|
+
product: ProductDetail,
|
|
9
|
+
search: Search,
|
|
10
|
+
about: About,
|
|
11
|
+
error: NotFound,
|
|
12
|
+
};
|
|
@@ -4,19 +4,24 @@
|
|
|
4
4
|
"target": "ESNext",
|
|
5
5
|
"module": "ESNext",
|
|
6
6
|
"moduleResolution": "bundler",
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
7
11
|
"esModuleInterop": true,
|
|
8
12
|
"forceConsistentCasingInFileNames": true,
|
|
9
13
|
"skipLibCheck": true,
|
|
10
14
|
"noEmit": true,
|
|
15
|
+
"rootDir": ".",
|
|
11
16
|
"paths": {
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"@finesoft/core": ["../../packages/core/src/index.ts"],
|
|
16
|
-
"@finesoft/browser": ["../../packages/browser/src/index.ts"],
|
|
17
|
-
"@finesoft/ssr": ["../../packages/ssr/src/index.ts"],
|
|
18
|
-
"@finesoft/server": ["../../packages/server/src/index.ts"]
|
|
17
|
+
"@finesoft/front": [
|
|
18
|
+
"./.finesoft/front.d.ts"
|
|
19
|
+
]
|
|
19
20
|
}
|
|
20
21
|
},
|
|
21
|
-
"include": [
|
|
22
|
+
"include": [
|
|
23
|
+
"src/**/*",
|
|
24
|
+
"env.d.ts",
|
|
25
|
+
"vite.config.ts"
|
|
26
|
+
]
|
|
22
27
|
}
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import { finesoftFrontViteConfig } from "@finesoft/front";
|
|
2
2
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
|
3
|
-
import { defineConfig } from "vite-plus";
|
|
3
|
+
import { defineConfig, lazyPlugins } from "vite-plus";
|
|
4
|
+
|
|
5
|
+
const front = finesoftFrontViteConfig({
|
|
6
|
+
controllerTypes: { root: import.meta.dirname },
|
|
7
|
+
ssr: { entry: "src/ssr.ts" },
|
|
8
|
+
proxies: [{ prefix: "/api", target: "https://jsonplaceholder.typicode.com" }],
|
|
9
|
+
});
|
|
4
10
|
|
|
5
11
|
export default defineConfig({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
finesoftFrontViteConfig({
|
|
9
|
-
ssr: { entry: "src/ssr.ts" },
|
|
10
|
-
proxies: [
|
|
11
|
-
{
|
|
12
|
-
prefix: "/api",
|
|
13
|
-
target: "https://jsonplaceholder.typicode.com",
|
|
14
|
-
},
|
|
15
|
-
],
|
|
16
|
-
}),
|
|
17
|
-
],
|
|
12
|
+
lint: { options: { typeAware: true, typeCheck: true } },
|
|
13
|
+
plugins: lazyPlugins(() => [front, svelte()]),
|
|
18
14
|
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Finesoft Front minimal application
|
|
2
|
+
|
|
3
|
+
Feed, detail and Notes demonstrate tabs/stacks, retained drafts, a per-application profile and session restoration. All three native frameworks have the same example.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
Node must satisfy `^22.18.0 || >=24.11.0`. Use Vite+ for tooling.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
vp install
|
|
11
|
+
vp run dev
|
|
12
|
+
vp run build
|
|
13
|
+
vp run preview
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Visit `/`, open a Feed item, enter a detail note, switch to Notes and type a draft. Switch tabs and reload to see both drafts restored. Use Back to remove the detail entry; opening it again starts a fresh draft. Your name is global to this application and is saved on blur.
|
|
17
|
+
|
|
18
|
+
## Server controller example
|
|
19
|
+
|
|
20
|
+
`src/lib/controllers/detail.ts` extends `BaseServerController` from `@finesoft/front`. Open `/item/2` and inspect the response header `x-demo-request-method: GET`. Return to Feed and open another item: browser navigation loads the controller remotely using POST, whose response contains `x-demo-request-method: POST`. This demonstrates reading `context.request` and explicitly writing `context.responseHeaders` without adding an authentication policy.
|
|
21
|
+
|
|
22
|
+
SSR executes the controller directly, and hydration reuses the result. Its implementation stays on the server; `markPublic` selects the data sent to the native view. Home and Notes remain ordinary `BaseController` examples. Route types come from `src/app-definition.ts`; the Vite plugin maintains the generated `Input` type with the appropriate server context.
|
|
23
|
+
|
|
24
|
+
Keep the configured SSR entry and deploy to a request host such as Node or Worker. Serving only `dist/client` cannot execute this controller. Login state and any credential forwarding remain application-owned.
|
|
25
|
+
|
|
26
|
+
## Edit
|
|
27
|
+
|
|
28
|
+
- `src/config.ts`: application identity and persistence namespace.
|
|
29
|
+
- `src/app-definition.ts`: pages, routes, guards and navigation.
|
|
30
|
+
- `src/lib/controllers/` and `src/lib/models/`: page loading and typed public data.
|
|
31
|
+
- `src/pages/`, `src/App.*`, `src/styles.css`: native UI and presentation.
|
|
32
|
+
- `src/views.ts`: the view registry shared by browser and server.
|
|
33
|
+
- `src/main.ts` and `src/ssr.ts`: standard browser and SSR startup.
|
|
34
|
+
|
|
35
|
+
`src/instance.ts` creates one profile store/provider per mount. For an embedded application, call `mountApplication(target, uniquePersistenceKey, "memory", "/notes")`. The `data-restore-root` fields belong to their page entries.
|
|
36
|
+
|
|
37
|
+
`src/locales/` contains English and Chinese JSON messages. Set `frameworkConfig.locale` in `src/app-definition.ts` to `en-US` or `zh-Hans` (default), then restart/rebuild. Navigation labels and sample items remain English.
|
|
38
|
+
|
|
39
|
+
All framework APIs import from `@finesoft/front`. Select the native component with `Outlet("react")`, `Outlet("vue")`, or `Outlet("svelte")`, and pass the same renderer to `useSnapshot`. The plugin maintains `.finesoft/front.d.ts` and the exact TypeScript import mapping; install only the chosen renderer. For standalone TypeScript checks, first run `vp exec finesoft-types`. The project is standalone and requires no sibling templates or private runtime packages.
|
|
@@ -8,12 +8,16 @@
|
|
|
8
8
|
"preview": "vp preview"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@finesoft/front": "^0.5.
|
|
12
|
-
"svelte": "^5.
|
|
11
|
+
"@finesoft/front": "^0.5.3",
|
|
12
|
+
"svelte": "^5.56.8"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"@
|
|
16
|
-
"vite": "
|
|
17
|
-
"
|
|
15
|
+
"@hono/node-server": "^2.1.0",
|
|
16
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
17
|
+
"@types/node": "^22.20.1",
|
|
18
|
+
"hono": "^4.13.1",
|
|
19
|
+
"typescript": "^6.0.3",
|
|
20
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.8",
|
|
21
|
+
"vite-plus": "0.2.8"
|
|
18
22
|
}
|
|
19
23
|
}
|
|
@@ -1,63 +1,51 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
title: "Error",
|
|
27
|
-
description:
|
|
28
|
-
err instanceof Error ? err.message : "Failed to load page",
|
|
29
|
-
}),
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 当前页面数据:
|
|
35
|
-
* - BasePage: SSR 直出 或 已完成的导航
|
|
36
|
-
* - Promise<BasePage>: SPA 导航中
|
|
37
|
-
* - null: 初始空白态
|
|
38
|
-
*/
|
|
39
|
-
let currentPage = $state<Promise<BasePage> | BasePage | null>(
|
|
40
|
-
untrack(() => props.page ?? null),
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
/** 由 main.ts 的 mount 回调调用,驱动 SPA 导航 */
|
|
44
|
-
export function updatePage(newPage: Promise<BasePage> | BasePage): void {
|
|
45
|
-
if (newPage instanceof Promise) {
|
|
46
|
-
currentPage = wrapError(newPage);
|
|
47
|
-
} else {
|
|
48
|
-
currentPage = newPage;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
2
|
+
import { Outlet as selectOutlet, useSnapshot, type WebAppView } from "@finesoft/front";
|
|
3
|
+
import { onMount, untrack } from "svelte";
|
|
4
|
+
import { TAB_LABELS } from "./lib/navigation";
|
|
5
|
+
import { views } from "./views";
|
|
6
|
+
const Outlet = selectOutlet("svelte");
|
|
7
|
+
let { app }: { app: WebAppView } = $props();
|
|
8
|
+
const snapshot = untrack(() => useSnapshot("svelte", app));
|
|
9
|
+
let name = $state("");
|
|
10
|
+
onMount(() => app.session?.register({
|
|
11
|
+
key: "profile",
|
|
12
|
+
version: 1,
|
|
13
|
+
decode: (data) => {
|
|
14
|
+
if (
|
|
15
|
+
!data ||
|
|
16
|
+
typeof data !== "object" ||
|
|
17
|
+
typeof (data as { name?: unknown }).name !== "string"
|
|
18
|
+
) throw Error("invalid-profile-state");
|
|
19
|
+
return data as { name: string };
|
|
20
|
+
},
|
|
21
|
+
capture: () => ({ name }),
|
|
22
|
+
restore: (data) => {
|
|
23
|
+
name = data.name;
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
51
26
|
</script>
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
27
|
+
<div class="app-chrome">
|
|
28
|
+
<header class="profile">
|
|
29
|
+
<label>
|
|
30
|
+
Your name (global):
|
|
31
|
+
<input bind:value={name} placeholder="anon" onblur={() => app.session?.save()} />
|
|
32
|
+
</label>
|
|
33
|
+
{#if name}<span>👋 {name}</span>{/if}
|
|
34
|
+
</header>
|
|
35
|
+
{#if $snapshot.navigation.tabs}
|
|
36
|
+
<nav class="tabs" aria-label="Pages">
|
|
37
|
+
{#each $snapshot.navigation.tabs.order as key (key)}
|
|
38
|
+
<button
|
|
39
|
+
aria-current={key === $snapshot.navigation.tabs.active}
|
|
40
|
+
onclick={() => app.perform({ kind: "selectTab", key })}
|
|
41
|
+
>
|
|
42
|
+
{TAB_LABELS[key] ?? key}
|
|
43
|
+
</button>
|
|
44
|
+
{/each}
|
|
45
|
+
</nav>
|
|
46
|
+
{/if}
|
|
47
|
+
{#if $snapshot.navigation.canGoBack}
|
|
48
|
+
<button onclick={() => app.perform({ kind: "pop" })}>← Back</button>
|
|
49
|
+
{/if}
|
|
50
|
+
<Outlet {app} {views} />
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { loadMessages } from "virtual:finesoft-front/i18n-loader";
|
|
2
|
+
import { appId } from "./config";
|
|
3
|
+
import { getErrorPage } from "./lib/controllers/error";
|
|
4
|
+
import {
|
|
5
|
+
definePage,
|
|
6
|
+
defineWebApp,
|
|
7
|
+
stack,
|
|
8
|
+
tabs,
|
|
9
|
+
type LeafNode,
|
|
10
|
+
type RouteMatch,
|
|
11
|
+
} from "@finesoft/front";
|
|
12
|
+
import { DetailController } from "./lib/controllers/detail";
|
|
13
|
+
import { HomeController } from "./lib/controllers/home";
|
|
14
|
+
import { NotesController } from "./lib/controllers/notes";
|
|
15
|
+
export const homePage = definePage({
|
|
16
|
+
id: "home",
|
|
17
|
+
create: () => new HomeController(),
|
|
18
|
+
routes: ["/"],
|
|
19
|
+
});
|
|
20
|
+
export const detailPage = definePage({
|
|
21
|
+
id: "detail",
|
|
22
|
+
create: () => new DetailController(),
|
|
23
|
+
routes: ["/item/:id"],
|
|
24
|
+
});
|
|
25
|
+
export const notesPage = definePage({
|
|
26
|
+
id: "notes",
|
|
27
|
+
create: () => new NotesController(),
|
|
28
|
+
routes: ["/notes"],
|
|
29
|
+
});
|
|
30
|
+
export function createNavigation({ match, target }: { match?: RouteMatch; target?: LeafNode }) {
|
|
31
|
+
if (!match || !target) return undefined;
|
|
32
|
+
const home =
|
|
33
|
+
match.intent.id === "detail" ? stack([homePage.leaf(), target]) : stack(homePage.leaf());
|
|
34
|
+
return tabs({
|
|
35
|
+
active: match.intent.id === "notes" ? "notes" : "home",
|
|
36
|
+
branches: { home, notes: stack(match.intent.id === "notes" ? target : notesPage.leaf()) },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const app = defineWebApp({
|
|
41
|
+
id: appId,
|
|
42
|
+
loadMessages,
|
|
43
|
+
configuration: { locale: "zh-Hans" },
|
|
44
|
+
pages: [homePage, detailPage, notesPage],
|
|
45
|
+
getErrorPage,
|
|
46
|
+
|
|
47
|
+
navigation: createNavigation,
|
|
48
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BaseServerController } from "@finesoft/front";
|
|
2
|
+
import { markPublic } from "@finesoft/front";
|
|
3
|
+
import type { DetailPage } from "../models/page";
|
|
4
|
+
import type { DetailControllerInput as Input } from "../../../.finesoft/controller-types";
|
|
5
|
+
|
|
6
|
+
export class DetailController extends BaseServerController<Input, DetailPage> {
|
|
7
|
+
execute({ params, context }: Input): DetailPage {
|
|
8
|
+
const id = params.id ?? "?";
|
|
9
|
+
// GET during SSR, POST when browser navigation loads this controller remotely.
|
|
10
|
+
context.responseHeaders.set("x-demo-request-method", context.request.method);
|
|
11
|
+
return markPublic(
|
|
12
|
+
{
|
|
13
|
+
id: `detail-${id}`,
|
|
14
|
+
pageType: "detail",
|
|
15
|
+
url: `/item/${id}`,
|
|
16
|
+
title: `Item ${id}`,
|
|
17
|
+
description: "A pushed detail screen. Its scoped note is lost once you pop it.",
|
|
18
|
+
itemId: id,
|
|
19
|
+
},
|
|
20
|
+
["itemId"],
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { markPublic } from "@finesoft/front";
|
|
2
|
+
import type { ErrorPage } from "../models/page";
|
|
3
|
+
|
|
4
|
+
export function getErrorPage(status: number, message: string): ErrorPage {
|
|
5
|
+
return markPublic(
|
|
6
|
+
{
|
|
7
|
+
id: "error",
|
|
8
|
+
pageType: "error",
|
|
9
|
+
title: `Error ${status}`,
|
|
10
|
+
description: message,
|
|
11
|
+
status,
|
|
12
|
+
},
|
|
13
|
+
["status"],
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -1,27 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
type BasePage,
|
|
5
|
-
type Container,
|
|
6
|
-
type Translator,
|
|
7
|
-
} from "@finesoft/front";
|
|
1
|
+
import { BaseController, type ControllerInput } from "@finesoft/front";
|
|
2
|
+
import { markPublic } from "@finesoft/front";
|
|
3
|
+
import type { FeedItem, HomePage } from "../models/page";
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const ITEMS: readonly FeedItem[] = [
|
|
6
|
+
{ id: "1", title: "Structured navigation" },
|
|
7
|
+
{ id: "2", title: "Session restoration" },
|
|
8
|
+
{ id: "3", title: "Navigation-scoped state" },
|
|
9
|
+
];
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
11
|
+
export class HomeController extends BaseController<
|
|
12
|
+
ControllerInput<Record<string, string>>,
|
|
13
|
+
HomePage
|
|
14
|
+
> {
|
|
15
|
+
execute(): HomePage {
|
|
16
|
+
return markPublic(
|
|
17
|
+
{
|
|
18
|
+
id: "home",
|
|
19
|
+
pageType: "home",
|
|
20
|
+
url: "/",
|
|
21
|
+
title: "Feed",
|
|
22
|
+
description: "Tap an item to push a detail screen.",
|
|
23
|
+
items: ITEMS,
|
|
24
|
+
},
|
|
25
|
+
{ items: { id: true, title: true } },
|
|
26
|
+
);
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BaseController, type ControllerInput } from "@finesoft/front";
|
|
2
|
+
import type { NotesPage } from "../models/page";
|
|
3
|
+
|
|
4
|
+
/** Notes 页面:第二个 tab,演示切 tab 保活该分支的作用域状态。 */
|
|
5
|
+
export class NotesController extends BaseController<
|
|
6
|
+
ControllerInput<Record<string, string>>,
|
|
7
|
+
NotesPage
|
|
8
|
+
> {
|
|
9
|
+
execute(): NotesPage {
|
|
10
|
+
return {
|
|
11
|
+
id: "notes",
|
|
12
|
+
pageType: "notes",
|
|
13
|
+
url: "/notes",
|
|
14
|
+
title: "Notes",
|
|
15
|
+
description: "This textarea is navigation-scoped — switch tabs and it survives.",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WebAppView } from "@finesoft/front";
|
|
2
|
+
|
|
3
|
+
/** Both SSR and hydrated views read the translator from their own application. */
|
|
4
|
+
export function getHomeLocale(app?: WebAppView) {
|
|
5
|
+
const translator = app?.translator;
|
|
6
|
+
return {
|
|
7
|
+
lang: app?.locale?.lang ?? "unknown",
|
|
8
|
+
label: translator?.t("home.localeLabel") ?? "Current locale",
|
|
9
|
+
badge:
|
|
10
|
+
translator?.t("home.runtimeBadge") ??
|
|
11
|
+
"Hydration kept the translator alive on the client.",
|
|
12
|
+
hint:
|
|
13
|
+
translator?.t("home.switchHint") ??
|
|
14
|
+
"Change frameworkConfig.locale in src/app-definition.ts to load another JSON file.",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { BasePage } from "@finesoft/front";
|
|
2
|
+
|
|
3
|
+
export interface FeedItem {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly title: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface HomePage extends BasePage {
|
|
9
|
+
readonly pageType: "home";
|
|
10
|
+
readonly items: readonly FeedItem[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DetailPage extends BasePage {
|
|
14
|
+
readonly pageType: "detail";
|
|
15
|
+
readonly itemId: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface NotesPage extends BasePage {
|
|
19
|
+
readonly pageType: "notes";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ErrorPage extends BasePage {
|
|
23
|
+
readonly pageType: "error";
|
|
24
|
+
readonly status: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type AppPage = HomePage | DetailPage | NotesPage | ErrorPage;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { isStackNode, isTabsNode, type NavigationSnapshot } from "@finesoft/front";
|
|
2
|
+
|
|
3
|
+
export const TAB_LABELS: Readonly<Record<string, string>> = { home: "Feed", notes: "Notes" };
|
|
4
|
+
|
|
5
|
+
/** The renderer supplies each committed snapshot; chrome needs no second subscription. */
|
|
6
|
+
export function getNavigationChrome(snapshot?: NavigationSnapshot) {
|
|
7
|
+
const tree = snapshot?.tree;
|
|
8
|
+
const tabs = tree && isTabsNode(tree) ? { order: tree.order, active: tree.active } : null;
|
|
9
|
+
const branch = tree && isTabsNode(tree) ? tree.branches[tree.active] : null;
|
|
10
|
+
return { tabs, canGoBack: !!branch && isStackNode(branch) && branch.entries.length > 1 };
|
|
11
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"home.title": "Hello from Finesoft Front",
|
|
3
|
-
"home.description": "This page uses locale JSON files loaded by finesoftFrontViteConfig.",
|
|
4
2
|
"home.localeLabel": "Current locale",
|
|
5
3
|
"home.runtimeBadge": "Hydration kept the translator alive on the client.",
|
|
6
|
-
"home.switchHint": "Change frameworkConfig.locale in src/
|
|
4
|
+
"home.switchHint": "Change frameworkConfig.locale in src/app-definition.ts to en-US or zh-Hans to load the corresponding JSON file."
|
|
7
5
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"home.title": "你好,来自 Finesoft Front",
|
|
3
|
-
"home.description": "这个页面的文案来自 finesoftFrontViteConfig 自动加载的 JSON 翻译文件。",
|
|
4
2
|
"home.localeLabel": "当前语言",
|
|
5
3
|
"home.runtimeBadge": "浏览器接管后,翻译器仍然可用。",
|
|
6
|
-
"home.switchHint": "
|
|
4
|
+
"home.switchHint": "修改 src/app-definition.ts 中的 frameworkConfig.locale:en-US 为英文,zh-Hans 为中文。"
|
|
7
5
|
}
|
|
@@ -1,22 +1,40 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { hydrate } from "svelte";
|
|
1
|
+
import { createBrowserApp } from "@finesoft/front";
|
|
2
|
+
import { hydrate, mount, unmount } from "svelte";
|
|
3
|
+
import { app } from "./app-definition";
|
|
4
|
+
import { appId } from "./config";
|
|
3
5
|
import App from "./App.svelte";
|
|
4
|
-
import
|
|
6
|
+
import "./styles.css";
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
export async function mountApplication(
|
|
9
|
+
target: HTMLElement,
|
|
10
|
+
persistenceKey = appId,
|
|
11
|
+
history: "browser" | "memory" = "browser",
|
|
12
|
+
url?: string,
|
|
13
|
+
) {
|
|
14
|
+
const handle = await createBrowserApp({
|
|
15
|
+
definition: app,
|
|
16
|
+
target,
|
|
17
|
+
history,
|
|
18
|
+
url,
|
|
19
|
+
persistenceKey,
|
|
20
|
+
domRestore: true,
|
|
21
|
+
session: {},
|
|
22
|
+
});
|
|
23
|
+
if (!handle.shouldHydrate) target.replaceChildren();
|
|
24
|
+
const root = (handle.shouldHydrate ? hydrate : mount)(App, { target, props: { app: handle } });
|
|
25
|
+
const originalDispose = handle.dispose.bind(handle);
|
|
26
|
+
let disposal: Promise<void> | undefined;
|
|
27
|
+
Object.assign(handle, {
|
|
28
|
+
dispose: () => (disposal ??= originalDispose().finally(() => unmount(root))),
|
|
29
|
+
});
|
|
30
|
+
await handle.ready;
|
|
31
|
+
return handle;
|
|
32
|
+
}
|
|
11
33
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
onNavigate() {},
|
|
20
|
-
onModal() {},
|
|
21
|
-
},
|
|
22
|
-
});
|
|
34
|
+
export const started = mountApplication(document.getElementById("app")!);
|
|
35
|
+
|
|
36
|
+
if (import.meta.hot) {
|
|
37
|
+
import.meta.hot.dispose(async () => {
|
|
38
|
+
await (await started).dispose();
|
|
39
|
+
});
|
|
40
|
+
}
|