@jskit-ai/agent-docs 0.1.279 → 0.1.281

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 (138) hide show
  1. package/guide/agent/app-extras/assistant-conversation.md +5 -1
  2. package/guide/agent/app-extras/assistant.md +123 -0
  3. package/guide/agent/app-extras/realtime.md +20 -10
  4. package/guide/agent/app-setup/authentication.md +54 -4
  5. package/guide/agent/app-setup/database-layer.md +108 -35
  6. package/guide/agent/app-setup/existing-application-migration.md +7 -1
  7. package/guide/agent/app-setup/material-3-migration.md +38 -9
  8. package/guide/agent/app-setup/multi-homing.md +14 -4
  9. package/guide/agent/app-setup/upgrading-jskit.md +105 -0
  10. package/guide/agent/app-setup/users.md +9 -1
  11. package/guide/agent/framework/client-features.md +35 -1
  12. package/guide/agent/framework/material-3.md +111 -71
  13. package/guide/agent/framework/sanity-checks.md +181 -0
  14. package/guide/agent/framework/translation.md +4 -0
  15. package/guide/agent/framework/typed-features.md +222 -120
  16. package/guide/agent/framework/ui-operations.md +3 -1
  17. package/guide/agent/index.md +3 -0
  18. package/package.json +2 -2
  19. package/patterns/feature-package/example/booking-engine/package.json +1 -1
  20. package/patterns/minimal-foundation/PATTERN.md +6 -2
  21. package/patterns/minimal-foundation/example/AGENTS.md +22 -5
  22. package/patterns/minimal-foundation/example/package.json +6 -6
  23. package/patterns/minimal-foundation/example/playwright.config.mjs +3 -30
  24. package/patterns/minimal-foundation/example/server.js +7 -1
  25. package/patterns/minimal-foundation/example/src/main.js +3 -2
  26. package/patterns/minimal-foundation/example/tests/e2e/startup.spec.ts +13 -0
  27. package/patterns/minimal-foundation/example/vite.config.mjs +3 -1
  28. package/patterns/shell-foundation/PATTERN.md +4 -1
  29. package/patterns/shell-foundation/example/AGENTS.md +22 -5
  30. package/patterns/shell-foundation/example/package.json +7 -7
  31. package/patterns/shell-foundation/example/packages/main/package.json +1 -1
  32. package/patterns/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +3 -0
  33. package/patterns/shell-foundation/example/playwright.config.mjs +3 -30
  34. package/patterns/shell-foundation/example/server.js +7 -1
  35. package/patterns/shell-foundation/example/src/components/menus/SectionTabLinkItem.vue +42 -0
  36. package/patterns/shell-foundation/example/src/main.js +3 -2
  37. package/patterns/shell-foundation/example/src/pages/home.vue +1 -1
  38. package/patterns/shell-foundation/example/src/views/NotFound.vue +1 -1
  39. package/patterns/shell-foundation/example/tests/e2e/startup.spec.ts +13 -0
  40. package/patterns/shell-foundation/example/vite.config.mjs +3 -1
  41. package/reference/autogen/PATTERN_INDEX.md +37 -37
  42. package/reference/autogen/packages/agent-docs.md +4 -0
  43. package/reference/autogen/packages/architecture-inspector.md +1 -0
  44. package/reference/autogen/packages/assistant-runtime.md +28 -3
  45. package/reference/autogen/packages/auth-provider-local-core.md +2 -1
  46. package/reference/autogen/packages/auth-web.md +10 -0
  47. package/reference/autogen/packages/connectors-catalog.md +8 -0
  48. package/reference/autogen/packages/crud-core.md +3 -0
  49. package/reference/autogen/packages/database-runtime.md +33 -0
  50. package/reference/autogen/packages/http-web.md +0 -4
  51. package/reference/autogen/packages/json-rest-api-core.md +17 -4
  52. package/reference/autogen/packages/kernel.md +37 -2
  53. package/reference/autogen/packages/material-web.md +2 -1
  54. package/reference/autogen/packages/oidc-issuer-core.md +0 -1
  55. package/reference/autogen/packages/realtime.md +8 -5
  56. package/reference/autogen/packages/shell-web.md +88 -3
  57. package/reference/autogen/packages/users-web.md +5 -2
  58. package/reference/autogen/packages/workspaces-core.md +3 -1
  59. package/reference/autogen/tooling/jskit-catalog.md +122 -5
  60. package/reference/autogen/tooling/testUtils.md +15 -1
  61. package/skills/jskit/SKILL.md +17 -5
  62. package/skills/jskit/references/client-features.md +35 -1
  63. package/skills/jskit/references/existing-application-migration.md +7 -1
  64. package/skills/jskit/references/heuristics/INDEX.md +3 -1
  65. package/skills/jskit/references/heuristics/row-policies.md +3 -3
  66. package/skills/jskit/references/heuristics/typed-features.md +7 -7
  67. package/skills/jskit/references/heuristics/ui-testing.md +27 -21
  68. package/skills/jskit/references/material-3.md +111 -71
  69. package/skills/jskit/references/multi-homing.md +14 -4
  70. package/skills/jskit/references/pattern-index.md +37 -37
  71. package/skills/jskit/references/patterns/app/minimal-foundation/PATTERN.md +6 -2
  72. package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +22 -5
  73. package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +6 -6
  74. package/skills/jskit/references/patterns/app/minimal-foundation/example/playwright.config.mjs +3 -30
  75. package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +7 -1
  76. package/skills/jskit/references/patterns/app/minimal-foundation/example/src/main.js +3 -2
  77. package/skills/jskit/references/patterns/app/minimal-foundation/example/tests/e2e/startup.spec.ts +13 -0
  78. package/skills/jskit/references/patterns/app/minimal-foundation/example/vite.config.mjs +3 -1
  79. package/skills/jskit/references/patterns/app/shell-foundation/PATTERN.md +4 -1
  80. package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +22 -5
  81. package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +7 -7
  82. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
  83. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +3 -0
  84. package/skills/jskit/references/patterns/app/shell-foundation/example/playwright.config.mjs +3 -30
  85. package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +7 -1
  86. package/skills/jskit/references/patterns/app/shell-foundation/example/src/components/menus/SectionTabLinkItem.vue +42 -0
  87. package/skills/jskit/references/patterns/app/shell-foundation/example/src/main.js +3 -2
  88. package/skills/jskit/references/patterns/app/shell-foundation/example/src/pages/home.vue +1 -1
  89. package/skills/jskit/references/patterns/app/shell-foundation/example/src/views/NotFound.vue +1 -1
  90. package/skills/jskit/references/patterns/app/shell-foundation/example/tests/e2e/startup.spec.ts +13 -0
  91. package/skills/jskit/references/patterns/app/shell-foundation/example/vite.config.mjs +3 -1
  92. package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +2 -2
  93. package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +18 -5
  94. package/skills/jskit/references/patterns/auth/auth-surface/example/src/components/AppBrand.vue +17 -0
  95. package/skills/jskit/references/patterns/auth/auth-surface/example/src/pages/auth/login.vue +2 -2
  96. package/skills/jskit/references/patterns/auth/auth-surface/example/src/pages/auth/reset-password.vue +2 -2
  97. package/skills/jskit/references/patterns/auth/auth-surface/example/src/pages/auth/signout.vue +2 -2
  98. package/skills/jskit/references/patterns/auth/auth-surface/example/src/views/auth/LoginView.vue +6 -1
  99. package/skills/jskit/references/patterns/auth/auth-surface/example/src/views/auth/ResetPasswordView.vue +6 -1
  100. package/skills/jskit/references/patterns/auth/auth-surface/example/src/views/auth/SignOutView.vue +6 -1
  101. package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
  102. package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
  103. package/skills/jskit/references/patterns/connectors/aws-storage-queries/example/formats/package.json +1 -1
  104. package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
  105. package/skills/jskit/references/patterns/console/console-surface/example/src/pages/console.vue +1 -1
  106. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/package.json +2 -2
  107. package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +34 -24
  108. package/skills/jskit/references/patterns/database/mysql-application/example/package.json +3 -2
  109. package/skills/jskit/references/patterns/database/mysql-application/example/tests/e2e/databases.js +25 -0
  110. package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +12 -0
  111. package/skills/jskit/references/patterns/database/postgres-application/example/package.json +3 -2
  112. package/skills/jskit/references/patterns/database/postgres-application/example/tests/e2e/databases.js +25 -0
  113. package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
  114. package/skills/jskit/references/patterns/rewards/google-rewarded/example/googlePublisherTag.js +5 -2
  115. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
  116. package/skills/jskit/references/patterns/shell/application-shell/PATTERN.md +11 -6
  117. package/skills/jskit/references/patterns/shell/application-shell/example/src/components/menus/SectionTabLinkItem.vue +42 -0
  118. package/skills/jskit/references/patterns/shell/application-shell/example/src/pages/home.vue +1 -1
  119. package/skills/jskit/references/patterns/shell/application-shell/example/src/views/NotFound.vue +1 -1
  120. package/skills/jskit/references/patterns/ui/page-and-placement/PATTERN.md +6 -0
  121. package/skills/jskit/references/patterns/ui/page-and-placement/example/src/pages/home/reports.vue +9 -4
  122. package/skills/jskit/references/patterns/ui/page-and-placement/example/src/placementTopology.js +5 -3
  123. package/skills/jskit/references/patterns/users/account-settings/PATTERN.md +24 -3
  124. package/skills/jskit/references/patterns/users/account-settings/example/src/components/account/settings/AccountSettingsNotificationsSection.vue +1 -1
  125. package/skills/jskit/references/patterns/users/account-settings/example/src/components/account/settings/AccountSettingsPreferencesSection.vue +2 -1
  126. package/skills/jskit/references/patterns/users/account-settings/example/src/components/account/settings/AccountSettingsProfileSection.vue +3 -2
  127. package/skills/jskit/references/patterns/users/account-settings/example/src/pages/account/[section].vue +23 -0
  128. package/skills/jskit/references/patterns/users/account-settings/example/src/pages/account.vue +4 -3
  129. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +2 -2
  130. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +3 -3
  131. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/packages/main/src/client/components/AccountPendingInvitesCue.vue +3 -3
  132. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/surfaces/admin/root.vue +1 -1
  133. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/surfaces/app/root.vue +1 -1
  134. package/skills/jskit/references/sanity-checks.md +181 -0
  135. package/skills/jskit/references/translation.md +4 -0
  136. package/skills/jskit/references/typed-features.md +222 -120
  137. package/skills/jskit/references/ui-operations.md +3 -1
  138. package/skills/jskit/references/patterns/database/mysql-application/example/tests/browser/database.js +0 -68
