@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.
Files changed (198) hide show
  1. package/dist/index.mjs +22 -26
  2. package/package.json +14 -5
  3. package/templates/react/README.md +35 -0
  4. package/templates/react/index.html +2 -3
  5. package/templates/react/package.json +12 -8
  6. package/templates/react/src/App.tsx +7 -15
  7. package/templates/react/src/app-definition.ts +35 -0
  8. package/templates/react/src/components/Layout.tsx +5 -52
  9. package/templates/react/src/components/Loading.tsx +1 -1
  10. package/templates/react/src/components/Navigation.tsx +10 -37
  11. package/templates/react/src/components/ProductCard.tsx +5 -23
  12. package/templates/react/src/config.ts +2 -0
  13. package/templates/react/src/lib/controllers/about.ts +18 -13
  14. package/templates/react/src/lib/controllers/error.ts +11 -7
  15. package/templates/react/src/lib/controllers/home.ts +36 -41
  16. package/templates/react/src/lib/controllers/product-detail.ts +71 -40
  17. package/templates/react/src/lib/controllers/search.ts +33 -54
  18. package/templates/react/src/lib/data/products.ts +36 -0
  19. package/templates/react/src/lib/mappers/product.ts +3 -3
  20. package/templates/react/src/lib/models/product.ts +11 -1
  21. package/templates/react/src/lib/services/api-client.ts +7 -4
  22. package/templates/react/src/main.ts +34 -0
  23. package/templates/react/src/pages/About.tsx +2 -4
  24. package/templates/react/src/pages/Home.tsx +6 -15
  25. package/templates/react/src/pages/NotFound.tsx +4 -14
  26. package/templates/react/src/pages/ProductDetail.tsx +5 -23
  27. package/templates/react/src/pages/Search.tsx +5 -7
  28. package/templates/react/src/ssr.ts +11 -0
  29. package/templates/react/src/styles.css +71 -0
  30. package/templates/react/src/views.ts +12 -0
  31. package/templates/react/tsconfig.json +14 -9
  32. package/templates/react/vite.config.ts +9 -13
  33. package/templates/react-minimal/README.md +39 -0
  34. package/templates/react-minimal/index.html +2 -3
  35. package/templates/react-minimal/package.json +12 -8
  36. package/templates/react-minimal/src/App.tsx +40 -85
  37. package/templates/react-minimal/src/app-definition.ts +48 -0
  38. package/templates/react-minimal/src/config.ts +2 -0
  39. package/templates/react-minimal/src/lib/controllers/detail.ts +19 -18
  40. package/templates/react-minimal/src/lib/controllers/error.ts +15 -0
  41. package/templates/react-minimal/src/lib/controllers/home.ts +19 -24
  42. package/templates/react-minimal/src/lib/controllers/notes.ts +7 -5
  43. package/templates/react-minimal/src/lib/locale.ts +16 -0
  44. package/templates/react-minimal/src/lib/models/page.ts +27 -0
  45. package/templates/react-minimal/src/lib/navigation.ts +11 -0
  46. package/templates/react-minimal/src/locales/en-US.json +5 -0
  47. package/templates/react-minimal/src/locales/zh-Hans.json +5 -0
  48. package/templates/react-minimal/src/main.ts +43 -0
  49. package/templates/react-minimal/src/pages/Detail.tsx +20 -0
  50. package/templates/react-minimal/src/pages/Home.tsx +38 -0
  51. package/templates/react-minimal/src/pages/NotFound.tsx +11 -0
  52. package/templates/react-minimal/src/pages/Notes.tsx +21 -0
  53. package/templates/react-minimal/src/ssr.ts +11 -0
  54. package/templates/react-minimal/src/styles.css +74 -0
  55. package/templates/react-minimal/src/views.ts +6 -0
  56. package/templates/react-minimal/src/vite-env.d.ts +3 -0
  57. package/templates/react-minimal/tsconfig.json +14 -9
  58. package/templates/react-minimal/vite.config.ts +9 -7
  59. package/templates/svelte/README.md +35 -0
  60. package/templates/svelte/index.html +1 -2
  61. package/templates/svelte/package.json +9 -5
  62. package/templates/svelte/src/App.svelte +8 -80
  63. package/templates/svelte/src/app-definition.ts +35 -0
  64. package/templates/svelte/src/components/Layout.svelte +2 -2
  65. package/templates/svelte/src/components/Loading.svelte +1 -1
  66. package/templates/svelte/src/components/Navigation.svelte +5 -66
  67. package/templates/svelte/src/components/ProductCard.svelte +3 -24
  68. package/templates/svelte/src/config.ts +2 -0
  69. package/templates/svelte/src/lib/controllers/about.ts +18 -13
  70. package/templates/svelte/src/lib/controllers/error.ts +11 -7
  71. package/templates/svelte/src/lib/controllers/home.ts +36 -41
  72. package/templates/svelte/src/lib/controllers/product-detail.ts +71 -40
  73. package/templates/svelte/src/lib/controllers/search.ts +33 -54
  74. package/templates/svelte/src/lib/data/products.ts +36 -0
  75. package/templates/svelte/src/lib/mappers/product.ts +3 -3
  76. package/templates/svelte/src/lib/models/product.ts +12 -1
  77. package/templates/svelte/src/lib/services/api-client.ts +7 -4
  78. package/templates/svelte/src/main.ts +28 -24
  79. package/templates/svelte/src/pages/About.svelte +2 -2
  80. package/templates/svelte/src/pages/Home.svelte +3 -15
  81. package/templates/svelte/src/pages/NotFound.svelte +3 -12
  82. package/templates/svelte/src/pages/ProductDetail.svelte +4 -21
  83. package/templates/svelte/src/pages/Search.svelte +2 -10
  84. package/templates/svelte/src/ssr.ts +10 -9
  85. package/templates/svelte/src/styles.css +71 -0
  86. package/templates/svelte/src/views.ts +12 -0
  87. package/templates/svelte/svelte.config.js +2 -0
  88. package/templates/svelte/tsconfig.json +13 -8
  89. package/templates/svelte/vite.config.ts +9 -13
  90. package/templates/svelte-minimal/README.md +39 -0
  91. package/templates/svelte-minimal/index.html +1 -2
  92. package/templates/svelte-minimal/package.json +9 -5
  93. package/templates/svelte-minimal/src/App.svelte +49 -61
  94. package/templates/svelte-minimal/src/app-definition.ts +48 -0
  95. package/templates/svelte-minimal/src/config.ts +2 -0
  96. package/templates/svelte-minimal/src/lib/controllers/detail.ts +23 -0
  97. package/templates/svelte-minimal/src/lib/controllers/error.ts +15 -0
  98. package/templates/svelte-minimal/src/lib/controllers/home.ts +24 -23
  99. package/templates/svelte-minimal/src/lib/controllers/notes.ts +18 -0
  100. package/templates/svelte-minimal/src/lib/locale.ts +16 -0
  101. package/templates/svelte-minimal/src/lib/models/page.ts +27 -0
  102. package/templates/svelte-minimal/src/lib/navigation.ts +11 -0
  103. package/templates/svelte-minimal/src/locales/en-US.json +1 -3
  104. package/templates/svelte-minimal/src/locales/zh-Hans.json +1 -3
  105. package/templates/svelte-minimal/src/main.ts +37 -19
  106. package/templates/svelte-minimal/src/pages/Detail.svelte +11 -0
  107. package/templates/svelte-minimal/src/pages/Home.svelte +17 -21
  108. package/templates/svelte-minimal/src/pages/NotFound.svelte +10 -0
  109. package/templates/svelte-minimal/src/pages/Notes.svelte +11 -0
  110. package/templates/svelte-minimal/src/ssr.ts +9 -17
  111. package/templates/svelte-minimal/src/styles.css +74 -0
  112. package/templates/svelte-minimal/src/views.ts +6 -0
  113. package/templates/svelte-minimal/src/vite-env.d.ts +3 -0
  114. package/templates/svelte-minimal/svelte.config.js +2 -0
  115. package/templates/svelte-minimal/tsconfig.json +13 -8
  116. package/templates/svelte-minimal/vite.config.ts +9 -10
  117. package/templates/vue/README.md +35 -0
  118. package/templates/vue/index.html +1 -2
  119. package/templates/vue/package.json +9 -5
  120. package/templates/vue/src/App.vue +7 -22
  121. package/templates/vue/src/app-definition.ts +35 -0
  122. package/templates/vue/src/components/Layout.vue +4 -6
  123. package/templates/vue/src/components/Loading.vue +1 -1
  124. package/templates/vue/src/components/Navigation.vue +10 -43
  125. package/templates/vue/src/components/ProductCard.vue +4 -28
  126. package/templates/vue/src/config.ts +2 -0
  127. package/templates/vue/src/lib/controllers/about.ts +18 -13
  128. package/templates/vue/src/lib/controllers/error.ts +11 -7
  129. package/templates/vue/src/lib/controllers/home.ts +36 -41
  130. package/templates/vue/src/lib/controllers/product-detail.ts +71 -40
  131. package/templates/vue/src/lib/controllers/search.ts +33 -54
  132. package/templates/vue/src/lib/data/products.ts +36 -0
  133. package/templates/vue/src/lib/mappers/product.ts +3 -3
  134. package/templates/vue/src/lib/models/product.ts +12 -1
  135. package/templates/vue/src/lib/services/api-client.ts +7 -7
  136. package/templates/vue/src/main.ts +28 -43
  137. package/templates/vue/src/pages/About.vue +3 -5
  138. package/templates/vue/src/pages/Home.vue +5 -23
  139. package/templates/vue/src/pages/NotFound.vue +4 -13
  140. package/templates/vue/src/pages/ProductDetail.vue +5 -23
  141. package/templates/vue/src/pages/Search.vue +4 -18
  142. package/templates/vue/src/ssr.ts +5 -18
  143. package/templates/vue/src/styles.css +71 -0
  144. package/templates/vue/src/views.ts +12 -0
  145. package/templates/vue/tsconfig.json +14 -9
  146. package/templates/vue/vite.config.ts +9 -13
  147. package/templates/vue-minimal/README.md +39 -0
  148. package/templates/vue-minimal/index.html +1 -2
  149. package/templates/vue-minimal/package.json +9 -5
  150. package/templates/vue-minimal/src/App.vue +46 -43
  151. package/templates/vue-minimal/src/app-definition.ts +48 -0
  152. package/templates/vue-minimal/src/config.ts +2 -0
  153. package/templates/vue-minimal/src/lib/controllers/detail.ts +19 -18
  154. package/templates/vue-minimal/src/lib/controllers/error.ts +15 -0
  155. package/templates/vue-minimal/src/lib/controllers/home.ts +19 -24
  156. package/templates/vue-minimal/src/lib/controllers/notes.ts +7 -5
  157. package/templates/vue-minimal/src/lib/locale.ts +16 -0
  158. package/templates/vue-minimal/src/lib/models/page.ts +27 -0
  159. package/templates/vue-minimal/src/lib/navigation.ts +11 -0
  160. package/templates/vue-minimal/src/locales/en-US.json +5 -0
  161. package/templates/vue-minimal/src/locales/zh-Hans.json +5 -0
  162. package/templates/vue-minimal/src/main.ts +35 -70
  163. package/templates/vue-minimal/src/pages/Detail.vue +20 -0
  164. package/templates/vue-minimal/src/pages/Home.vue +34 -0
  165. package/templates/vue-minimal/src/pages/NotFound.vue +13 -0
  166. package/templates/vue-minimal/src/pages/Notes.vue +22 -0
  167. package/templates/vue-minimal/src/ssr.ts +7 -46
  168. package/templates/vue-minimal/src/styles.css +74 -0
  169. package/templates/vue-minimal/src/views.ts +6 -0
  170. package/templates/vue-minimal/src/vite-env.d.ts +3 -0
  171. package/templates/vue-minimal/tsconfig.json +14 -9
  172. package/templates/vue-minimal/vite.config.ts +9 -7
  173. package/templates/react/src/actions.ts +0 -14
  174. package/templates/react/src/bootstrap.ts +0 -36
  175. package/templates/react/src/components/PageRenderer.tsx +0 -41
  176. package/templates/react/src/main.tsx +0 -49
  177. package/templates/react/src/ssr.tsx +0 -23
  178. package/templates/react-minimal/src/bootstrap.ts +0 -39
  179. package/templates/react-minimal/src/main.tsx +0 -99
  180. package/templates/react-minimal/src/ssr.tsx +0 -51
  181. package/templates/react-minimal/src/views/DetailView.tsx +0 -28
  182. package/templates/react-minimal/src/views/HomeView.tsx +0 -27
  183. package/templates/react-minimal/src/views/NotesView.tsx +0 -11
  184. package/templates/svelte/src/actions.ts +0 -14
  185. package/templates/svelte/src/bootstrap.ts +0 -36
  186. package/templates/svelte/src/components/PageRenderer.svelte +0 -22
  187. package/templates/svelte/src/lib/framework-svelte.ts +0 -34
  188. package/templates/svelte/src/lib/render.ts +0 -15
  189. package/templates/svelte-minimal/src/bootstrap.ts +0 -6
  190. package/templates/svelte-minimal/src/lib/framework-svelte.ts +0 -34
  191. package/templates/svelte-minimal/src/lib/render.ts +0 -12
  192. package/templates/vue/src/actions.ts +0 -14
  193. package/templates/vue/src/bootstrap.ts +0 -42
  194. package/templates/vue/src/components/PageRenderer.vue +0 -26
  195. package/templates/vue-minimal/src/bootstrap.ts +0 -39
  196. package/templates/vue-minimal/src/views/DetailView.vue +0 -29
  197. package/templates/vue-minimal/src/views/HomeView.vue +0 -26
  198. 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, onAction } = defineProps<{ page: SearchPage; onAction?: (action: Action) => void }>();
