@finesoft/create-app 0.1.34 → 0.1.35
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
|
@@ -1,33 +1,19 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Action } from "@finesoft/front";
|
|
3
2
|
import ProductCard from "../components/ProductCard.vue";
|
|
4
3
|
import type { SearchPage } from "../lib/models/product";
|
|
5
4
|
|
|
6
|
-
const { page
|
|
5
|
+
const { page } = defineProps<{ page: SearchPage }>();
|
|
7
6
|
</script>
|
|
8
7
|
|
|
9
8
|
<template>
|
|
10
|
-
<
|
|
9
|
+
<section class="page page-search">
|
|
11
10
|
<h1>{{ page.title }}</h1>
|
|
12
11
|
<p>{{ page.description }}</p>
|
|
13
12
|
|
|
14
13
|
<div class="results">
|
|
15
|
-
<ProductCard
|
|
16
|
-
v-for="item in page.results"
|
|
17
|
-
:key="item.id"
|
|
18
|
-
:item="item"
|
|
19
|
-
:on-action="onAction"
|
|
20
|
-
/>
|
|
14
|
+
<ProductCard v-for="item in page.results" :key="item.id" :item="item" />
|
|
21
15
|
</div>
|
|
22
16
|
|
|
23
17
|
<p v-if="page.results.length === 0">No products found.</p>
|
|
24
|
-
</
|
|
18
|
+
</section>
|
|
25
19
|
</template>
|
|
26
|
-
|
|
27
|
-
<style scoped>
|
|
28
|
-
.results {
|
|
29
|
-
display: flex;
|
|
30
|
-
gap: 1rem;
|
|
31
|
-
flex-wrap: wrap;
|
|
32
|
-
}
|
|
33
|
-
</style>
|
package/templates/vue/src/ssr.ts
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
|
-
import { createSSRRender, serializeServerData } from "@finesoft/front";
|
|
2
1
|
import { createSSRApp } from "vue";
|
|
3
2
|
import { renderToString } from "vue/server-renderer";
|
|
3
|
+
import { createSSRRender } from "@finesoft/front";
|
|
4
|
+
import { app } from "./app-definition";
|
|
4
5
|
import App from "./App.vue";
|
|
5
|
-
import { bootstrap } from "./bootstrap";
|
|
6
|
-
import { getErrorPage } from "./lib/controllers/error";
|
|
7
6
|
|
|
8
7
|
export const render = createSSRRender({
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
async renderApp(page, _framework) {
|
|
12
|
-
const app = createSSRApp(App, { page });
|
|
13
|
-
const html = await renderToString(app);
|
|
14
|
-
return {
|
|
15
|
-
html,
|
|
16
|
-
head: `<title>${page.title}</title><meta name="description" content="${
|
|
17
|
-
page.description ?? ""
|
|
18
|
-
}">`,
|
|
19
|
-
css: "",
|
|
20
|
-
};
|
|
21
|
-
},
|
|
8
|
+
definition: app,
|
|
9
|
+
render: (app) => renderToString(createSSRApp(App, { app })),
|
|
22
10
|
});
|
|
23
|
-
|
|
24
|
-
export { serializeServerData };
|
|
11
|
+
export { serializeServerData } from "@finesoft/front";
|
|
@@ -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.vue";
|
|
2
|
+
import Home from "./pages/Home.vue";
|
|
3
|
+
import NotFound from "./pages/NotFound.vue";
|
|
4
|
+
import ProductDetail from "./pages/ProductDetail.vue";
|
|
5
|
+
import Search from "./pages/Search.vue";
|
|
6
|
+
export const views = {
|
|
7
|
+
home: Home,
|
|
8
|
+
product: ProductDetail,
|
|
9
|
+
search: Search,
|
|
10
|
+
about: About,
|
|
11
|
+
error: NotFound,
|
|
12
|
+
};
|
|
@@ -4,20 +4,25 @@
|
|
|
4
4
|
"target": "ESNext",
|
|
5
5
|
"module": "ESNext",
|
|
6
6
|
"moduleResolution": "bundler",
|
|
7
|
-
"
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
8
11
|
"esModuleInterop": true,
|
|
9
12
|
"forceConsistentCasingInFileNames": true,
|
|
10
13
|
"skipLibCheck": true,
|
|
11
14
|
"noEmit": true,
|
|
15
|
+
"rootDir": ".",
|
|
16
|
+
"jsx": "preserve",
|
|
12
17
|
"paths": {
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"@finesoft/core": ["../../packages/core/src/index.ts"],
|
|
17
|
-
"@finesoft/browser": ["../../packages/browser/src/index.ts"],
|
|
18
|
-
"@finesoft/ssr": ["../../packages/ssr/src/index.ts"],
|
|
19
|
-
"@finesoft/server": ["../../packages/server/src/index.ts"]
|
|
18
|
+
"@finesoft/front": [
|
|
19
|
+
"./.finesoft/front.d.ts"
|
|
20
|
+
]
|
|
20
21
|
}
|
|
21
22
|
},
|
|
22
|
-
"include": [
|
|
23
|
+
"include": [
|
|
24
|
+
"src/**/*",
|
|
25
|
+
"env.d.ts",
|
|
26
|
+
"vite.config.ts"
|
|
27
|
+
]
|
|
23
28
|
}
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import { finesoftFrontViteConfig } from "@finesoft/front";
|
|
2
2
|
import vue from "@vitejs/plugin-vue";
|
|
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, vue()]),
|
|
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
|
-
"vue": "^3.5.
|
|
11
|
+
"@finesoft/front": "^0.5.2",
|
|
12
|
+
"vue": "^3.5.41"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"@
|
|
16
|
-
"
|
|
17
|
-
"
|
|
15
|
+
"@hono/node-server": "^2.1.0",
|
|
16
|
+
"@types/node": "^22.20.1",
|
|
17
|
+
"@vitejs/plugin-vue": "^6.0.8",
|
|
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,62 +1,65 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
2
|
+
import { Outlet as selectOutlet, useSnapshot, type WebAppView } from "@finesoft/front";
|
|
3
|
+
import { onMounted, onUnmounted, ref } from "vue";
|
|
4
|
+
import { TAB_LABELS } from "./lib/navigation";
|
|
5
|
+
import { views } from "./views";
|
|
6
|
+
const Outlet = selectOutlet("vue");
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
8
|
+
defineOptions({ inheritAttrs: false });
|
|
9
|
+
const { app } = defineProps<{ app: WebAppView }>();
|
|
10
|
+
const snapshot = useSnapshot("vue", app);
|
|
11
|
+
// Start empty on both server and client; restore the profile after hydration.
|
|
12
|
+
const name = ref("");
|
|
13
|
+
let unregister: (() => void) | undefined;
|
|
14
|
+
onUnmounted(() => unregister?.());
|
|
15
|
+
onMounted(() => {
|
|
16
|
+
unregister = app.session?.register({
|
|
17
|
+
key: "profile",
|
|
18
|
+
version: 1,
|
|
19
|
+
decode: (data) => {
|
|
20
|
+
if (
|
|
21
|
+
!data ||
|
|
22
|
+
typeof data !== "object" ||
|
|
23
|
+
typeof (data as { name?: unknown }).name !== "string"
|
|
24
|
+
)
|
|
25
|
+
throw Error("invalid-profile-state");
|
|
26
|
+
return data as { name: string };
|
|
27
|
+
},
|
|
28
|
+
capture: () => ({ name: name.value }),
|
|
29
|
+
restore: (data) => {
|
|
30
|
+
name.value = data.name;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
31
33
|
});
|
|
32
34
|
</script>
|
|
33
35
|
|
|
34
36
|
<template>
|
|
35
|
-
<div
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<label v-if="state" style="flex: 1">
|
|
37
|
+
<div class="app-chrome">
|
|
38
|
+
<header class="profile">
|
|
39
|
+
<label>
|
|
39
40
|
Your name (global):
|
|
40
|
-
<input
|
|
41
|
+
<input
|
|
42
|
+
:value="name"
|
|
43
|
+
placeholder="anon"
|
|
44
|
+
@input="name = ($event.target as HTMLInputElement).value"
|
|
45
|
+
@blur="app.session?.save()"
|
|
46
|
+
/>
|
|
41
47
|
</label>
|
|
42
48
|
<span v-if="name">👋 {{ name }}</span>
|
|
43
49
|
</header>
|
|
44
|
-
|
|
45
|
-
<!-- TabView -->
|
|
46
|
-
<nav v-if="tabs" style="display: flex; gap: 0.5rem; margin-bottom: 1rem">
|
|
50
|
+
<nav v-if="snapshot.navigation.tabs" class="tabs" aria-label="Pages">
|
|
47
51
|
<button
|
|
48
|
-
v-for="key in tabs.order"
|
|
52
|
+
v-for="key in snapshot.navigation.tabs.order"
|
|
49
53
|
:key="key"
|
|
50
|
-
:
|
|
51
|
-
|
|
52
|
-
@click="controller?.selectTab(key)"
|
|
54
|
+
:aria-current="key === snapshot.navigation.tabs.active"
|
|
55
|
+
@click="app.perform({ kind: 'selectTab', key })"
|
|
53
56
|
>
|
|
54
|
-
{{
|
|
57
|
+
{{ TAB_LABELS[key] ?? key }}
|
|
55
58
|
</button>
|
|
56
59
|
</nav>
|
|
57
|
-
|
|
58
|
-
<button v-if="canGoBack" style="margin-bottom: 0.5rem" @click="controller?.pop()">
|
|
60
|
+
<button v-if="snapshot.navigation.canGoBack" @click="app.perform({ kind: 'pop' })">
|
|
59
61
|
← Back
|
|
60
62
|
</button>
|
|
63
|
+
<Outlet :app="app" :views="views" />
|
|
61
64
|
</div>
|
|
62
65
|
</template>
|
|
@@ -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
|
+
});
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
readonly itemId: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export class DetailController extends BaseController<{ id?: string }, DetailPage> {
|
|
9
|
-
readonly intentId = "detail";
|
|
10
|
-
|
|
11
|
-
execute(params: { id?: string }): DetailPage {
|
|
6
|
+
export class DetailController extends BaseServerController<Input, DetailPage> {
|
|
7
|
+
execute({ params, context }: Input): DetailPage {
|
|
12
8
|
const id = params.id ?? "?";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
);
|
|
21
22
|
}
|
|
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,15 +1,6 @@
|
|
|
1
|
-
import { BaseController, type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export interface FeedItem {
|
|
5
|
-
readonly id: string;
|
|
6
|
-
readonly title: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/** Home(feed)页面:携带可 push 进 detail 的列表项。 */
|
|
10
|
-
export interface FeedPage extends BasePage {
|
|
11
|
-
readonly items: readonly FeedItem[];
|
|
12
|
-
}
|
|
1
|
+
import { BaseController, type ControllerInput } from "@finesoft/front";
|
|
2
|
+
import { markPublic } from "@finesoft/front";
|
|
3
|
+
import type { FeedItem, HomePage } from "../models/page";
|
|
13
4
|
|
|
14
5
|
const ITEMS: readonly FeedItem[] = [
|
|
15
6
|
{ id: "1", title: "Structured navigation" },
|
|
@@ -17,17 +8,21 @@ const ITEMS: readonly FeedItem[] = [
|
|
|
17
8
|
{ id: "3", title: "Navigation-scoped state" },
|
|
18
9
|
];
|
|
19
10
|
|
|
20
|
-
export class HomeController extends BaseController<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
);
|
|
32
27
|
}
|
|
33
28
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { BaseController, type
|
|
1
|
+
import { BaseController, type ControllerInput } from "@finesoft/front";
|
|
2
|
+
import type { NotesPage } from "../models/page";
|
|
2
3
|
|
|
3
4
|
/** Notes 页面:第二个 tab,演示切 tab 保活该分支的作用域状态。 */
|
|
4
|
-
export class NotesController extends BaseController<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export class NotesController extends BaseController<
|
|
6
|
+
ControllerInput<Record<string, string>>,
|
|
7
|
+
NotesPage
|
|
8
|
+
> {
|
|
9
|
+
execute(): NotesPage {
|
|
8
10
|
return {
|
|
9
11
|
id: "notes",
|
|
10
12
|
pageType: "notes",
|
|
@@ -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
|
+
}
|