@@ -164,12 +164,108 @@ The check fails when:
164
164
  - a JSKIT override tries to force a private graph;
165
165
  - a root/workspace declaration has no lockfile installation, or the lockfile is missing;
166
166
  - installed package versions or root/workspace declarations disagree with the lockfile.
167
+ - a package script runs tests outside the machine's test budget (`test/*`, see
168
+ [Test files run in parallel](/guide/framework/typed-features#test-files-run-in-parallel)).
167
169
 
168
170
  Shared client owners such as the kernel, HTTP runtime, shell, auth web runtime,
169
171
  realtime runtime, and assistant runtime use exact peer dependencies where they
170
172
  must share application state. A partial upgrade can therefore fail during npm
171
173
  installation instead of silently nesting a second client stack.
172
174
 
175
+ ## Run tests within the test budget
176
+
177
+ Plain `jskit check` fails a test run that ignores the machine's test budget
178
+ ([Test files run in parallel](/guide/framework/typed-features#test-files-run-in-parallel)).
179
+ An application adopts it once, at the upgrade that brings `jskit test`:
180
+
181
+ 1. Its test scripts run `jskit test` with their own files or globs, for
182
+ example `"test": "jskit test"`, never `node --test` or
183
+ `--test-concurrency`.
184
+ 2. `playwright.config.*` sets `workers: testConcurrency()` and the Vitest
185
+ config sets `maxWorkers: testConcurrency()`, with
186
+ `import { testConcurrency } from "@jskit-ai/jskit-catalog/test-budget";`,
187
+ and no script passes `--workers`, `-j` or `--maxWorkers`.
188
+ 3. Files that collide when run together get their own port, temporary
189
+ directory, database or schema, and Vite `cacheDir`; a file that truly
190
+ must run alone says why on its first line, `// jskit-test: alone - <reason>`.
191
+ 4. The whole suite passes with the budget, and its time before and after is
192
+ recorded.
193
+
194
+ ## Move browser and database tests onto the JSKIT harness
195
+
196
+ The release that brings `browserTestConfig()` and `jskit test --databases`
197
+ makes JSKIT own the fast test harness. Plain `jskit check` fails a Playwright
198
+ webServer on the development server (`test/playwright-dev-server`), one
199
+ webServer every worker shares (`test/playwright-isolation`), and a config
200
+ without a `globalTimeout` (`test/playwright-global-timeout`). At that repin, an
201
+ application:
202
+
203
+ 1. Deletes its own copies of the harness: a local test-budget helper
204
+ (`testConcurrency()` in its own scripts), a launcher that starts a server
205
+ per worker, a cached production-build step, worker port and database-name
206
+ helpers, a Node test runner wrapper that hands each file a database, and
207
+ its schema-snapshot code. It imports `testConcurrency()` from
208
+ `@jskit-ai/jskit-catalog/test-budget` wherever it still needs the number.
209
+ 2. Replaces `playwright.config.*` with
210
+ `defineConfig(browserTestConfig({ testDir: "./tests/e2e", prepare: "tests/e2e/databases.js" }))`
211
+ from `@jskit-ai/jskit-catalog/browser-tests`, with `server`, `build`, `env`
212
+ or `port` only where its own differ from `node ./bin/server.js`,
213
+ `npm run build` and 4173, and `globalTimeout` only for a suite measured to
214
+ need more than 20 minutes. Settings a config adds (projects, a globalSetup
215
+ that proves the server's identity, `timezoneId`) spread over the result:
216
+ `defineConfig({ ...base, projects, use: { ...base.use, timezoneId } })`. A
217
+ second local Playwright config and any `npm run develop` or `vite`
218
+ webServer go.
219
+ 3. Copies its database pattern's `example/tests/e2e/databases.js` and moves
220
+ its browser fixture seed into its `seed`; each worker's server then gets a
221
+ seeded copy of `BROWSER_TEST_DB_NAME`. Server settings for tests only (a
222
+ simulated email or payment driver) go in `browserTestConfig({ env })`.
223
+ 4. Runs Node tests with `"test": "jskit test --databases"`, with
224
+ `TEST_DB_NAME` distinct from `DB_NAME`, `BROWSER_TEST_DB_NAME` and
225
+ `PROD_DB_NAME`, and a database user that may create its numbered copies
226
+ (`CREATEDB` on PostgreSQL). Test files read `DB_NAME`/`TEST_DB_NAME` as
227
+ given rather than naming the test database themselves, and no longer
228
+ create, migrate or drop it: the run hands each file a restored copy.
229
+ 5. Proves it with a timed full run: `npm test` and `npx playwright test`, every
230
+ test passing, the time of each before and after recorded, and the slowest
231
+ tests the run lists read.
232
+
233
+ ## Cache built assets for good
234
+
235
+ The foundations' `server.js` serves each content-hashed file under
236
+ `dist/assets/` with `Cache-Control: public, max-age=31536000, immutable`, so a
237
+ page load no longer revalidates every script and stylesheet the build made;
238
+ `index.html` and unhashed files are still revalidated on every load. An
239
+ application copies `HASHED_ASSET`, `HASHED_ASSET_CACHE` and the
240
+ `reply.sendFile()` choice from its foundation's `example/server.js` into its
241
+ own `server.js`, with the browser test "built assets are kept for good and the
242
+ page itself is revalidated" from `example/tests/e2e/startup.spec.ts`. A spec
243
+ that mocks requests with `page.route()` turns Playwright's HTTP cache off, so
244
+ it still fetches every asset on each navigation.
245
+
246
+ ## Sign each browser worker in once
247
+
248
+ A suite whose tests sign in one by one, such as a helper that calls
249
+ `loginAsExistingUser()` at the start of each test, signs each worker's server
250
+ in once instead, with `browserTestConfig({ signIn: "tests/e2e/sign-in.js" })`:
251
+
252
+ ```js
253
+ // tests/e2e/sign-in.js: `env` is the worker server's environment.
254
+ import { loginRequestAsExistingUser } from "@jskit-ai/auth-web/test/playwright";
255
+
256
+ export default ({ request, env }) =>
257
+ loginRequestAsExistingUser(request, { email: "owner@example.com", secret: env.AUTH_DEV_BYPASS_SECRET });
258
+ ```
259
+
260
+ The servers' launcher runs it once per server, after the servers start. Each
261
+ test of that worker then starts from the signed-in storage state, which the
262
+ worker's process also finds in `PLAYWRIGHT_STORAGE_STATE`, as on a managed
263
+ target, so a helper that skips signing in under `PLAYWRIGHT_STORAGE_STATE`
264
+ skips it locally too. A test that must start signed out uses
265
+ `test.use({ storageState: { cookies: [], origins: [] } })`; a test that signs
266
+ out or ends other sessions signs in on its own first, so the worker's shared
267
+ session lives on. A login state the host supplies is used as it is.
268
+
173
269
  ## Application migration
174
270
 
175
271
  The updater changes package manifests and the lockfile only. It does not
@@ -189,6 +285,15 @@ app source: the theme, icons, type classes, page titles and parents, and page
189
285
  actions. [Move an app to the Material 3 defaults](./material-3-migration.md)
190
286
  lists each change, most of which delete app CSS and overrides.
191
287
 
288
+ A release can add message keys (each package's changelog names its new ones,
289
+ such as `jskit.auth.local.register.closed`). JSKIT keeps no separate list: in
290
+ an app with other locales, run `npx jskit i18n export <locale>` for each one
291
+ after updating, which adds every new key in English beside the existing
292
+ translations, translate them, and let `npx jskit i18n check <locale>` confirm
293
+ none is missing (see [Translation](../framework/translation.md#add-a-language)).
294
+ Missing keys fall back to English, so nothing breaks before then, but an app
295
+ whose CI runs the check fails until they are added.
296
+
192
297
  Commit `package.json`, every changed workspace manifest, and
193
298
  `package-lock.json` together. Do not hand-edit one JSKIT version after the
194
299
  updater succeeds; select a different published cohort only through a catalog
@@ -25,6 +25,13 @@ Use the `users/account-settings` pattern for profile, preference, and
25
25
  notification sections. Decide which fields are editable and which operations
26
26
  require reauthentication.
27
27
 
28
+ The sections are pages in Material 3's list-detail layout: `/account` lists
29
+ them (`src/pages/account/index.vue`) and `/account/<section>` opens one
30
+ (`src/pages/account/[section].vue`, whose parent is the list, `"/"`). A phone
31
+ shows the list or one section, with Up from the section back to the list; a wide
32
+ window shows the list beside the open section. Link to a section by its path,
33
+ such as `/account/preferences`.
34
+
28
35
  Use the framework account settings elements and command composables rather than
29
36
  new page-local request clients. Cached query data must hydrate editable fields
30
37
  immediately; clearing a value must send the clear operation rather than omit the
@@ -47,7 +54,8 @@ record existence is not leaked.
47
54
  ## Verification
48
55
 
49
56
  Test profile creation from an authenticated identity, read/update/clear flows,
50
- warm-cache navigation, uploads when enabled, forbidden administration, missing
57
+ warm-cache navigation, the section list and a section one at a time at 390 px
58
+ (Up returns to the list) and side by side at 1280 px, uploads when enabled, forbidden administration, missing
51
59
  records, pagination, and workspace isolation where relevant.
52
60
 
53
61
  Do not copy users-core tables or services into the application. Do not add
@@ -237,7 +237,7 @@ const ConversationClientProvider = defineProvider({
237
237
  },
238
238
  /** @param {{ components: import("@jskit-ai/kernel/client/types").ClientComponents }} capabilities */
239
239
  setup({ components }) {
240
- components.register("conversation.web.settings.element", ConversationSettingsElement);
240
+ components.register("conversation.web.status.widget", ConversationStatusWidget);
241
241
  }
242
242
  });
243
243
 
@@ -246,6 +246,40 @@ const components = new Map();
246
246
  export function registerComponent(id, component) { components.set(id, component); }
247
247
  ```
248
248
 
249
+ ### Provider subpaths
250
+
251
+ **Export each client provider by a subpath of its own. The provider's file
252
+ imports only what every page needs, such as a widget the shell always shows; a
253
+ screen that only some routes show is imported by those routes.**
254
+
255
+ Why: every page of every app loads the client providers, and what they import,
256
+ before it shows anything. The client bootstrap imports a provider through the
257
+ subpath export that names its file, so the screens `./client` re-exports, and
258
+ their libraries, load only with the routes that show them. A provider without a
259
+ subpath, or a `./client` that declares `routeComponents` or `clientRoutes` or
260
+ uses `export *`, brings all of `./client` to every page.
261
+
262
+ ```json
263
+ // good: package.json
264
+ "exports": {
265
+ "./client": "./src/client/index.js",
266
+ "./client/providers/ConversationClientProvider": "./src/client/providers/ConversationClientProvider.js"
267
+ }
268
+
269
+ // bad: package.json, no subpath for the provider, so every page loads all of ./client
270
+ "exports": {
271
+ "./client": "./src/client/index.js"
272
+ }
273
+ ```
274
+
275
+ ```js
276
+ // good: the provider's file imports the widget every page shows
277
+ import ConversationStatusWidget from "../components/ConversationStatusWidget.vue";
278
+
279
+ // bad: the provider's file imports a screen that only its route shows
280
+ import ConversationSettingsScreen from "../components/ConversationSettingsScreen.vue";
281
+ ```
282
+
249
283
  ### Injected values
250
284
 
251
285
  **`inject()` takes the owner's exported key, never a literal. A required value
@@ -47,7 +47,8 @@ app and never leaves it.
47
47
  - **What the person sees:** one Up arrow on every inner page, also when opened
48
48
  directly: icon only (`$arrowBack`), accessible name "Back" (localized), a
49
49
  tooltip on pointer devices. None on top-level destinations, and no other
50
- back control anywhere; the bar title says where the person is. A list-detail
50
+ back control anywhere; the bar title says where the person is. A new or edit
51
+ form shows a close (X) in the arrow's place, which cancels. A list-detail
51
52
  layout showing the parent beside the page needs no arrow for that step;
52
53
  collapsed to one pane, the record pane gets it back.
53
54
  - **Where Up goes:** to the previous trail entry while it is valid, so inside
@@ -79,11 +80,17 @@ Exactly this, and nothing more:
79
80
  </script>
80
81
  ```
81
82
 
82
- The same key works in a `<route lang="json">` block. A record names its
83
- list, an edit page its record, a sub-page its page. Tabs under a layout
83
+ The same key works in a `<route lang="json">` block. Tabs under a layout
84
84
  (`settings.vue` rendering `<RouterView>` for `settings/`) declare nothing:
85
85
  they inherit the nearest layout's declaration, and moving between them keeps
86
- the trail.
86
+ the trail and the scroll. A folder without a layout (`bookings/` with no
87
+ `bookings.vue`) only groups routes: its pages are different pages, each
88
+ opening at the top. The parent is read from the page up to its surface root, the
89
+ record that declares `jskit.surface`, and stops there. So the surface goes
90
+ only on the surface's layout page (`account.vue`: `ShellLayout` around
91
+ `<RouterView />`) and the parent on the page it renders
92
+ (`account/index.vue`). A package route declares its parent in its
93
+ `package.json` `meta`; under no layout, its component draws `ShellLayout`.
87
94
  2. **Each top-level destination is a main-navigation item** (a `kind: "link"`
88
95
  placement with `target: "shell.primary-nav"` or `"shell.secondary-nav"`) and
89
96
  declares no parent; nor do its tabs. A shell page with no parent is
@@ -95,14 +102,20 @@ Exactly this, and nothing more:
95
102
  `:to="paths.page('/suppliers')"` and `router.push(paths.page(...))` with
96
103
  `usePaths()`, the CRUD runtime's `resolveViewUrl()`, `resolveEditUrl()` and
97
104
  `resolveParams()`, and link placements (`surface` with `scopedSuffix` and
98
- `unscopedSuffix`; `to` only for a global route such as `/auth/login`). The
99
- trail follows the router, so there is no special link component.
105
+ `unscopedSuffix`; `to` only for a global route). A global route
106
+ (`scope: "global"`, on no surface) is
107
+ `paths.page("/auth/login", { scope: "global" })`; `explicitTo` only passes
108
+ through a finished target that arrives as data, never a hand-written path.
109
+ The trail follows the router, so there is no special link component.
100
110
  4. **A custom new or edit page finishes through the shell:**
101
111
  `useShellNavigation()` (`@jskit-ai/shell-web/client/navigation/useShellNavigation`)
102
112
  gives `finish(target)` for Save (the target replaces the form in the trail)
103
113
  and `finish()` for Cancel (back to where the form was opened, else its
104
- parent). A page whose record turns out deleted or forbidden calls
105
- `pageUnavailable()`. JSKIT's CRUD screens already do both.
114
+ parent); `useShellPageClose({ label, close })`
115
+ (`@jskit-ai/shell-web/client/composables/useShellPageClose`) makes the
116
+ arrow its Cancel, never an X beside it. A page whose
117
+ record turns out deleted or forbidden calls `pageUnavailable()`. JSKIT's
118
+ CRUD screens already do all three.
106
119
  5. **A full page load goes through the shell too:** `reloadAppPage(path)`
107
120
  (after sign-in or a user switch, to rebuild client state) and
108
121
  `leaveApp(url)` (an external site in the same tab, such as a payment page),
@@ -118,11 +131,12 @@ Exactly this, and nothing more:
118
131
  | `nav/history` | `history.back()`, `history.go()`, `history.pushState()`/`replaceState()`, `router.back()`, `router.go()` |
119
132
  | `nav/window-location` | `window.location` assignments, `.href =`, `location.assign()`, `location.replace()` |
120
133
  | `nav/raw-link` | a hand-built in-app path in `to`, `href`, `router.push()`, `router.replace()` or a `to:`/`href:` object key outside a placement |
121
- | `nav/back-control` | any back control but the shell's Up that navigates the app: "Back to X", "← Suppliers", back arrows and navigating chevrons, breadcrumbs, a custom app-bar back button, a back component, a `goBack()`-style handler |
122
- | `nav/missing-parent` | an inner page of a shell surface without a declared parent |
134
+ | `nav/back-control` | any back control but the shell's Up that navigates the app: "Back to X", "← Suppliers", back arrows and navigating chevrons, breadcrumbs, a custom app-bar back button, a back component (`<ContextBackButton />` or `<page-back-link>`, and its import), a `goBack()`-style handler |
135
+ | `nav/missing-parent` | an inner page of a shell surface without a declared parent, when it always renders under the shell's top app bar (a layout that also renders its pages without `ShellLayout`, such as a shop device's sign-in in a `v-if`, leaves its pages unjudged) |
123
136
  | `nav/top-level-parent` | a main-navigation destination that declares a parent |
124
137
  | `nav/placement-data` | a main-navigation placement that isn't literal data (no page is judged for a missing parent until it is) |
125
138
  | `nav/missing-shell-layout` | a page that declares a parent with no shell layout rendering it, so its title, Up and actions never show |
139
+ | `nav/surface-parent` | a parent declared beside `jskit.surface`, which the shell never reads |
126
140
  | `nav/allow` | any `m3-check: allow nav/...` comment: nothing excuses a navigation rule |
127
141
 
128
142
  ### Each rule, right and wrong
@@ -180,35 +194,43 @@ Exactly this, and nothing more:
180
194
  - **Type:** Vuetify 4 Material 3 roles (`text-headline-small`,
181
195
  `text-title-large`, `text-body-medium`, `text-label-large`). Never the
182
196
  Material 2 scale (`text-h5`, `text-body-2`) or a page-local type system.
183
- Roboto Flex is the font.
197
+ Material 3 has two typefaces, both Roboto Flex unless the app names its own
198
+ in `src/theme/index.js`:
199
+ `typography: { brand: '"Fraunces", serif', plain: '"Inter", sans-serif' }`.
200
+ `brand` sets display, headline and title-large, `plain` the rest (title-medium
201
+ and -small, body, label); each is a CSS font-family value, and the app loads
202
+ the font. Never set `font-family` or `--v-font-body` in app CSS.
184
203
  - **Icons:** Material Symbols Rounded through Vuetify `$` aliases: Vuetify's
185
204
  standard ones (`$close`, `$menu`, `$delete`, `$prev`, `$next`) and JSKIT's
186
205
  camelCase names (`$add`, `$edit`, `$search`, `$moreVert`, `$filterList`,
187
206
  `$arrowBack`); `$homeFilled`-style `Filled` variants mark active navigation.
188
207
  An app adds an icon under `theme.icons` in `src/theme/index.js` as
189
208
  `{ name: "<24x24 path data>" }` from Material Symbols Rounded, then uses
190
- `$name`. No `mdi-*` strings and no `@mdi/js` imports in app code.
209
+ `$name`. No `mdi-*` strings and no `@mdi/js` imports in app code. Material
210
+ Symbols has no brand marks (Google, GitHub, a payment network): a brand's
211
+ logo is the brand's own artwork, a content image or SVG used as its owner
212
+ publishes it, not an icon alias.
191
213
 
192
214
  ## Exact sizes
193
215
 
194
- JSKIT ships these exact sizes; an app never re-implements or overrides them,
195
- and needs no Sass and no `!important` (Vuetify 4 keeps its CSS in cascade
196
- layers):
216
+ JSKIT ships these exact sizes; an app never re-implements or overrides them:
197
217
 
198
218
  - 40dp buttons with a 48dp touch target, icon buttons included; a smaller
199
219
  button is `size="small"` or `size="x-small"`, which keeps its smaller look
200
- and the 48dp target;
201
- - one-line button labels: a label longer than its button ends in an ellipsis.
202
- Keep labels short, and give a button whose label can run long, such as a
203
- name, the full label as its `title`;
204
- - 12dp cards: elevated (the default) at level 1, flat and tonal at level 0;
220
+ and the 48dp target. No icon button is under 32dp, and a target stops at
221
+ the edge of a button beside it, so each of a row's touching icon buttons
222
+ answers its own press;
223
+ - one-line button labels, ending in an ellipsis: keep labels short, and give
224
+ one that can run long, such as a name, its full label as `title`;
225
+ - 12dp cards: flat (the default) and tonal at level 0, `variant="elevated"`
226
+ at level 1; flat expansion panels;
205
227
  - 28dp dialogs and bottom-sheet top corners, square full-screen dialogs;
206
228
  - 8dp chips, and FABs of 56dp (small 40dp, large 96dp; extended 56dp tall)
207
229
  with 16dp corners;
208
230
  - emphasized easing, with reduced motion honoured.
209
231
 
210
232
  So never set `height` or `min-height` on a button, or a radius on a card,
211
- dialog, chip or FAB. A missing size is a JSKIT defect.
233
+ dialog, chip or FAB.
212
234
 
213
235
  ## The top app bar carries the page
214
236
 
@@ -216,35 +238,46 @@ dialog, chip or FAB. A missing size is a JSKIT defect.
216
238
  `useShellPageTitle(title)` (`@jskit-ai/shell-web/client/composables/useShellPageTitle`)
217
239
  for a runtime title such as the record's name. The shell renders it as the
218
240
  app bar's only `h1`; pages add no `h1`, page header, title block or
219
- standalone heading. A section inside a page uses a title role. A page with
220
- no top app bar (a hero front page, an intro, an invite or hand-off page)
221
- has its own one `h1`.
222
- - **Up** sits in the bar on every inner page (see Navigation and Up). On phones
223
- it replaces the menu icon.
241
+ standalone heading. Route pages own the title: a section or a nested
242
+ component uses a title role (a component's claim gives the title back when
243
+ it goes). A page with no top app bar (a hero front page, an intro, an invite
244
+ or hand-off page) has its own one `h1`.
245
+ - **Up** leads the bar on every inner page; on phones it replaces the menu
246
+ icon.
224
247
  - **A layout that draws its own top app bar** (a workspace bar with the
225
248
  app's own controls, instead of `ShellLayout`) builds it from `ShellLayout`'s
226
249
  own pieces, from `@jskit-ai/shell-web/client`. It calls `useShellAppBar()`
227
250
  once, so its pages enter the trail and their `ShellPageActions` show in its
228
251
  bar, and renders `ShellUpButton` first, then `ShellPageTitle` (the page's one
229
- `h1`: `useShellPageTitle()`, else the route's `jskit.title`, else its `title`
230
- prop) and `ShellPageActionsTarget`. A title that is also a switcher, such as a
252
+ `h1`) and `ShellPageActionsTarget`. A title that is also a switcher, such as a
231
253
  project menu, goes in `ShellPageTitle`'s default slot (given `{ title }`) as
232
- the menu's activator, so it stays the one `h1`. `jskit check` counts it as a shell
233
- layout, so its inner pages need parents; an Up the app draws itself is a
234
- `nav/back-control` finding.
254
+ the menu's activator, so it stays the one `h1`. `jskit check` counts it as a
255
+ shell layout, so its inner pages need parents.
235
256
  - **Actions:** a screen's actions go in the bar inside `ShellPageActions`
236
257
  (`@jskit-ai/shell-web/client/components/ShellPageActions`), each a
237
258
  `ShellPageAction` with `icon`, `label` (also its accessible name and compact
238
- tooltip), and `primary`, `to`, `replace`, `disabled`, `loading` or `color`.
239
- On compact widths every action shows only its icon except the single
240
- `primary` one.
259
+ tooltip), and `primary`, `to`, `replace`, `href` (with `download` for a
260
+ file, such as a CSV export), `disabled`, `loading` or `color` (`error` for a
261
+ destructive one); attributes such as `target` reach its button. On compact
262
+ widths the bar keeps one trailing item after the title: the single
263
+ `primary` action when the page has one (a form's Save), otherwise its own
264
+ item nearest the corner (a top-right placement such as the account avatar).
265
+ Every other action and the bar's other items (a notification bell, tools)
266
+ go to its "More actions" menu, where `overflow` actions always are. Medium
267
+ and wider bars show them all.
241
268
  - **Window size classes:** compact under 600 (navigation bar), medium 600–839
242
- (navigation rail), expanded 840–1199 and large from 1200 (drawer). A surface
243
- with no navigation destinations, such as an account surface, draws no
244
- navigation bar, rail or drawer; its pages keep the app bar. Read them
245
- from `useShellWindowSizeClass()`
269
+ (navigation rail), expanded 840–1199 and large from 1200 (drawer), read from
270
+ `useShellWindowSizeClass()`
246
271
  (`@jskit-ai/shell-web/client/composables/useShellWindowSizeClass`), never from
247
- copied breakpoint numbers.
272
+ copied breakpoint numbers. A surface with fewer than two navigation
273
+ destinations (none, as on an account surface, or Home alone) draws no
274
+ navigation bar, rail or drawer; its pages keep the app bar.
275
+ - **Section tabs:** a page's own sections (`page.section-nav`) are Material 3
276
+ primary tabs: the topology names `ShellSectionTabLinkItem`'s token (the
277
+ starter registers it as `local.main.ui.section-tab-link-item`), and the page
278
+ draws one tab row, `display: flex; overflow-x: auto` over a 1dp
279
+ `outline-variant` bottom border. `ShellTabLinkItem` is the navigation bar's
280
+ destination; placed in a page it draws a section tab.
248
281
 