5
+ const { page } = defineProps<{ page: SearchPage }>();
7
6
  </script>
8
7
 
9
8
  <template>
10
- <div>
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
- </div>
18
+ </section>
25
19
  </template>
26
-
27
- <style scoped>
28
- .results {
29
- display: flex;
30
- gap: 1rem;
31
- flex-wrap: wrap;
32
- }
33
- </style>
@@ -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
- bootstrap,
10
- getErrorPage,
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
- "jsx": "preserve",
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
- "@/*": ["./src/*"],
14
- "@finesoft/front": ["../../packages/front/src/index.ts"],
15
- "@finesoft/front/browser": ["../../packages/front/src/browser.ts"],
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": ["src/**/*", "env.d.ts", "vite.config.ts"]
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
- plugins: [
7
- vue(),
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.
@@ -6,8 +6,7 @@
6
6
  <!--ssr-head-->
7
7
  </head>
8
8
  <body>
9
- <div id="app"><!--ssr-body--></div>
10
- <!--ssr-data-->
9
+ <div id="app"><!--ssr-body--><!--ssr-data--></div>
11
10
  <script type="module" src="/src/main.ts"></script>
12
11
  </body>
13
12
  </html>
@@ -8,12 +8,16 @@
8
8
  "preview": "vp preview"
9
9
  },
