@jskit-ai/agent-docs 0.1.131 → 0.1.132

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 (170) hide show
  1. package/guide/agent/app-extras/assistant.md +29 -605
  2. package/guide/agent/app-extras/mobile-capacitor.md +29 -362
  3. package/guide/agent/app-extras/realtime.md +29 -277
  4. package/guide/agent/app-setup/a-more-interesting-shell.md +44 -815
  5. package/guide/agent/app-setup/authentication.md +43 -1073
  6. package/guide/agent/app-setup/console.md +26 -298
  7. package/guide/agent/app-setup/database-layer.md +110 -790
  8. package/guide/agent/app-setup/initial-scaffolding.md +50 -784
  9. package/guide/agent/app-setup/multi-homing.md +39 -712
  10. package/guide/agent/app-setup/quickstart.md +43 -179
  11. package/guide/agent/app-setup/users.md +34 -353
  12. package/guide/agent/index.md +16 -23
  13. package/package.json +2 -2
  14. package/patterns/INDEX.md +7 -7
  15. package/patterns/child-cruds.md +3 -3
  16. package/patterns/client-requests.md +6 -6
  17. package/patterns/crud-authoring.md +94 -0
  18. package/patterns/crud-links.md +1 -1
  19. package/patterns/feature-package/PATTERN.md +108 -0
  20. package/patterns/feature-package/example/booking-engine/package.json +48 -0
  21. package/patterns/feature-package/example/booking-engine/src/server/BookingEngineProvider.js +33 -0
  22. package/patterns/feature-package/example/booking-engine/src/server/actions.js +26 -0
  23. package/patterns/feature-package/example/booking-engine/src/server/inputSchemas.js +19 -0
  24. package/patterns/feature-package/example/variations/AvailabilityEngineProvider.js +31 -0
  25. package/patterns/feature-package/example/variations/InvoiceRollupProvider.js +36 -0
  26. package/patterns/feature-package/example/variations/customKnexRepository.js +34 -0
  27. package/patterns/feature-package/example/variations/orchestratorService.js +23 -0
  28. package/patterns/filters.md +8 -8
  29. package/patterns/live-actions.md +5 -18
  30. package/patterns/minimal-foundation/PATTERN.md +98 -0
  31. package/patterns/minimal-foundation/example/.nvmrc +1 -0
  32. package/patterns/minimal-foundation/example/AGENTS.md +17 -0
  33. package/patterns/minimal-foundation/example/Procfile +2 -0
  34. package/patterns/minimal-foundation/example/app.json +14 -0
  35. package/patterns/minimal-foundation/example/bin/develop.js +71 -0
  36. package/patterns/minimal-foundation/example/bin/server.js +8 -0
  37. package/patterns/minimal-foundation/example/config/public.js +40 -0
  38. package/patterns/minimal-foundation/example/config/server.js +1 -0
  39. package/patterns/minimal-foundation/example/config/surfaceAccessPolicies.js +3 -0
  40. package/patterns/minimal-foundation/example/eslint.config.mjs +19 -0
  41. package/patterns/minimal-foundation/example/favicon.svg +7 -0
  42. package/patterns/minimal-foundation/example/gitignore +9 -0
  43. package/patterns/minimal-foundation/example/index.html +13 -0
  44. package/patterns/minimal-foundation/example/jsconfig.json +8 -0
  45. package/patterns/minimal-foundation/example/package.json +57 -0
  46. package/patterns/minimal-foundation/example/packages/main/package.json +42 -0
  47. package/patterns/minimal-foundation/example/packages/main/src/shared/index.js +10 -0
  48. package/patterns/minimal-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
  49. package/patterns/minimal-foundation/example/playwright.config.mjs +31 -0
  50. package/patterns/minimal-foundation/example/server/lib/runtimeEnv.js +45 -0
  51. package/patterns/minimal-foundation/example/server/lib/surfaceRuntime.js +10 -0
  52. package/patterns/minimal-foundation/example/server.js +195 -0
  53. package/patterns/minimal-foundation/example/src/App.vue +13 -0
  54. package/patterns/minimal-foundation/example/src/main.js +85 -0
  55. package/patterns/minimal-foundation/example/src/pages/home/index.vue +48 -0
  56. package/patterns/minimal-foundation/example/src/pages/home.vue +13 -0
  57. package/patterns/minimal-foundation/example/src/views/NotFound.vue +13 -0
  58. package/patterns/minimal-foundation/example/tests/client/smoke.vitest.js +7 -0
  59. package/patterns/minimal-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
  60. package/patterns/minimal-foundation/example/tests/server/smoke.test.js +16 -0
  61. package/patterns/minimal-foundation/example/vite.config.mjs +81 -0
  62. package/patterns/page-scaffolding.md +20 -17
  63. package/patterns/placements.md +17 -15
  64. package/patterns/row-policies.md +4 -5
  65. package/patterns/server-search.md +3 -3
  66. package/patterns/shell-foundation/PATTERN.md +104 -0
  67. package/patterns/shell-foundation/example/.nvmrc +1 -0
  68. package/patterns/shell-foundation/example/AGENTS.md +17 -0
  69. package/patterns/shell-foundation/example/Procfile +2 -0
  70. package/patterns/shell-foundation/example/app.json +14 -0
  71. package/patterns/shell-foundation/example/bin/develop.js +71 -0
  72. package/patterns/shell-foundation/example/bin/server.js +8 -0
  73. package/patterns/shell-foundation/example/config/public.js +40 -0
  74. package/patterns/shell-foundation/example/config/server.js +1 -0
  75. package/patterns/shell-foundation/example/config/surfaceAccessPolicies.js +3 -0
  76. package/patterns/shell-foundation/example/eslint.config.mjs +19 -0
  77. package/patterns/shell-foundation/example/favicon.svg +7 -0
  78. package/patterns/shell-foundation/example/gitignore +9 -0
  79. package/patterns/shell-foundation/example/index.html +13 -0
  80. package/patterns/shell-foundation/example/jsconfig.json +8 -0
  81. package/patterns/shell-foundation/example/package.json +59 -0
  82. package/patterns/shell-foundation/example/packages/main/package.json +56 -0
  83. package/patterns/shell-foundation/example/packages/main/src/client/index.js +9 -0
  84. package/patterns/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +18 -0
  85. package/patterns/shell-foundation/example/packages/main/src/shared/index.js +10 -0
  86. package/patterns/shell-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
  87. package/patterns/shell-foundation/example/playwright.config.mjs +31 -0
  88. package/patterns/shell-foundation/example/server/lib/runtimeEnv.js +45 -0
  89. package/patterns/shell-foundation/example/server/lib/surfaceRuntime.js +10 -0
  90. package/patterns/shell-foundation/example/server.js +195 -0
  91. package/patterns/shell-foundation/example/src/App.vue +11 -0
  92. package/patterns/shell-foundation/example/src/components/ShellLayout.vue +12 -0
  93. package/patterns/shell-foundation/example/src/components/menus/MenuLinkItem.vue +30 -0
  94. package/patterns/shell-foundation/example/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
  95. package/patterns/shell-foundation/example/src/components/menus/TabLinkItem.vue +42 -0
  96. package/patterns/shell-foundation/example/src/error.js +19 -0
  97. package/patterns/shell-foundation/example/src/main.js +85 -0
  98. package/patterns/shell-foundation/example/src/pages/home/index.vue +116 -0
  99. package/patterns/shell-foundation/example/src/pages/home/settings/general/index.vue +40 -0
  100. package/patterns/shell-foundation/example/src/pages/home/settings/index.vue +7 -0
  101. package/patterns/shell-foundation/example/src/pages/home/settings.vue +109 -0
  102. package/patterns/shell-foundation/example/src/pages/home.vue +20 -0
  103. package/patterns/shell-foundation/example/src/placement.js +56 -0
  104. package/patterns/shell-foundation/example/src/placementTopology.js +149 -0
  105. package/patterns/shell-foundation/example/src/views/NotFound.vue +13 -0
  106. package/patterns/shell-foundation/example/tests/client/smoke.vitest.js +7 -0
  107. package/patterns/shell-foundation/example/tests/e2e/adaptive-shell.spec.ts +10 -0
  108. package/patterns/shell-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
  109. package/patterns/shell-foundation/example/tests/server/smoke.test.js +16 -0
  110. package/patterns/shell-foundation/example/vite.config.mjs +81 -0
  111. package/patterns/ui-contract.md +56 -0
  112. package/patterns/ui-testing.md +10 -12
  113. package/reference/autogen/KERNEL_MAP.md +29 -107
  114. package/reference/autogen/PATTERN_INDEX.md +230 -0
  115. package/reference/autogen/README.md +4 -8
  116. package/reference/autogen/packages/agent-docs.md +259 -0
  117. package/reference/autogen/packages/assistant-core.md +3 -3
  118. package/reference/autogen/packages/assistant-runtime.md +32 -17
  119. package/reference/autogen/packages/auth-core.md +31 -33
  120. package/reference/autogen/packages/auth-provider-local-core.md +4 -12
  121. package/reference/autogen/packages/auth-provider-local-db-core.md +4 -4
  122. package/reference/autogen/packages/auth-provider-supabase-core.md +14 -18
  123. package/reference/autogen/packages/auth-web.md +42 -22
  124. package/reference/autogen/packages/console-core.md +8 -25
  125. package/reference/autogen/packages/console-web.md +5 -5
  126. package/reference/autogen/packages/crud-core.md +61 -17
  127. package/reference/autogen/packages/database-runtime-mysql.md +12 -2
  128. package/reference/autogen/packages/database-runtime-postgres.md +12 -2
  129. package/reference/autogen/packages/database-runtime.md +26 -25
  130. package/reference/autogen/packages/google-rewarded-core.md +19 -104
  131. package/reference/autogen/packages/http-runtime.md +4 -8
  132. package/reference/autogen/packages/http-web.md +32 -0
  133. package/reference/autogen/packages/json-rest-api-core.md +4 -6
  134. package/reference/autogen/packages/kernel.md +109 -390
  135. package/reference/autogen/packages/mobile-capacitor.md +2 -13
  136. package/reference/autogen/packages/realtime.md +29 -26
  137. package/reference/autogen/packages/resource-crud-core.md +6 -0
  138. package/reference/autogen/packages/shell-web.md +69 -54
  139. package/reference/autogen/packages/storage-runtime.md +3 -3
  140. package/reference/autogen/packages/uploads-image-web.md +0 -1
  141. package/reference/autogen/packages/uploads-runtime.md +3 -3
  142. package/reference/autogen/packages/users-core.md +45 -90
  143. package/reference/autogen/packages/users-web.md +5 -7
  144. package/reference/autogen/packages/workspaces-core.md +53 -74
  145. package/reference/autogen/packages/workspaces-web.md +15 -16
  146. package/reference/autogen/tooling/jskit-catalog.md +34 -0
  147. package/reference/autogen/tooling/testUtils.md +4 -4
  148. package/skills/jskit/SKILL.md +34 -29
  149. package/skills/jskit/references/app-operations.md +68 -53
  150. package/skills/jskit/references/crud-operations.md +58 -106
  151. package/skills/jskit/references/material-3.md +1 -1
  152. package/skills/jskit/references/ui-operations.md +41 -44
  153. package/templates/app/AGENTS.md +7 -3
  154. package/guide/agent/app-setup/upgrade-beta-1-to-final.md +0 -252
  155. package/guide/agent/app-setup/working-with-the-jskit-cli.md +0 -325
  156. package/guide/agent/generators/advanced-cruds.md +0 -1935
  157. package/guide/agent/generators/crud-generators.md +0 -948
  158. package/guide/agent/generators/intro.md +0 -65
  159. package/guide/agent/generators/row-policies.md +0 -537
  160. package/guide/agent/generators/ui-generators.md +0 -690
  161. package/patterns/crud-scaffolding.md +0 -198
  162. package/patterns/generated-ui-contract-tracking.md +0 -66
  163. package/reference/autogen/packages/assistant.md +0 -68
  164. package/reference/autogen/packages/crud-server-generator.md +0 -215
  165. package/reference/autogen/packages/crud-ui-generator.md +0 -192
  166. package/reference/autogen/packages/feature-server-generator.md +0 -65
  167. package/reference/autogen/packages/ui-generator.md +0 -127
  168. package/reference/autogen/tooling/create-app.md +0 -317
  169. package/reference/autogen/tooling/jskit-cli.md +0 -933
  170. package/reference/autogen/tooling/test-support.md +0 -27
