@jskit-ai/agent-docs 0.1.130 → 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.
- package/guide/agent/app-extras/assistant.md +29 -605
- package/guide/agent/app-extras/mobile-capacitor.md +29 -362
- package/guide/agent/app-extras/realtime.md +29 -277
- package/guide/agent/app-setup/a-more-interesting-shell.md +44 -815
- package/guide/agent/app-setup/authentication.md +43 -1073
- package/guide/agent/app-setup/console.md +26 -298
- package/guide/agent/app-setup/database-layer.md +110 -790
- package/guide/agent/app-setup/initial-scaffolding.md +50 -784
- package/guide/agent/app-setup/multi-homing.md +39 -712
- package/guide/agent/app-setup/quickstart.md +43 -179
- package/guide/agent/app-setup/users.md +34 -353
- package/guide/agent/index.md +16 -23
- package/package.json +2 -2
- package/patterns/INDEX.md +7 -7
- package/patterns/child-cruds.md +3 -3
- package/patterns/client-requests.md +6 -6
- package/patterns/crud-authoring.md +94 -0
- package/patterns/crud-links.md +1 -1
- package/patterns/feature-package/PATTERN.md +108 -0
- package/patterns/feature-package/example/booking-engine/package.json +48 -0
- package/patterns/feature-package/example/booking-engine/src/server/BookingEngineProvider.js +33 -0
- package/patterns/feature-package/example/booking-engine/src/server/actions.js +26 -0
- package/patterns/feature-package/example/booking-engine/src/server/inputSchemas.js +19 -0
- package/patterns/feature-package/example/variations/AvailabilityEngineProvider.js +31 -0
- package/patterns/feature-package/example/variations/InvoiceRollupProvider.js +36 -0
- package/patterns/feature-package/example/variations/customKnexRepository.js +34 -0
- package/patterns/feature-package/example/variations/orchestratorService.js +23 -0
- package/patterns/filters.md +8 -8
- package/patterns/live-actions.md +5 -18
- package/patterns/minimal-foundation/PATTERN.md +98 -0
- package/patterns/minimal-foundation/example/.nvmrc +1 -0
- package/patterns/minimal-foundation/example/AGENTS.md +17 -0
- package/patterns/minimal-foundation/example/Procfile +2 -0
- package/patterns/minimal-foundation/example/app.json +14 -0
- package/patterns/minimal-foundation/example/bin/develop.js +71 -0
- package/patterns/minimal-foundation/example/bin/server.js +8 -0
- package/patterns/minimal-foundation/example/config/public.js +40 -0
- package/patterns/minimal-foundation/example/config/server.js +1 -0
- package/patterns/minimal-foundation/example/config/surfaceAccessPolicies.js +3 -0
- package/patterns/minimal-foundation/example/eslint.config.mjs +19 -0
- package/patterns/minimal-foundation/example/favicon.svg +7 -0
- package/patterns/minimal-foundation/example/gitignore +9 -0
- package/patterns/minimal-foundation/example/index.html +13 -0
- package/patterns/minimal-foundation/example/jsconfig.json +8 -0
- package/patterns/minimal-foundation/example/package.json +57 -0
- package/patterns/minimal-foundation/example/packages/main/package.json +42 -0
- package/patterns/minimal-foundation/example/packages/main/src/shared/index.js +10 -0
- package/patterns/minimal-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
- package/patterns/minimal-foundation/example/playwright.config.mjs +31 -0
- package/patterns/minimal-foundation/example/server/lib/runtimeEnv.js +45 -0
- package/patterns/minimal-foundation/example/server/lib/surfaceRuntime.js +10 -0
- package/patterns/minimal-foundation/example/server.js +195 -0
- package/patterns/minimal-foundation/example/src/App.vue +13 -0
- package/patterns/minimal-foundation/example/src/main.js +85 -0
- package/patterns/minimal-foundation/example/src/pages/home/index.vue +48 -0
- package/patterns/minimal-foundation/example/src/pages/home.vue +13 -0
- package/patterns/minimal-foundation/example/src/views/NotFound.vue +13 -0
- package/patterns/minimal-foundation/example/tests/client/smoke.vitest.js +7 -0
- package/patterns/minimal-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
- package/patterns/minimal-foundation/example/tests/server/smoke.test.js +16 -0
- package/patterns/minimal-foundation/example/vite.config.mjs +81 -0
- package/patterns/page-scaffolding.md +20 -17
- package/patterns/placements.md +17 -15
- package/patterns/row-policies.md +4 -5
- package/patterns/server-search.md +3 -3
- package/patterns/shell-foundation/PATTERN.md +104 -0
- package/patterns/shell-foundation/example/.nvmrc +1 -0
- package/patterns/shell-foundation/example/AGENTS.md +17 -0
- package/patterns/shell-foundation/example/Procfile +2 -0
- package/patterns/shell-foundation/example/app.json +14 -0
- package/patterns/shell-foundation/example/bin/develop.js +71 -0
- package/patterns/shell-foundation/example/bin/server.js +8 -0
- package/patterns/shell-foundation/example/config/public.js +40 -0
- package/patterns/shell-foundation/example/config/server.js +1 -0
- package/patterns/shell-foundation/example/config/surfaceAccessPolicies.js +3 -0
- package/patterns/shell-foundation/example/eslint.config.mjs +19 -0
- package/patterns/shell-foundation/example/favicon.svg +7 -0
- package/patterns/shell-foundation/example/gitignore +9 -0
- package/patterns/shell-foundation/example/index.html +13 -0
- package/patterns/shell-foundation/example/jsconfig.json +8 -0
- package/patterns/shell-foundation/example/package.json +59 -0
- package/patterns/shell-foundation/example/packages/main/package.json +56 -0
- package/patterns/shell-foundation/example/packages/main/src/client/index.js +9 -0
- package/patterns/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +18 -0
- package/patterns/shell-foundation/example/packages/main/src/shared/index.js +10 -0
- package/patterns/shell-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
- package/patterns/shell-foundation/example/playwright.config.mjs +31 -0
- package/patterns/shell-foundation/example/server/lib/runtimeEnv.js +45 -0
- package/patterns/shell-foundation/example/server/lib/surfaceRuntime.js +10 -0
- package/patterns/shell-foundation/example/server.js +195 -0
- package/patterns/shell-foundation/example/src/App.vue +11 -0
- package/patterns/shell-foundation/example/src/components/ShellLayout.vue +12 -0
- package/patterns/shell-foundation/example/src/components/menus/MenuLinkItem.vue +30 -0
- package/patterns/shell-foundation/example/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
- package/patterns/shell-foundation/example/src/components/menus/TabLinkItem.vue +42 -0
- package/patterns/shell-foundation/example/src/error.js +19 -0
- package/patterns/shell-foundation/example/src/main.js +85 -0
- package/patterns/shell-foundation/example/src/pages/home/index.vue +116 -0
- package/patterns/shell-foundation/example/src/pages/home/settings/general/index.vue +40 -0
- package/patterns/shell-foundation/example/src/pages/home/settings/index.vue +7 -0
- package/patterns/shell-foundation/example/src/pages/home/settings.vue +109 -0
- package/patterns/shell-foundation/example/src/pages/home.vue +20 -0
- package/patterns/shell-foundation/example/src/placement.js +56 -0
- package/patterns/shell-foundation/example/src/placementTopology.js +149 -0
- package/patterns/shell-foundation/example/src/views/NotFound.vue +13 -0
- package/patterns/shell-foundation/example/tests/client/smoke.vitest.js +7 -0
- package/patterns/shell-foundation/example/tests/e2e/adaptive-shell.spec.ts +10 -0
- package/patterns/shell-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
- package/patterns/shell-foundation/example/tests/server/smoke.test.js +16 -0
- package/patterns/shell-foundation/example/vite.config.mjs +81 -0
- package/patterns/ui-contract.md +56 -0
- package/patterns/ui-testing.md +10 -12
- package/reference/autogen/KERNEL_MAP.md +29 -107
- package/reference/autogen/PATTERN_INDEX.md +230 -0
- package/reference/autogen/README.md +4 -8
- package/reference/autogen/packages/agent-docs.md +259 -0
- package/reference/autogen/packages/assistant-core.md +3 -3
- package/reference/autogen/packages/assistant-runtime.md +32 -17
- package/reference/autogen/packages/auth-core.md +31 -33
- package/reference/autogen/packages/auth-provider-local-core.md +4 -12
- package/reference/autogen/packages/auth-provider-local-db-core.md +4 -4
- package/reference/autogen/packages/auth-provider-supabase-core.md +14 -18
- package/reference/autogen/packages/auth-web.md +42 -22
- package/reference/autogen/packages/console-core.md +8 -25
- package/reference/autogen/packages/console-web.md +5 -5
- package/reference/autogen/packages/crud-core.md +61 -17
- package/reference/autogen/packages/database-runtime-mysql.md +12 -2
- package/reference/autogen/packages/database-runtime-postgres.md +12 -2
- package/reference/autogen/packages/database-runtime.md +26 -25
- package/reference/autogen/packages/google-rewarded-core.md +19 -104
- package/reference/autogen/packages/http-runtime.md +4 -8
- package/reference/autogen/packages/http-web.md +32 -0
- package/reference/autogen/packages/json-rest-api-core.md +4 -6
- package/reference/autogen/packages/kernel.md +109 -390
- package/reference/autogen/packages/mobile-capacitor.md +2 -13
- package/reference/autogen/packages/realtime.md +29 -26
- package/reference/autogen/packages/resource-crud-core.md +6 -0
- package/reference/autogen/packages/shell-web.md +69 -54
- package/reference/autogen/packages/storage-runtime.md +3 -3
- package/reference/autogen/packages/uploads-image-web.md +0 -1
- package/reference/autogen/packages/uploads-runtime.md +3 -3
- package/reference/autogen/packages/users-core.md +45 -90
- package/reference/autogen/packages/users-web.md +5 -7
- package/reference/autogen/packages/workspaces-core.md +53 -74
- package/reference/autogen/packages/workspaces-web.md +15 -16
- package/reference/autogen/tooling/jskit-catalog.md +34 -0
- package/reference/autogen/tooling/testUtils.md +4 -4
- package/skills/jskit/SKILL.md +36 -28
- package/skills/jskit/agents/openai.yaml +2 -2
- package/skills/jskit/references/app-operations.md +68 -53
- package/skills/jskit/references/crud-operations.md +58 -106
- package/skills/jskit/references/material-3.md +105 -0
- package/skills/jskit/references/ui-operations.md +41 -44
- package/templates/app/AGENTS.md +7 -3
- package/guide/agent/app-setup/upgrade-beta-1-to-final.md +0 -252
- package/guide/agent/app-setup/working-with-the-jskit-cli.md +0 -325
- package/guide/agent/generators/advanced-cruds.md +0 -1935
- package/guide/agent/generators/crud-generators.md +0 -948
- package/guide/agent/generators/intro.md +0 -65
- package/guide/agent/generators/row-policies.md +0 -537
- package/guide/agent/generators/ui-generators.md +0 -690
- package/patterns/crud-scaffolding.md +0 -198
- package/patterns/generated-ui-contract-tracking.md +0 -66
- package/reference/autogen/packages/assistant.md +0 -68
- package/reference/autogen/packages/crud-server-generator.md +0 -215
- package/reference/autogen/packages/crud-ui-generator.md +0 -192
- package/reference/autogen/packages/feature-server-generator.md +0 -65
- package/reference/autogen/packages/ui-generator.md +0 -127
- package/reference/autogen/tooling/create-app.md +0 -317
- package/reference/autogen/tooling/jskit-cli.md +0 -933
- package/reference/autogen/tooling/test-support.md +0 -27
|
@@ -2,834 +2,63 @@
|
|
|
2
2
|
|
|
3
3
|
# A more interesting shell
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
17
|
+
## The shell contract
|
|
772
18
|
|
|
773
|
-
|
|
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
|
-
|
|
29
|
+
This is composition, not code injection. There is one visible placement
|
|
30
|
+
registry and one route tree to inspect.
|
|
776
31
|
|
|
777
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
+
## UI invariants
|
|
821
39
|
|
|
822
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
57
|
+
## Verification
|
|
832
58
|
|
|
833
|
-
|
|
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
|
-
|
|
63
|
+
Do not add a second navigation registry, generator markers, placement receipts,
|
|
64
|
+
or source-append machinery.
|