249
282
  ## Colour
250
283
 
@@ -267,35 +300,41 @@ dialog, chip or FAB. A missing size is a JSKIT defect.
267
300
  control; never a permanent panel of outlined fields. A card opens its record
268
301
  when tapped (a stretched title link); a table row opens on click. Row actions
269
302
  are trailing icon buttons (edit, a `$moreVert` overflow menu, a chevron to
270
- open), never "View" or "Edit" text buttons on every row. "New" is an extended
271
- FAB on compact and medium widths and a filled bar button on expanded. Empty
272
- and error states carry an icon.
303
+ open), never "View" or "Edit" text buttons on every row. "New" is
304
+ `ShellPrimaryAction` (`label`, and `to` or a click handler): an extended FAB
305
+ on compact and medium widths, a filled bar button on expanded. On/off filters
306
+ are `aria-pressed` chips, or chips in a `v-chip-group`; JSKIT draws them as
307
+ filter chips. Empty and error states carry an icon.
273
308
  - **A list never scrolls sideways, at any width.** The list's own width (a
274
309
  container query, not the window) chooses cards or table. Cards flow into a
275
310
  grid; tables use a fixed layout with an ellipsis on long values, and fold
276
311
  low-priority columns into a stronger column's second line. Lists show figures
277
312
  ("120 kg", tabular and right-aligned) and names, never ids.
