@lessly/sdk-app 48.0.2 → 48.0.3

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/docs/README.md CHANGED
@@ -33,9 +33,12 @@ does not apply.
33
33
  `X-Product-Id` header — never from a token — and must not implement its own
34
34
  auth.
35
35
  - **Manifest is `lessly.app.yaml`, schema v1.** Exactly five top-level fields:
36
- `app_schema_version: 1`, `id`, `title`, `version`, `nav` (with `nav.icon`).
37
- No backend or extension manifest field (`s2s`, `webhooks`, `billing`,
38
- `analytics`, `routing`, ...) is valid here.
36
+ `app_schema_version: 1`, `id`, `title`, `version`, `nav` (`nav.icon`, plus an
37
+ optional `nav.sections` rail menu). No backend or extension manifest field
38
+ (`s2s`, `webhooks`, `billing`, `analytics`, `routing`, ...) is valid here.
39
+ - **Your rail menu is manifest data.** The menu the shell draws while your App
40
+ is open comes from your own `nav.sections` block, not from your App code and
41
+ not from a toolkit manifest — see APP-011.
39
42
 
40
43
  ## Quickstart
41
44
 
@@ -56,6 +59,9 @@ nav:
56
59
  icon: flask
57
60
  ```
58
61
 
62
+ `nav.sections` — the App's own rail menu — is optional and not scaffolded; add
63
+ it when your App has more than one page (APP-011).
64
+
59
65
  Your app's Module Federation remote exposes a single React component at
60
66
  `./App`. The scaffold wires this up for you; see `recipes/federation.md` if
61
67
  you need to touch it directly.
@@ -28,9 +28,8 @@ There is no `./navigation` module. FED-011 — the shell's extension-menu
28
28
  contract — is **RETIRED (2026-08)**: the shell never registers, preloads or
29
29
  calls it, there is no `subscribeNavItems` (v1 or v2) and no
30
30
  `navContractVersion` negotiation, and the menu the shell renders while your app
31
- is open comes from the toolkit manifest your App is registered under
32
- (`nav.sections`, MFST-022 in the toolkit-guide) your App code does not ship
33
- it.
31
+ is open comes from **your own** `lessly.app.yaml` the `nav.sections` block
32
+ (APP-011) not from your App code, and not from a toolkit manifest.
34
33
 
35
34
  Still mapping the expose is inert, not an error — you just ship a chunk nobody
36
35
  fetches. If your app has one, the cleanup is: delete
@@ -42,6 +41,31 @@ status dots, a settings gear — belongs **in your own pages**, rendered by
42
41
  button are unaffected: the dynamic items only ever fed the menu, never the
43
42
  router.
44
43
 
44
+ ## The menu moves to the manifest, not to a module
45
+
46
+ The rows the module used to publish are now data in your App's manifest:
47
+
48
+ ```yaml
49
+ # lessly.app.yaml
50
+ nav:
51
+ icon: flask-conical
52
+ sections:
53
+ - id: overview
54
+ items:
55
+ - label: Dashboard
56
+ path: '' # '' = your App's root
57
+ match: exact
58
+ icon: gauge
59
+ - label: Activity
60
+ path: activity
61
+ icon: activity
62
+ ```
63
+
64
+ `path` is relative to your App's base path (`/<productSlug>/~/<id>`), so these
65
+ are the same relative routes `./App` already renders under (FED-003). The full
66
+ field constraints, the worked example, what happens when you ship no block, and
67
+ how a change reaches production are in APP-011.
68
+
45
69
  ## Shared singletons: `>=` floors, not carets, on 0.x packages
46
70
 
47
71
  `react`, `react-dom`, `react-router`, and `@lessly/ui` MUST be declared as
package/docs/rules.md CHANGED
@@ -33,10 +33,16 @@ service-to-service call, it belongs in an extension, not an App.
33
33
  ### APP-003 (MUST) — Manifest is `lessly.app.yaml`, schema v1
34
34
 
35
35
  The manifest file is `lessly.app.yaml`, and its top-level surface is exactly:
36
- `app_schema_version: 1`, `id`, `title`, `version`, `nav` (with `nav.icon`). No
37
- other top-level field is valid — in particular, no extension/backend manifest
38
- field (`s2s`, `webhooks`, `billing`, `analytics`, `routing`, `public`, ...)
39
- belongs in an App manifest.
36
+ `app_schema_version: 1`, `id`, `title`, `version`, `nav`. No other top-level
37
+ field is valid — in particular, no extension/backend manifest field (`s2s`,
38
+ `webhooks`, `billing`, `analytics`, `routing`, `public`, ...) belongs in an App
39
+ manifest.
40
+
41
+ `nav` has exactly two keys: `nav.icon` (**required**, the rail icon) and
42
+ `nav.sections` (**optional**, the App's own rail menu — APP-011). Any other key
43
+ under `nav` is rejected, and rejection is fatal: registration validation is
44
+ whitelist-strict, so an unknown key fails the whole registration rather than
45
+ being stripped.
40
46
 
41
47
  ### APP-004 (MUST) — Module Federation contract
42
48
 
@@ -52,12 +58,11 @@ artifacts (FED-007).
52
58
  `./App` is the **only** module an App exposes. There is no `./navigation`
53
59
  contract: the shell no longer loads such a module, there is no
54
60
  `subscribeNavItems` and no `navContractVersion` negotiation, and the menu the
55
- shell renders comes from the toolkit manifest your App is registered under
56
- (`nav.sections`, MFST-022 in the toolkit-guide) your App code does not ship
57
- it. FED-011 is **RETIRED (2026-08)**. Still mapping the expose is inert rather
58
- than an error — it just ships a chunk nobody fetches — but a new App MUST NOT
59
- add it, and an existing
60
- one SHOULD delete `src/federation/navigation.ts`, drop the expose, and move
61
+ shell renders comes from **your own** `lessly.app.yaml` the `nav.sections`
62
+ block (APP-011) not from your App code, and not from a toolkit manifest.
63
+ FED-011 is **RETIRED (2026-08)**. Still mapping the expose is inert rather than
64
+ an error — it just ships a chunk nobody fetches — but a new App MUST NOT add
65
+ it, and an existing one SHOULD delete `src/federation/navigation.ts`, drop the expose, and move
61
66
  whatever that module fed the menu (environment lists, tool groups, badges,
62
67
  status dots, a settings gear) into its own pages under `./App`'s relative
63
68
  routes (FED-003).
@@ -235,3 +240,81 @@ See FED-014 in the toolkit-guide for the measurement (App `.px-2` against
235
240
  shell `.sm:px-2.5` — different rule names, so no shared preset can reach them;
236
241
  5 of 283 shell chrome nodes affected) and for the accepted symmetric risk on
237
242
  the App's own nodes.
243
+
244
+ ### APP-011 (SHOULD) — Declare the rail menu in your own manifest
245
+
246
+ The rail menu the shell draws while your App is open comes from your App's own
247
+ `lessly.app.yaml`, in an optional `nav.sections` block. An App with more than
248
+ one page SHOULD declare it; without it the shell has nothing of yours to draw
249
+ (see *No block* below).
250
+
251
+ ```yaml
252
+ app_schema_version: 1
253
+ id: my-app
254
+ title: My App
255
+ version: 0.2.0
256
+ nav:
257
+ icon: flask-conical
258
+ sections:
259
+ - id: overview
260
+ items:
261
+ - label: Dashboard
262
+ path: ''
263
+ match: exact
264
+ icon: gauge
265
+ - label: Activity
266
+ path: activity
267
+ icon: activity
268
+ - id: settings
269
+ header: Settings
270
+ items:
271
+ - label: Members
272
+ path: settings/members
273
+ icon: users-round
274
+ - label: API keys
275
+ path: settings/api-keys
276
+ icon: key
277
+ ```
278
+
279
+ **Field constraints.** These are validated on registration by the platform's
280
+ shared `NavSectionDto`/`NavItemDto` — literally the same DTO classes the
281
+ toolkit manifest validates against (MFST-022 in the toolkit-guide), so the two
282
+ manifests speak exactly one menu language. A violation is a hard reject of the
283
+ registration, not a silent strip.
284
+
285
+ | Field | Required | Rule |
286
+ | --- | --- | --- |
287
+ | `nav.sections` | no | array, 1–20 sections; `id` unique across the array |
288
+ | `sections[].id` | yes | 1–50 chars, lowercase-kebab `^[a-z0-9-]+$` |
289
+ | `sections[].header` | no | 1–50 chars; absent → the group renders with no header line |
290
+ | `sections[].items` | yes | array, 1–50 items |
291
+ | `items[].label` | yes | 1–50 chars, non-empty |
292
+ | `items[].path` | yes | 0–200 chars, `^[a-z0-9-_/]*$`, **no leading slash**, no `..` segment; `''` = your tool root |
293
+ | `items[].icon` | no | 1–50 chars, kebab-case Lucide icon name (e.g. `rocket`) |
294
+ | `items[].match` | no | `exact` or `prefix`; default `prefix` |
295
+
296
+ `path` is relative to your App's own base path (`/<productSlug>/~/<id>`), so a
297
+ manifest can name a page **inside** your App and nothing else — no absolute
298
+ paths, no external URLs, no traversal. Use `match: exact` on a row whose path
299
+ is a prefix of a sibling's (typically the `''` root row), or it stays lit while
300
+ the sibling is open.
301
+
302
+ **No block.** `nav.sections` absent is legal. The shell then falls back to its
303
+ transitional table (`extension-menus.ts`, keyed by slug, entity-agnostic) if
304
+ your slug is listed there; if it is not, your rail row stays a **single plain
305
+ link** into the App, with no disclosure mark promising rows that do not exist.
306
+ The fallback is transitional: an App leaves that table the moment its manifest
307
+ ships the menu, and the entry there should be deleted by the same PR. Narrowing
308
+ of the served block is **all-or-nothing** — a half-valid menu falls back to the
309
+ table rather than drawing a partial disclosure.
310
+
311
+ **Shipping a change.** The menu is picked up on re-registration, including a
312
+ **same-version** one: the registration overlay replaces the whole `nav` blob,
313
+ so adding, editing and removing sections all land without a `version` bump.
314
+ Bumping `version` on release remains APP-007.
315
+
316
+ **The scaffold's linter does not check this block.** `create-lessly-app lint`
317
+ validates the top-level surface and `nav.icon` only; it neither rejects nor
318
+ warns on `nav.sections`, and it will not catch a malformed one. The platform's
319
+ registration validation is the authority — a menu that lints clean locally can
320
+ still fail registration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessly/sdk-app",
3
- "version": "48.0.2",
3
+ "version": "48.0.3",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "engines": {