@@ -2,834 +2,63 @@
2
2
 
3
3
  # A more interesting shell
4
4
 
5
- In the first chapter, the app started with `shell-web` already installed. That is the normal JSKIT starting point: a real layout, navigation outlets, a first settings area, and a proper app-level error host.
6
-
7
- This still is not authentication, not database work, and not multi-surface routing. The app remains small. But it starts to look and behave like something you could actually grow.
8
-
9
- ## Running the shell
10
-
11
- From inside `exampleapp`, run:
12
-
13
- ```bash
14
- npm run dev
15
- npm run server
16
- ```
17
-
18
- You want both processes running. The starter home page fetches `/api/health`, so the browser-facing dev server on port `5173` expects the backend on port `3000` to be alive as well.
19
-
20
- **Minimal: Adding `shell-web` To A Bare Scaffold**
21
-
22
- The normal `create-app` template already has `shell-web`. If you deliberately created the bare scaffold with `--minimal` or `--template minimal-shell`, install the shell before you personalize the files it claims:
23
-
24
- ```bash
25
- npx jskit add package shell-web
26
- ```
27
-
28
- That package addition is intentionally strict: `shell-web` only takes over scaffold files if they are still **exactly** the same as the files that `create-app --minimal` originally wrote. If you have already edited those starter files, `shell-web` refuses to claim them instead of overwriting your work.
29
-
30
- Open `http://localhost:5173/` in the browser. The app lands in the `home` surface inside a real shell with an app bar, a navigation drawer, and a settings route at `/home/settings`.
31
-
32
- Two shell pieces are worth noticing first.
33
-
34
- - Navigation lives in the drawer itself. `Home` and `Settings` are real shell menu entries from the start.
35
- - `Settings` is already a real nested section. Opening `/home/settings` redirects to `/home/settings/general`, and the left-side menu already contains a starter `General` entry.
36
-
37
- Open `http://localhost:5173/home/settings` in the browser to see that nested settings shell immediately:
38
-
39
- ## Module features
40
-
41
- The most important idea in `shell-web` is that the app has _specific, named places_ where UI can be inserted later. JSKIT calls those places _placements_. In practice, this means later packages or generators do not have to rewrite the whole shell every time they want to add a menu entry, a widget, or a settings section.
42
-
43
- Start by asking JSKIT what public placement targets already exist:
44
-
45
- ```bash
46
- npx jskit list-placements
47
- ```
48
-
49
- In a fresh `shell-web` app, the result includes entries like these:
50
-
51
- ```text
52
- Available placements:
53
- - shell.primary-nav (default): Primary top-level navigation for the current surface.
54
- - compact -> shell-layout:primary-bottom-nav
55
- - medium -> shell-layout:primary-menu
56
- - expanded -> shell-layout:primary-menu
57
- - page.section-nav [owner:home-settings]: Navigation between child pages in the home settings section.
58
- - compact -> home-settings:primary-menu
59
- - medium -> home-settings:primary-menu
60
- - expanded -> home-settings:primary-menu
61
- ```
62
-
63
- This command lists semantic placements, not the content inside them. Later, when you place things into the shell, this list stays stable unless the public placement topology changes.
64
-
65
- The concrete outlets still exist, but they are implementation details. If you need to inspect them directly, use:
66
-
67
- ```bash
68
- npx jskit list-placements --concrete
69
- ```
70
-
71
- Those concrete target names come from real `ShellOutlet` elements in the app. See `src/components/ShellLayout.vue`:
72
-
73
- ```html
74
- ...
75
- <ShellOutlet target="shell-layout:top-left" />
76
- ...
77
- <ShellOutlet target="shell-layout:top-right" />
78
- ...
79
- <ShellOutlet
80
- target="shell-layout:primary-menu"
81
- default
82
- />
83
- ...
84
- <ShellOutlet target="shell-layout:secondary-menu" />
85
- ...
86
- <ShellOutlet target="shell-layout:primary-bottom-nav" />
87
- ...
88
- <ShellOutlet target="shell-layout:supporting-bottom-sheet" />
89
- <ShellOutlet target="shell-layout:supporting-side-panel" />
90
- ```
91
-
92
- And the settings page introduces its own nested outlet in `src/pages/home/settings.vue`:
93
-
94
- ```html
95
- <ShellOutlet target="home-settings:primary-menu" />
96
- ```
97
-
98
- That nested example matters. It shows that the shell is not the only place that can host placements. A page inside the shell can define its own insertion point too. That is how JSKIT can later build menus inside sections such as settings without rewriting the whole shell.
99
-
100
- Just as importantly, `shell-web` uses that placement system itself. The starter app exposes semantic placement targets and seeds real placement entries into them. `Home` and `Settings` land in primary navigation, and `General` lands in the nested settings menu. The shell is exercising the same extension contract that later package and generator output uses.
101
-
102
- The shell also ships with a few app-owned component tokens that it can use as default link renderers. You can inspect those too:
103
-
104
- ```bash
105
- npx jskit list-component-tokens --prefix local.main.
106
- ```
107
-
108
- The starter result looks like this:
109
-
110
- ```text
111
- Available placement component tokens:
112
- Showing link-item tokens only (token must end with "link-item"). Tip: use --all for full token list.
113
- - local.main.ui.menu-link-item [app:packages/main/src/client/providers/MainClientProvider.js]
114
- - local.main.ui.surface-aware-menu-link-item [app:packages/main/src/client/providers/MainClientProvider.js, app:src/placement.js, package:@jskit-ai/shell-web:templates/src/placement.js]
115
- - local.main.ui.tab-link-item [app:packages/main/src/client/providers/MainClientProvider.js]
116
- ```
117
-
118
- The shell knows about three local link-item tokens. They are app-owned components registered by the local package, and topology uses them when an outlet needs to render links or tabs.
119
-
120
- At this point the shell is using placements itself. The next step is to add one of our own.
121
-
122
- ### Adding generic elements directly
123
-
124
- To add a small UI element to the shell itself:
125
-
126
- ```bash
127
- npx jskit generate ui-generator placed-element --name "Alerts Widget"
128
- ```
129
-
130
- That command creates a Vue component under `src/components/` (in this case `src/components/AlertsWidgetElement.vue`), registers a local token for it, and adds a placement entry targeting `shell.status`. After running it, refresh the home page in the browser. The shell renders the app-owned widget inside one of its named placement targets.
131
-
132
- In this app, there is no need to pass `--surface`: since the app only has one enabled surface, JSKIT can infer it automatically.
133
-
134
- ### Adding a page with automatic menu placement
135
-
136
- The settings host uses the same placement machinery, but the normal way to grow it is not by dropping a free-standing widget there. The more interesting case is adding a child page and letting JSKIT wire the menu entry for you.
137
-
138
- The owner in the `list-placements` output helps you reason about where child pages belong. When you see:
139
-
140
- ```text
141
- - page.section-nav [owner:home-settings]
142
- ```
143
-
144
- you know that the semantic placement is owned by the settings host page. So if you want a child page to appear in that menu, you should create it under that part of the route tree instead of treating the menu like a generic widget area. For example, `src/pages/home/settings/profile/index.vue` belongs to that settings section, so JSKIT can wire its preferred menu entry into `page.section-nav` with owner `home-settings` automatically.
145
-
146
- Now use the settings host the way it is normally meant to be used: add a real child page under it.
147
-
148
- ```bash
149
- npx jskit generate ui-generator page home/settings/profile/index.vue --name "Profile"
150
- ```
151
-
152
- This is a more interesting example than the widget case. JSKIT creates the page file, notices that `src/pages/home/settings.vue` owns the settings section navigation, and adds the preferred semantic menu entry there automatically. You do not have to write that placement entry by hand.
153
-
154
- Open `/home/settings/profile` in the browser. The settings shell shows a second real child page and a second real menu entry created by the same page-generation command. `General` comes from `shell-web`; `Profile` is the first additional settings page you add yourself. This is the important part of the chapter: the exact same placement system works both at the top shell level and inside a page-owned nested outlet.
155
-
156
- Add a second sibling page:
157
-
158
- ```bash
159
- npx jskit generate ui-generator page home/settings/notifications/index.vue --name "Notifications"
160
- ```
161
-
162
- Open `/home/settings/notifications` in the browser. You get a third settings menu entry without touching `settings.vue`, without writing a second menu component, and without hand-editing `src/placement.js`. JSKIT appends another placement entry targeting the same `page.section-nav` owner, so the links stack in the menu for free.
163
-
164
- The order is also easy to reason about:
165
-
166
- - `General` comes first because `shell-web` seeds it with a lower order than generated child pages.
167
- - `Profile` and `Notifications` both use the generator's default order, so between those two the menu keeps source order.
168
-
169
- **Routing: Child Pages Under Layouts**
170
-
171
- In JSKIT's file-based routing, a page file can act as a layout if it renders a `RouterView`.
172
-
173
- - `src/pages/home/settings.vue` owns the settings shell and wraps its child routes.
174
- - `src/pages/home/settings/index.vue` is just a redirect, so `/home/settings` lands on `/home/settings/general`.
175
- - `src/pages/home/settings/general/index.vue` is the first real child page created by the starter shell.
176
- - `src/pages/home/settings/profile/index.vue` becomes `/home/settings/profile` and still renders inside the layout from `settings.vue`.
177
-
178
- JSKIT uses a small helper for that redirect instead of hand-building the child path:
179
-
180
- ```js
181
- import { redirectToChild } from "@jskit-ai/kernel/client/pageRedirects";
182
-
183
- definePage({
184
- redirect: redirectToChild("general")
185
- });
186
- ```
187
-
188
- This is why the `page.section-nav` owner from `list-placements` is such a useful clue: it tells you which page is acting as the host.
189
-
190
- Even an `index.vue` page can have children. If you want an index page to stay visible while child routes render underneath it, put those children under an `index/` directory such as `src/pages/home/settings/profile/index/details.vue`.
191
-
192
- **Icons: Menu Metadata Is Not The Same As Vue Icon Props**
193
-
194
- When you start customizing generated pages and menu links, icons are one of the first details you usually add.
195
-
196
- There are two different paths to keep straight:
197
-
198
- 1. placement and menu metadata such as `src/placement.js`
199
- 2. direct Vuetify icon props inside normal `.vue` components
200
-
201
- For placement metadata, prefer importing app-specific icons from `@mdi/js` in the placement file itself. The shell menu runtime accepts the resolved SVG path value and passes it through to Vuetify:
202
-
203
- ```js
204
- import { mdiAccountCircleOutline } from "@mdi/js";
205
-
206
- addPlacement({
207
- id: "home.settings.profile.link",
208
- target: "page.section-nav",
209
- owner: "home-settings",
210
- kind: "link",
211
- surfaces: ["home"],
212
- props: {
213
- label: "Profile",
214
- to: "./profile",
215
- icon: mdiAccountCircleOutline
216
- }
217
- });
218
- ```
219
-
220
- Do not copy raw `mdi-*` strings into a normal Vue component:
221
-
222
- ```vue
223
- <v-list-item prepend-icon="mdi-account-circle-outline" />
224
- ```
225
-
226
- JSKIT apps use Vuetify's SVG MDI renderer, so direct Vue icon props should use the same `@mdi/js` path or a Vuetify alias instead:
227
-
228
- ```vue
229
- <script setup>
230
- import { mdiAccountCircleOutline } from "@mdi/js";
231
- </script>
232
-
233
- <v-list-item :prepend-icon="mdiAccountCircleOutline" />
234
- ```
235
-
236
- So the practical rule is:
237
-
238
- - editing `src/placement.js` or other shell menu metadata: import app-specific icons from `@mdi/js` and pass the constant
239
- - raw `mdi-*` metadata strings are only safe for shell-web's small core normalized icon map
240
- - editing a normal `.vue` file: use `@mdi/js` or a Vuetify alias such as `$close`
241
-
242
- Later in the guide, `jskit doctor` will help catch the second mistake automatically.
243
-
244
- ### Component tokens
245
-
246
- If you rerun the token listing, the widget command is the only command in this chapter that created an app-owned component token:
247
-
248
- ```bash
249
- npx jskit list-component-tokens --all --prefix local.main.
250
- ```
251
-
252
- The output includes:
253
-
254
- ```text
255
- - local.main.ui.element.alerts-widget
256
- - local.main.ui.menu-link-item
257
- - local.main.ui.surface-aware-menu-link-item
258
- - local.main.ui.tab-link-item
259
- ```
260
-
261
- That difference is useful. `npx jskit generate ui-generator placed-element` creates a new component token. `page` reuses the existing default link-item token for the parent outlet and adds a placement entry for the new route.
262
-
263
- So the shell story in this chapter is:
264
-
265
- - `ShellOutlet` defines named places where UI can appear
266
- - `jskit list-placements` shows those places
267
- - `shell-web` already uses those places for the starter `Home`, `Settings`, and `General` entries
268
- - `jskit generate ui-generator placed-element ...` creates app-owned UI and places it into one of them
269
- - `jskit generate ui-generator page ...` can also discover a parent outlet and add the right menu entry automatically
270
- - repeating that page command for the same host gives you a stacked menu, still without hand-editing the host page
271
- - nested pages can host placements too, not just the top shell
272
-
273
- That is the first real example of JSKIT behaving like an extension system rather than just a scaffold generator.
274
-
275
- ## What `shell-web` owns in the app
276
-
277
- The most interesting files look roughly like this:
278
-
279
- ```text
280
- src/
281
- components/
282
- AlertsWidgetElement.vue
283
- ShellLayout.vue
284
- menus/
285
- MenuLinkItem.vue
286
- SurfaceAwareMenuLinkItem.vue
287
- TabLinkItem.vue
288
- error.js
289
- placement.js
290
- pages/
291
- home.vue
292
- home/
293
- index.vue
294
- settings.vue
295
- settings/
296
- index.vue
297
- general/
298
- index.vue
299
- profile/
300
- index.vue
301
- notifications/
302
- index.vue
303
- ```
304
-
305
- This chapter is where the default scaffold starts to feel layered instead of flat.
306
-
307
- ### `package.json`
308
-
309
- The first file worth reopening is still `package.json`. Because the default app includes `shell-web`, the important shell dependency entries are already present:
310
-
311
- ```json
312
- {
313
- "dependencies": {
314
- "@jskit-ai/shell-web": "0.1.152",
315
- "@mdi/js": "^7.4.47"
316
- }
317
- }
318
- ```
319
-
320
- The important part is not just that `@jskit-ai/shell-web` appears. The package brings in shell runtime code and icon data; Vue Query is already owned by the base app bootstrap so every client package uses the same query client.
321
-
322
- It is also worth noticing what does **not** happen here. The `placed-element` and `page` commands from this chapter mutate app-owned files, but they do not add a permanent runtime dependency to `package.json`. They are tooling actions, not runtime package installs.
323
-
324
- The installed package exposes its runtime and generator metadata through
325
- `package.json.jskit`. The source created in the application is then reviewed
326
- and maintained as normal application code.
327
-
328
- ### The `home` surface gets a real wrapper
329
-
330
- The surface itself did not change. `home` is still the same surface defined in `config/public.js`. What changed is the page tree inside it.
331
-
332
- In the shell-web scaffold, `src/pages/home.vue` is more than a tiny route owner with a `RouterView`. It wraps the surface in the app-owned shell layout:
333
-
334
- ```vue
335
- <route lang="json">
336
- {
337
- "meta": {
338
- "jskit": {
339
- "surface": "home"
340
- }
341
- }
342
- }
343
- </route>
344
-
345
- <script setup>
346
- import ShellLayout from "@/components/ShellLayout.vue";
347
- import { RouterView } from "vue-router";
348
- </script>
349
-
350
- <template>
351
- <ShellLayout title="" subtitle="">
352
- <RouterView />
353
- </ShellLayout>
354
- </template>
355
- ```
356
-
357
- That one change explains a lot. The `home` surface is a shell-wrapped surface. Every child page under `src/pages/home/` renders inside that app-owned `ShellLayout`.
358
-
359
- ### `src/placement.js` is the placement registry
360
-
361
- The app has a placement registry file:
362
-
363
- ```js
364
- import { createPlacementRegistry } from "@jskit-ai/shell-web/client/placement";
365
-
366
- const registry = createPlacementRegistry();
367
- const { addPlacement } = registry;
368
-
369
- export { addPlacement };
370
-
371
- export default function getPlacements() {
372
- return registry.build();
373
- }
374
- ```
375
-
376
- That file is the app-owned seam for placements. `shell-web` owns the runtime that can render placements, but the app owns the registry source that lists what should appear in those targets.
377
-
378
- After the scaffold's shell entries plus the `placed-element` and `page` commands from this chapter, the bottom of the file contains real placement entries:
379
-
380
- ```js
381
- addPlacement({
382
- id: "shell-web.home.menu.home",
383
- target: "shell.primary-nav",
384
- kind: "link",
385
- surfaces: ["home"],
386
- order: 50,
387
- props: {
388
- label: "Home",
389
- surface: "home",
390
- scopedSuffix: "/",
391
- unscopedSuffix: "/",
392
- exact: true
393
- }
394
- });
395
-
396
- addPlacement({
397
- id: "shell-web.home.menu.settings",
398
- target: "shell.primary-nav",
399
- kind: "link",
400
- surfaces: ["home"],
401
- order: 100,
402
- props: {
403
- label: "Settings",
404
- surface: "home",
405
- scopedSuffix: "/settings",
406
- unscopedSuffix: "/settings"
407
- }
408
- });
409
-
410
- addPlacement({
411
- id: "shell-web.home.settings.general",
412
- target: "page.section-nav",
413
- owner: "home-settings",
414
- kind: "link",
415
- surfaces: ["home"],
416
- order: 100,
417
- props: {
418
- label: "General",
419
- surface: "home",
420
- scopedSuffix: "/settings/general",
421
- unscopedSuffix: "/settings/general",
422
- to: "./general"
423
- }
424
- });
425
-
426
- addPlacement({
427
- id: "ui-generator.element.alerts-widget",
428
- target: "shell.status",
429
- kind: "component",
430
- surfaces: ["home"],
431
- order: 155,
432
- componentToken: "local.main.ui.element.alerts-widget"
433
- });
434
-
435
- addPlacement({
436
- id: "ui-generator.page.home.settings.profile.link",
437
- target: "page.section-nav",
438
- owner: "home-settings",
439
- kind: "link",
440
- surfaces: ["home"],
441
- order: 155,
442
- props: {
443
- label: "Profile",
444
- surface: "home",
445
- scopedSuffix: "/settings/profile",
446
- unscopedSuffix: "/settings/profile",
447
- to: "./profile"
448
- }
449
- });
450
-
451
- addPlacement({
452
- id: "ui-generator.page.home.settings.notifications.link",
453
- target: "page.section-nav",
454
- owner: "home-settings",
455
- kind: "link",
456
- surfaces: ["home"],
457
- order: 155,
458
- props: {
459
- label: "Notifications",
460
- surface: "home",
461
- scopedSuffix: "/settings/notifications",
462
- unscopedSuffix: "/settings/notifications",
463
- to: "./notifications"
464
- }
465
- });
466
- ```
467
-
468
- That snippet shows the full placement contract clearly:
469
-
470
- - the target says which semantic placement should receive the entry
471
- - the owner disambiguates page-owned semantic placements such as `page.section-nav`
472
- - `kind: "link"` lets topology choose the concrete link renderer for the current layout
473
- - component placements still provide their own `componentToken`
474
- - `props.to` tells the generated menu link which child route to open
475
- - `props.icon`, when you add one, belongs to menu metadata rather than direct Vuetify icon rendering
476
- - the surface list says where it is active
477
- - lower `order` values come first
478
- - when multiple entries target the same semantic placement with the same order, the shell keeps their source order
479
-
480
- That is why the settings menu shows `General` first, followed by `Profile` and `Notifications`: `General` is seeded by `shell-web` with a lower order, while the two generated pages share the same later order and keep their source order.
481
-
482
- So the shell itself remains stable. What changes is the registry that feeds it.
483
-
484
- ### The local client provider publishes the app-owned components
485
-
486
- The placement registry only points at tokens. Those tokens still need to resolve to real Vue components somewhere. That happens in the app-local client provider in `packages/main/src/client/providers/MainClientProvider.js`.
487
-
488
- After the scaffold's shell registrations and the `Alerts Widget` generator command, that file contains registrations like these:
489
-
490
- ```js
491
- import AlertsWidgetElement from "/src/components/AlertsWidgetElement.vue";
492
-
493
- registerMainClientComponent("local.main.ui.element.alerts-widget", () => AlertsWidgetElement);
494
-
495
- registerMainClientComponent("local.main.ui.menu-link-item", () => MenuLinkItem);
496
- registerMainClientComponent("local.main.ui.surface-aware-menu-link-item", () => SurfaceAwareMenuLinkItem);
497
- registerMainClientComponent("local.main.ui.tab-link-item", () => TabLinkItem);
498
- ```
499
-
500
- This is the same app-local provider seam from the previous chapter, and this chapter shows why it matters. The provider is what lets the placement runtime resolve app-owned components by token instead of hard-coding imports inside the shell runtime.
501
-
502
- The `Profile` and `Notifications` pages did not need to add another provider registration because they reuse the existing `local.main.ui.surface-aware-menu-link-item` token for their menu entries.
503
-
504
- So the flow is:
505
-
506
- 1. a placement entry names a component token
507
- 2. the local client provider publishes that token
508
- 3. the `ShellOutlet` resolves it at runtime
509
-
510
- That is why the placement system feels dynamic even though the app still owns all of the concrete Vue files.
511
-
512
- ### `App.vue` and `error.js` provide shell-level errors
513
-
514
- The top-level app root includes the normal route outlet and the shell error host:
515
-
516
- ```vue
517
- <script setup>
518
- import { RouterView } from "vue-router";
519
- import ShellErrorHost from "@jskit-ai/shell-web/client/components/ShellErrorHost";
520
- </script>
521
-
522
- <template>
523
- <v-app>
524
- <RouterView />
525
- <ShellErrorHost />
526
- </v-app>
527
- </template>
528
- ```
529
-
530
- That host is backed by the app-owned `src/error.js` file:
531
-
532
- ```js
533
- import { createDefaultErrorPolicy } from "@jskit-ai/shell-web/client/error";
534
-
535
- export default Object.freeze({
536
- defaultPresenterId: "material.snackbar",
537
- policy: createDefaultErrorPolicy({
538
- resourceLoadChannel: "silent",
539
- actionFeedbackChannel: "snackbar",
540
- appRecoverableChannel: "banner",
541
- blockingChannel: "dialog"
542
- }),
543
- presenters: []
544
- });
545
- ```
546
-
547
- The idea is the same as with placements: `shell-web` provides the runtime, but the app owns the configuration file that the runtime reads.
548
-
549
- The default error policy is intent-based. Runtime code reports what kind of error happened and the app-owned policy decides the presentation:
550
-
551
- - `resource-load` uses `silent`; the screen keeps the load message and retry action local.
552
- - `action-feedback` uses `snackbar`; saves, commands, and other user-triggered actions report through one lightweight feedback channel.
553
- - `app-recoverable` uses `banner`; shell refresh and recoverable navigation failures stay visible without blocking the app.
554
- - `blocking` uses `dialog`; unexpected UI failures and other blocking errors require explicit attention.
555
-
556
- When app code catches a dynamic import failure itself, report it through the shell async module recovery runtime so it uses the same reload banner as router chunk failures:
557
-
558
- ```js
559
- import { useShellAsyncModuleRecoveryRuntime } from "@jskit-ai/shell-web/client/asyncModuleRecovery";
560
-
561
- const asyncModuleRecovery = useShellAsyncModuleRecoveryRuntime();
562
-
563
- try {
564
- await import("@xterm/xterm");
565
- } catch (error) {
566
- asyncModuleRecovery?.notify(error, {
567
- label: "Terminal"
568
- });
569
- }
570
- ```
571
-
572
- `useShellAsyncModuleRecoveryRuntime()` returns `null` when the shell runtime is not available in the current Vue context. That lets app-owned components use optional chaining instead of duplicating shell-web's internal injection token.
573
-
574
- Use the narrow `@jskit-ai/shell-web/client/asyncModuleRecovery` subpath for this runtime, especially from modules that are imported by Node-mode Vitest suites. The aggregate `@jskit-ai/shell-web/client` barrel also exports the composable for normal Vite app code, but that barrel includes `.vue` component exports and can require Vue SFC handling in tests.
575
-
576
- Request connectivity failures use a separate shell recovery path. Generated apps already configure TanStack Query to retry transient failures with capped backoff. `shell-web` then observes the app's `jskit.client.query-client` and, when an active safe-read query finishes in a transport failure such as `Network request failed.` or `Failed to fetch`, reports an `app-recoverable` banner with a `Retry` action that refetches that exact query. Normal HTTP validation and application errors stay local to the screen.
577
-
578
- That recovery path is intentionally a safe `GET`/`HEAD` read refetch system, not a general HTTP replay system. User-visible reads should go through Query-backed JSKIT primitives such as `useEndpointResource()`, `useList()`, `useView()`, `useAddEdit()`, or generated CRUD screen composables. Those primitives mark Query entries with `jskit.requestRecoveryMethod`, so the shell only offers Retry for safe reads. Do not catch raw `fetch(...)` failures in each panel just to call the shell recovery runtime manually.
579
-
580
- These neutral request and CRUD client APIs are exported by `@jskit-ai/http-web`. They do not require the users, authentication, uploads, storage, or database products.
581
-
582
- For a custom endpoint read, attach the recovery label to the Query-backed resource:
583
-
584
- ```js
585
- const projectAccess = useEndpointResource({
586
- queryKey: ["project-access", projectId],
587
- path: `/api/projects/${projectId}/access`,
588
- requestRecoveryLabel: "Project access"
589
- });
590
- ```
591
-
592
- If you need lower-level Query options, the same metadata can live on query meta:
593
-
594
- ```js
595
- const projectAccess = useEndpointResource({
596
- queryKey: ["project-access", projectId],
597
- path: `/api/projects/${projectId}/access`,
598
- queryOptions: {
599
- meta: {
600
- jskit: {
601
- requestRecoveryLabel: "Project access",
602
- requestRecoveryMethod: "GET"
603
- }
604
- }
605
- }
606
- });
607
- ```
608
-
609
- For JSKIT read-composable screens, the default is automatic. Hand-written TanStack Query reads outside those composables must set `meta.jskit.requestRecoveryMethod` to `GET` or `HEAD`; unmarked queries are ignored by the shell recovery observer. Set `meta: { jskit: { requestRecovery: false } }` only when a query deliberately owns its entire connectivity recovery UI.
610
-
611
- Writes are different. JSKIT does not automatically replay `POST`, `PATCH`, `PUT`, or `DELETE` after a network failure because the server may already have received the request. Save and command screens keep ownership of mutation state, field errors, conflict handling, and user feedback.
612
-
613
- Some apps need API URLs to be scoped by the active route before the browser request is sent. Configure that once at app startup instead of replacing `fetchImpl` in a local transport wrapper:
614
-
615
- ```js
616
- import { configureHttpWebClient } from "@jskit-ai/http-web/client/lib/httpClient";
617
-
618
- configureHttpWebClient({
619
- csrf: {
620
- enabled: false
621
- },
622
- resolveRequestUrl(url, context) {
623
- if (!url.startsWith("/api/")) {
624
- return url;
625
- }
626
-
627
- const projectSlug = readProjectSlugFromAppRoute();
628
- return url.replace(/^\/api\//u, `/api/app/${encodeURIComponent(projectSlug)}/`);
629
- }
630
- });
631
- ```
632
-
633
- Call `configureHttpWebClient()` before Vue mounts or before JSKIT composables are created. The resolver can close over the app router/store when it needs route data, and the `context` argument carries request details such as `originalUrl`, `method`, `requestOptions`, and whether the request is a stream. After configuration, normal `useEndpointResource()`, `useList()`, `useView()`, `useAddEdit()`, and `useCommand()` calls use the configured client. `resolveRequestUrl` runs after JSKIT adds query strings and before the underlying browser `fetch`, so request recovery metadata, JSON:API transport, credentials, CSRF, and command feedback stay on the standard path.
634
-
635
- For packages that create their own client, use the same lower-level hook directly:
636
-
637
- ```js
638
- import { createTransientRetryHttpClient } from "@jskit-ai/http-runtime/client";
639
-
640
- const studioHttpClient = createTransientRetryHttpClient({
641
- credentials: "include",
642
- csrf: {
643
- enabled: false
644
- },
645
- resolveRequestUrl(url) {
646
- return scopedStudioApiUrl(url);
647
- }
648
- });
649
- ```
650
-
651
- ### The home page talks to the backend
652
-
653
- `src/pages/home/index.vue` uses Vue Query to fetch `/api/health` and display the result in the UI.
654
-
655
- That is why this chapter keeps running both `npm run dev` and `npm run server`. The page expects the backend to be alive.
656
-
657
- This matters because it is a tiny example of the frontend and backend participating in the same shell. The request itself is simple, but it proves the shell can surface runtime status instead of only rendering static chrome.
658
-
659
- ### The first client stores appear
660
-
661
- `shell-web` also exposes app-facing Pinia stores:
662
-
663
- ```js
664
- import {
665
- useShellLayoutStore,
666
- useShellErrorPresentationStore
667
- } from "@jskit-ai/shell-web/client";
668
- ```
669
-
670
- `useShellLayoutStore()` owns the shell drawer state:
671
-
672
- - whether the drawer is open right now
673
- - whether the drawer should open by default on load
674
-
675
- The closed presentation is adaptive and uses Vuetify's Material navigation
676
- components. On compact/mobile layouts the temporary drawer closes completely.
677
- On medium and expanded layouts it collapses to a navigation rail by default,
678
- so primary navigation remains visible. Opening it restores the full drawer.
679
- Escape and outside dismissal close only the compact temporary drawer and return
680
- focus to the shell navigation toggle. They do not collapse permanent desktop
681
- navigation.
682
-
683
- The open drawer is content-aware by default. After the visible semantic
684
- placements and fonts settle, `ShellLayout` measures the rendered labels and
685
- uses Material's 12px outer item inset and one `navigationItemSpacing` value for
686
- the icon-to-label gap and the space between the widest label and the drawer
687
- edge. The spacing defaults to 12 CSS pixels and can be set from 8–24px. The
688
- shell remeasures when placements, localization, fonts, spacing, the active
689
- surface, or the window layout change, and clamps unusually long labels to a safe range. The
690
- surface label appears once in the top app bar; the drawer starts with its
691
- destinations instead of repeating that label as an uppercase subheader.
692
-
693
- The closed desktop rail remains 80 CSS pixels wide by default, with every icon
694
- and its Material selected-state indicator centred in the rail. The expanded
695
- drawer uses the same icon centreline, so toggling the drawer reveals or hides
696
- labels without making the icons jump sideways. The empty space around a 24px
697
- icon in the default rail is intentional Material touch/indicator space, not a
698
- second drawer padding. Products that deliberately need a denser rail can set
699
- `railWidth`; the shell still enforces a 48px minimum target. Rail tooltips use
700
- one explicit opaque theme color pair and open on pointer hover or keyboard
701
- focus.
702
-
703
- The app-owned `ShellLayout` can opt into a fully hidden wide drawer when the
704
- product has another discoverable navigation affordance:
705
-
706
- ```vue
707
- <ShellLayout desktop-drawer-closed-mode="hidden">
708
- <RouterView />
709
- </ShellLayout>
710
- ```
711
-
712
- The default is `desktop-drawer-closed-mode="rail"`. Prefer that Material 3
713
- pattern; do not imitate a rail with custom CSS or a second menu registry.
714
-
715
- Applications that need product-specific dimensions or spacing can use the
716
- public props instead of overriding Vuetify drawer CSS:
717
-
718
- ```vue
719
- <ShellLayout :drawer-width="280" :rail-width="64" :navigation-item-spacing="12">
720
- <RouterView />
721
- </ShellLayout>
722
- ```
723
-
724
- Omit `drawer-width` to retain content-aware sizing. Omit `rail-width` to retain
725
- the 80px Material navigation-rail default. Drawer overrides are clamped to
726
- 120–360px and rail overrides to 48–160px so shell navigation cannot consume an
727
- unsafe share of the viewport or shrink below its touch targets. The app-owned
728
- `src/components/ShellLayout.vue` wrapper also forwards
729
- `navigation-item-spacing`, so an application does not need to copy or restyle
730
- the drawer implementation.
731
-
732
- ### Updating an existing shell app
733
-
734
- Commit the app's work and run:
5
+ Use `@jskit-ai/shell-web` when the product needs responsive navigation,
6
+ semantic placements, settings sections, and a consistent application error
7
+ host.
735
8
 