278
313
  - **Record:** a header (avatar, title, status chips) and value-first detail
279
- lists, never a grid of label/value text. Edit sits in the bar; the record's
314
+ lists, never a grid of label/value text. Edit sits in the bar, its secondary
315
+ actions in the bar's overflow menu, never in the content; the record's
280
316
  next workflow step is the one filled button, shown in context rather than a
281
317
  row of mostly disabled actions.
282
- - **Form:** Cancel and Save sit in the bar. Fields use `hideDetails: "auto"`.
283
- Nothing moves under a finger: a multi-select chip field keeps its input width
284
- when it loses focus. A pick-one-of-N choice is a segmented button with
285
- `role="radiogroup"` and `role="radio"`/`aria-checked` buttons.
318
+ - **Form:** the bar's close (X) cancels and Save is its filled action. Fields
319
+ use `hideDetails: "auto"`. Nothing moves under a finger: a multi-select chip
320
+ field keeps its input width when it loses focus. A pick-one-of-N choice is a
321
+ segmented button (`v-btn-toggle`, drawn by JSKIT, with 12dp segment sides as
322
+ in any `v-btn-group`) with `role="radiogroup"` and
323
+ `role="radio"`/`aria-checked` buttons.
286
324
  - **Dialogs:** plain `v-dialog` and `v-bottom-sheet`; JSKIT gives them their