10
10
  "dependencies": {
11
- "@finesoft/front": "^0.5.1",
12
- "vue": "^3.5.0"
11
+ "@finesoft/front": "^0.5.2",
12
+ "vue": "^3.5.41"
13
13
  },
14
14
  "devDependencies": {
15
- "@vitejs/plugin-vue": "^5.0.0",
16
- "vite": "npm:@voidzero-dev/vite-plus-core@0.1.16",
17
- "vite-plus": "0.1.24"
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 { isStackNode, isTabsNode } from "@finesoft/front";
3
- import { computed } from "vue";
4
- import type { AppController, AppState } from "./main";
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
- const { state, controller } = defineProps<{ state?: AppState; controller?: AppController }>();
7
-
8
- const tree = computed(() => state?.snapshot?.tree ?? null);
9
-
10
- /** Tab bar(tree 为 tabs 节点时)。 */
11
- const tabs = computed(() => {
12
- const t = tree.value;
13
- return t && isTabsNode(t) ? { order: t.order, active: t.active } : null;
14
- });
15
- const tabLabels: Record<string, string> = { home: "Feed", notes: "Notes" };
16
-
17
- /** 激活 tab 的栈深 > 1 → 可返回。 */
18
- const canGoBack = computed(() => {
19
- const t = tree.value;
20
- if (!t || !isTabsNode(t)) return false;
21
- const branch = t.branches[t.active];
22
- return !!branch && isStackNode(branch) && branch.entries.length > 1;
23
- });
24
-
25
- /** 全局切片:名字(跨 tab / 跨重载)。 */
26
- const name = computed({
27
- get: () => state?.name ?? "",
28
- set: (v) => {
29
- if (state) state.name = v;
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 style="max-width: 32rem; margin: 0 auto; padding: 1rem; font-family: system-ui">
36
- <!-- 全局切片:名字 -->
37
- <header style="display: flex; gap: 0.5rem; align-items: center; margin-bottom: 1rem">
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 v-model="name" placeholder="anon" @blur="controller?.save()" />
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
- :style="{ fontWeight: key === tabs.active ? '700' : '400' }"
51
- :aria-current="key === tabs.active"
52
- @click="controller?.selectTab(key)"
54
+ :aria-current="key === snapshot.navigation.tabs.active"
55
+ @click="app.perform({ kind: 'selectTab', key })"
53
56
  >
54
- {{ tabLabels[key] ?? key }}
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
+ });
@@ -0,0 +1,2 @@
1
+ /** App identity also namespaces persisted browser sessions. */
2
+ export const appId = "vue-minimal";
@@ -1,22 +1,23 @@
1
- import { BaseController, type BasePage } from "@finesoft/front";
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
- /** Detail 页面:由 push("detail", { id }) 进入;id 进 params。 */
4
- export interface DetailPage extends BasePage {
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
- return {
14
- id: `detail-${id}`,
15
- pageType: "detail",
16
- url: `/item/${id}`,
17
- title: `Item ${id}`,
18
- description: "A pushed detail screen. Its scoped note is lost once you pop it.",
19
- itemId: id,
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 BasePage } from "@finesoft/front";
2
-
3
- /** 一个 feed 项。 */
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<Record<string, string>, FeedPage> {
21
- readonly intentId = "home";
22
-
23
- execute(): FeedPage {
24
- return {
25
- id: "home",
26
- pageType: "home",
27
- url: "/",
28
- title: "Feed",
29
- description: "Tap an item to push a detail screen.",
30
- items: ITEMS,
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 BasePage } from "@finesoft/front";
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<Record<string, string>, BasePage> {
5
- readonly intentId = "notes";
6
-
7
- execute(): BasePage {
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
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "home.localeLabel": "Current locale",
3
+ "home.runtimeBadge": "Hydration kept the translator alive on the client.",
4
+ "home.switchHint": "Change frameworkConfig.locale in src/app-definition.ts to en-US or zh-Hans to load the corresponding JSON file."
5
+ }