736
9
  ```bash
737
- npm run jskit:update
738
- ```
739
-
740
- Keep the generated/app-owned `src/components/ShellLayout.vue` wrapper and let
741
- it continue forwarding attributes to the package component. No copied drawer,
742
- `.v-navigation-drawer` CSS, duplicate surface heading, or replacement smoke
743
- test is required. If the product intentionally wants a denser desktop rail,
744
- set the supported prop in that wrapper, for example `:rail-width="64"`; omit it
745
- to keep the Material 3 80px default.
746
-
747
- `useShellErrorPresentationStore()` exposes the current banner, snackbar, and dialog presentation state behind `ShellErrorHost`.
748
-
749
- The simplest direct store usage looks like this:
750
-
751
- ```vue
752
- <script setup>
753
- import { computed } from "vue";
754
- import { useShellLayoutStore } from "@jskit-ai/shell-web/client";
755
-
756
- const shellLayout = useShellLayoutStore();
757
-
758
- const drawerDefaultOpenModel = computed({
759
- get() {
760
- return shellLayout.drawerDefaultOpen;
761
- },
762
- set(value) {
763
- shellLayout.setDrawerDefaultOpen(Boolean(value));
764
- }
765
- });
766
- </script>
10
+ npm install @jskit-ai/shell-web
767
11
  ```