287
325
  Material 3 shapes. A full-screen dialog (compact and medium) puts close, title
288
326
  and its confirm action in a top bar, never at the bottom where a snackbar
289
327
  covers them.
290
328
  - **Menus and badges:** a menu's list has `role="menu"` and its items
291
329
  `role="menuitem"`, with `aria-disabled` on unavailable ones; a badge has a
292
- `label` such as "3 batches".
330
+ `label` such as "3 batches". JSKIT draws a menu's list and a dialog's card;
331
+ an app sets no `VMenu` or `VDialog` defaults, under which
332
+ Vuetify 4.2.1 to 4.3.2 loses a `v-select`'s typeahead match, or a dialog's
333
+ virtual list, when an ancestor re-renders.
293
334
 
294
335
  ## Components, state, and feedback
295
336
 
296
- - Choose components by purpose. Prefer standard buttons, fields, lists,
297
- tables, sheets, dialogs, menus, navigation, progress, alerts, snackbars, and
298
- tooltips over custom imitations.
337
+ - Choose standard components by purpose over custom imitations.
299
338
  - Give one primary action emphasis. Distinguish destructive, secondary, and
300
339
  icon-only actions without relying on color; give icon controls accessible
301
340
  names and fields meaningful labels, errors, and autocomplete behavior.
@@ -312,6 +351,11 @@ dialog, chip or FAB. A missing size is a JSKIT defect.
312
351
  field. A user-triggered command uses JSKIT's shared action
313
352
  feedback/snackbar path. Never insert a transient command-error alert above
314
353
  page content where it shifts the working layout.
354
+ - A snackbar never covers the FAB or the actions it reports on. `ShellErrorHost`
355
+ centres it above the navigation bar on compact windows and starts it at the
356
+ content's start edge, at most half its width, on wider ones. A FAB takes
357
+ layout room (`<v-fab app layout>`, as `ShellPrimaryAction`'s does), so the
358
+ snackbar shows above it.
315
359
 
316
360
  ## Adaptive layout
317
361
 
@@ -333,16 +377,15 @@ npx jskit check m3 src/pages/home/books # a section
333
377
  npx jskit check m3 $(git diff --name-only --diff-filter=d main) # changed files
334
378
  ```
335
379
 
336
- Plain `jskit check` fails on the [navigation rules](#forbidden-in-app-code);
337
- fix every one, because nothing excuses them. `jskit check m3` lists those and
338
- the visual rules below. The visual rules are a report until every JSKIT app has
339
- been converted to them; the JSKIT release after that makes plain `jskit check`,
340
- and so every app's verify, fail on them too:
380
+ Plain `jskit check` fails on the [navigation rules](#forbidden-in-app-code).
381
+ `jskit check m3` lists those and the visual rules below. The visual rules are a
382
+ report until every JSKIT app has been converted to them; the JSKIT release after
383
+ that makes plain `jskit check`, and so every app's verify, fail on them too:
341
384
 
342
385
  | Rule | Finds |
343
386
  |---|---|
344
- | `m3/duplicate-h1` | a second `h1`, or an in-page `h1` under the shell's page title |
345
- | `m3/raw-hex` | a hex colour outside `src/theme/` (`palettes.js` holds content colours) |
387
+ | `m3/duplicate-h1` | a second `h1` that can show beside the first (one per `v-if` branch is one), or an in-page `h1` under a layout that always shows the shell's page title |
388
+ | `m3/raw-hex` | a hex colour, or one written as channels (`rgba(0, 0, 0, 0.15)`, as in a hand-made shadow), outside `src/theme/` (`palettes.js` holds content colours); `rgba(var(--v-theme-shadow), 0.2)` reads a role and passes |
346
389
  | `m3/dark-scheme` | no dark scheme, or the theme pinned to light |
347
390
  | `m3/outlined-card` | `variant="outlined"` or `border` on a card or sheet |
348
391
  | `m3/mdi-icon` | `mdi-*` icon strings and `@mdi/js` imports |
@@ -395,9 +438,9 @@ What is left, such as a colour inside static SVG markup that can't import,
395
438
  may be excused one line at a time, on its line or the line before: `<!-- m3-check: allow m3/raw-hex - the partner's contractual logo colour -->`
396
439
  (`// m3-check: ...` in scripts, `/* ... */` in CSS). **Never add one on your
397
440
  own:** ask the person first, naming the rule and the reason. A comment with no
398
- reason (`m3/allow-reason`) or an unknown rule (`m3/allow-rule`), and any
399
- `m3-check: allow nav/...` (`nav/allow`), is itself a finding; nothing turns the
400
- check off for a file, a folder or the app. `jskit check m3` lists every
441
+ reason (`m3/allow-reason`) or an unknown rule (`m3/allow-rule`) is itself a
442
+ finding, as any `nav/allow` is; nothing turns the check off for a file, a
443
+ folder or the app. `jskit check m3` lists every
401
444
  exception with its file, line and reason, and their total.
402
445
 
403
446
  ## Material 3 audit
@@ -406,19 +449,16 @@ Run this behavior-preserving audit over every affected screen:
406
449
 
407
450
  1. Identify the owning JSKIT surface, shared screen, shell, placement, theme,
408
451
  and Vuetify components before judging local markup.
409
- 2. Check navigation and Up first, as must-fix: plain `jskit check` passes;
410
- every inner page has a parent and one Up arrow in the bar; main-menu
411
- destinations have none; no inner link is hand-built; nothing writes back
412
- logic.
452
+ 2. Check navigation and Up first, as must-fix: plain `jskit check` passes and
453
+ every rule in Navigation and Up holds.
413
454
  3. Find parallel components, duplicated state chrome, page-local themes, raw
414
455
  visual constants, CSS against Vuetify internals, nested generic cards,
415
- borders instead of tones, Material 2 typography, and desktop-only layout.
416
- 4. Check hierarchy, token use, component purpose, responsive presentation,
417
- interaction states, keyboard/focus behavior, labels, targets, contrast, and
418
- reduced motion against the rules above.
419
- 5. Correct issues at the narrowest established owner. Consolidate only truly
456
+ borders instead of tones, Material 2 typography, and desktop-only layout;
457
+ check states, focus, labels, targets, contrast and reduced motion against
458
+ the rules above.
459
+ 4. Correct issues at the narrowest established owner. Consolidate only truly
420
460
  repeated policy; avoid pass-through wrappers and speculative abstractions.
421
- 6. Run `jskit check` and `jskit check m3` on the affected files, then focused
461
+ 5. Run `jskit check` and `jskit check m3` on the affected files, then focused
422
462
  tests and Playwright at compact, medium, and expanded widths in light and
423
463
  dark (`shellBackButton(page)` and `pressShellBack(page)` from
424
464
  `@jskit-ai/shell-web/test/shellNavigation` drive Up).