768
12
 
769
- That is the raw shared store behind the starter `General` settings page. It is just normal Pinia state and actions: read `drawerDefaultOpen`, write it back through `setDrawerDefaultOpen(...)`, and the shell reacts.
13
+ Installing the package supplies runtime APIs; it does not rewrite the app. Use
14
+ the `shell/application-shell` pattern for a complete shell or
15
+ `ui/page-and-placement` for a smaller addition to an existing shell.
770
16
 
771
- The same store also exposes the live drawer state through `drawerOpen`, plus `setDrawerOpen(...)` and `toggleDrawer()` for components that need to control the drawer directly instead of only changing its default preference.
17
+ ## The shell contract
772
18
 
773
- You do not need either store very often in this chapter, because `shell-web` already mounts the shell and error host for you. The starter `General` settings page uses the higher-level `useShellLayoutState()` helper instead of talking to `useShellLayoutStore()` directly, because that helper combines the store-backed drawer state with the current route and surface context that `ShellLayout` also needs.
19
+ - File routes remain application source.
20
+ - Placements describe semantic destinations such as primary navigation,
21
+ section navigation, profile controls, and status elements.
22
+ - Topology maps those placements to concrete outlets at compact, medium, and
23
+ expanded widths.
24
+ - App-owned components are registered by stable component id through the
25
+ client provider.
26
+ - Navigation links use the shell link components so current-route and surface
27
+ behavior stays consistent.
774
28
 
775
- That distinction is worth noticing:
29
+ This is composition, not code injection. There is one visible placement
30
+ registry and one route tree to inspect.
776
31
 
777
- - runtime services still do the operational work
778
- - Pinia stores are the normal Vue-facing shared-state surface
779
-
780
- ### The first settings route appears
781
-
782
- `shell-web` also creates a settings shell for the `home` surface:
783
-
784
- ```text
785
- src/pages/home/settings.vue
786
- src/pages/home/settings/index.vue
787
- src/pages/home/settings/general/index.vue
788
- ```
789
-
790
- The important host file is still `src/pages/home/settings.vue`:
791
-
792
- ```vue
793
- <v-list nav density="comfortable" rounded="lg" border>
794
- <ShellOutlet target="home-settings:primary-menu" />
795
- </v-list>
796
-
797
- <RouterView />
798
- ```
799
-
800
- This file matters for the same reason as `ShellLayout.vue`: it creates another named extension point instead of hard-coding a finished settings UI. The difference is that this one lives inside a page, not at the top shell level.
801
-
802
- The starter shell uses a real child-page structure right away:
803
-
804
- - `src/pages/home/settings/index.vue` is only a redirect into the first child page
805
- - `src/pages/home/settings/general/index.vue` is the first real settings page
806
- - `src/placement.js` already seeds a `General` link into `page.section-nav` with owner `home-settings`
807
-
808
- When you need that landing redirect yourself, use the same helper pattern:
809
-
810
- ```js
811
- import { redirectToChild } from "@jskit-ai/kernel/client/pageRedirects";
812
-
813
- definePage({
814
- redirect: redirectToChild("general")
815
- });
816
- ```
32
+ ## Product decisions
817
33
 
818
- That is what makes the page-generation examples in this chapter important. They are not inventing a new pattern. They are extending the exact same host-and-child-page structure that `shell-web` already uses for its own starter `General` page.
34
+ Choose the surfaces, routes, navigation hierarchy, labels, icons, section
35
+ ownership, ordering, and compact behavior. Do not infer primary navigation from
36
+ every route: detail and workflow pages usually should not appear there.
819
37
 
820
- ### What did not change
38
+ ## UI invariants
821
39
 
822
- It is also worth being explicit about what `shell-web` does **not** do yet:
40
+ - Compact controls have accessible names and at least 48 CSS-pixel targets.
41
+ - Loading uses layout-stable skeletons, never indeterminate spinners.
42
+ - Mutation failures use the standard toast instead of banners that push the
43
+ page down.
44
+ - Cached route or resource data hydrates writable state immediately.
45
+ - Browser back/forward restores the selected route and screen context.
823
46
 
824
- - it does not add authentication
825
- - it does not add a database
826
- - it does not add new surfaces
827
- - it does not change the local server provider model
47
+ ## Adaptive drawer
828
48
 
829
- The app is still structurally simple. `shell-web` just makes that simple app behave like a shell instead of a loose page.
49
+ Use Vuetify Material navigation. On compact/mobile layouts, close dismisses the
50
+ temporary drawer. Wide layouts normally use
51
+ `desktopDrawerClosedMode="rail"`; choose `hidden` only when another navigation
52
+ affordance remains. The public `drawerWidth`, `railWidth`, and
53
+ `navigationItemSpacing` controls own density and spacing. The standard 80px
54
+ rail centres 48px targets, and drawer items keep an outer inset of 12 CSS pixels.
55
+ Do not override private shell CSS to imitate these states.
830
56
 
831
- ## Summary
57
+ ## Verification
832
58
 
833
- After this chapter, the app is still small, but you have inspected the shell shape it starts with. `shell-web` provides an app-owned shell layout, a placement registry, a shell error host, menu-link tokens in the local client provider, real drawer navigation, and the first nested settings section under `home`.
59
+ Exercise direct URLs, link navigation, current-link state, keyboard operation,
60
+ warm-cache return navigation, and compact/medium/expanded widths. Run the
61
+ client tests and production build.
834
62
 
835
- More importantly, this chapter is where placements stop being theory. The shell already uses them for `Home`, `Settings`, and the starter `General` settings page. Then you inspect the available targets, place real UI into the outer shell, and add more child settings pages that automatically land in the nested settings menu. That is the first real example of JSKIT working as an extension system rather than just a scaffold generator.
63
+ Do not add a second navigation registry, generator markers, placement receipts,
64
+ or source-append machinery.