@xemahq/ui-kernel 0.11.1 → 0.12.0

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 (257) hide show
  1. package/dist/errors/http-error-status.d.ts +17 -0
  2. package/dist/errors/http-error-status.d.ts.map +1 -0
  3. package/dist/errors/http-error-status.js +59 -0
  4. package/dist/errors/http-error-status.js.map +1 -0
  5. package/dist/errors/index.d.ts +2 -0
  6. package/dist/errors/index.d.ts.map +1 -0
  7. package/dist/errors/index.js +2 -0
  8. package/dist/errors/index.js.map +1 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/lib/biome-host/errors.d.ts.map +1 -1
  14. package/dist/lib/biome-host/errors.js +1 -19
  15. package/dist/lib/biome-host/errors.js.map +1 -1
  16. package/dist/offline/index.d.ts.map +1 -1
  17. package/dist/offline/index.js.map +1 -1
  18. package/dist/offline/online-controller.d.ts.map +1 -1
  19. package/dist/offline/online-controller.js +6 -1
  20. package/dist/offline/online-controller.js.map +1 -1
  21. package/dist/offline/outbox.d.ts.map +1 -1
  22. package/dist/offline/outbox.js +2 -24
  23. package/dist/offline/outbox.js.map +1 -1
  24. package/package.json +1 -2
  25. package/src/index.ts +0 -17
  26. package/src/lib/biome-host/agent-validation.ts +0 -239
  27. package/src/lib/biome-host/biome-builders.ts +0 -112
  28. package/src/lib/biome-host/biome-mode.ts +0 -6
  29. package/src/lib/biome-host/biome-navigation.ts +0 -37
  30. package/src/lib/biome-host/biome-registry.ts +0 -299
  31. package/src/lib/biome-host/biome-scope-hooks.ts +0 -41
  32. package/src/lib/biome-host/biome-scope.tsx +0 -52
  33. package/src/lib/biome-host/biome-scoped-query.ts +0 -134
  34. package/src/lib/biome-host/create-biome-orval-config.ts +0 -82
  35. package/src/lib/biome-host/define-web-biome.spec.ts +0 -221
  36. package/src/lib/biome-host/define-web-biome.ts +0 -222
  37. package/src/lib/biome-host/errors.ts +0 -220
  38. package/src/lib/biome-host/frontend-biome.ts +0 -256
  39. package/src/lib/biome-host/host-bridge.ts +0 -281
  40. package/src/lib/biome-host/host-sources.ts +0 -54
  41. package/src/lib/biome-host/i18n-hooks.ts +0 -51
  42. package/src/lib/biome-host/i18n-port.test.ts +0 -50
  43. package/src/lib/biome-host/i18n-port.ts +0 -96
  44. package/src/lib/biome-host/index.ts +0 -35
  45. package/src/lib/biome-host/realtime-hooks.ts +0 -75
  46. package/src/lib/biome-host/realtime-port.ts +0 -115
  47. package/src/lib/biome-host/session-contributions.ts +0 -296
  48. package/src/lib/biome-host/use-mutation-with-error-toast.ts +0 -90
  49. package/src/lib/biome-host/use-page-state.ts +0 -248
  50. package/src/lib/capabilities/capability-context.ts +0 -25
  51. package/src/lib/capabilities/capability-provider.tsx +0 -73
  52. package/src/lib/capabilities/index.ts +0 -17
  53. package/src/lib/capabilities/types.ts +0 -69
  54. package/src/lib/capabilities/use-capability.ts +0 -79
  55. package/src/lib/contribution-registry/contribution-registry.spec.ts +0 -141
  56. package/src/lib/contribution-registry/contribution-registry.ts +0 -204
  57. package/src/lib/contribution-registry/index.ts +0 -9
  58. package/src/lib/contribution-registry/renderable-registry.ts +0 -142
  59. package/src/lib/system-bus/capability-invoke.ts +0 -92
  60. package/src/lib/system-bus/deeplink.ts +0 -210
  61. package/src/lib/system-bus/enums.ts +0 -86
  62. package/src/lib/system-bus/host-ports.ts +0 -93
  63. package/src/lib/system-bus/index.ts +0 -16
  64. package/src/lib/system-bus/intent-registry.ts +0 -109
  65. package/src/lib/system-bus/intents.ts +0 -113
  66. package/src/lib/system-bus/palette.ts +0 -78
  67. package/src/lib/system-bus/system-bus-builder.ts +0 -159
  68. package/src/lib/system-bus/system-bus.ts +0 -37
  69. package/src/lib/system-bus/windows.ts +0 -51
  70. package/src/offline/id.ts +0 -16
  71. package/src/offline/index.ts +0 -54
  72. package/src/offline/mutation.ts +0 -75
  73. package/src/offline/offline-status-badge.tsx +0 -51
  74. package/src/offline/online-controller.ts +0 -105
  75. package/src/offline/outbox.test.ts +0 -172
  76. package/src/offline/outbox.ts +0 -348
  77. package/src/offline/react-hooks.ts +0 -40
  78. package/src/offline/react.tsx +0 -32
  79. package/src/offline/storage.ts +0 -112
  80. package/src/offline/types.ts +0 -77
  81. package/src/org-db/components/DbResultTable.tsx +0 -176
  82. package/src/org-db/index.ts +0 -1
  83. package/src/pdf/index.ts +0 -11
  84. package/src/registry/index.ts +0 -9
  85. package/src/registry/lib/agent-validation-host.ts +0 -36
  86. package/src/registry/lib/biome-slot.tsx +0 -50
  87. package/src/registry/lib/biomes-enabled-context.ts +0 -20
  88. package/src/registry/lib/extension-points.ts +0 -134
  89. package/src/registry/lib/primitives/SessionEventCard.tsx +0 -164
  90. package/src/registry/lib/primitives/SessionEventTimeline.tsx +0 -95
  91. package/src/registry/lib/primitives/SessionMutationBar.tsx +0 -76
  92. package/src/registry/lib/primitives/SessionTabbedDrawer.tsx +0 -165
  93. package/src/registry/lib/primitives/index.ts +0 -18
  94. package/src/registry/lib/session-context-builder.ts +0 -84
  95. package/src/registry/lib/session-context-provider.tsx +0 -33
  96. package/src/registry/lib/session-context.ts +0 -21
  97. package/src/registry/lib/session-selectors.ts +0 -272
  98. package/src/session/comments/CommentRail.tsx +0 -164
  99. package/src/session/index.ts +0 -3
  100. package/src/session/lib/cn.ts +0 -11
  101. package/src/session/shell/SessionWorkspaceShell.tsx +0 -147
  102. package/src/session-kit/approvals/ApprovalButton.tsx +0 -89
  103. package/src/session-kit/approvals/ApprovalCard.tsx +0 -341
  104. package/src/session-kit/approvals/ApprovalsCenter.tsx +0 -336
  105. package/src/session-kit/approvals/CapabilityApprovalStickyBar.tsx +0 -118
  106. package/src/session-kit/approvals/Obligation.tsx +0 -111
  107. package/src/session-kit/approvals/ScopedBody.tsx +0 -392
  108. package/src/session-kit/approvals/approval-icons.ts +0 -31
  109. package/src/session-kit/approvals/approval-model.ts +0 -204
  110. package/src/session-kit/approvals/index.ts +0 -22
  111. package/src/session-kit/approvals/obligation-display.ts +0 -100
  112. package/src/session-kit/approvals/risk-accent.ts +0 -47
  113. package/src/session-kit/approvals/scope-icons.ts +0 -19
  114. package/src/session-kit/combobox/Combobox.tsx +0 -358
  115. package/src/session-kit/combobox/use-click-outside.ts +0 -21
  116. package/src/session-kit/display/ContextHeader.tsx +0 -148
  117. package/src/session-kit/display/FileDiffCard.tsx +0 -140
  118. package/src/session-kit/display/MD.tsx +0 -162
  119. package/src/session-kit/display/MessageTurn.tsx +0 -157
  120. package/src/session-kit/display/ThinkingPanel.tsx +0 -81
  121. package/src/session-kit/display/TodoChecklist.tsx +0 -120
  122. package/src/session-kit/display/TokenMeter.tsx +0 -89
  123. package/src/session-kit/display/ToolStrip.tsx +0 -278
  124. package/src/session-kit/display/TypingDots.tsx +0 -24
  125. package/src/session-kit/index.ts +0 -44
  126. package/src/session-kit/lib/enums.ts +0 -66
  127. package/src/session-kit/lib/portal-accent.ts +0 -30
  128. package/src/session-kit/lib/status-dot.ts +0 -68
  129. package/src/session-kit/primitives/Avatar.tsx +0 -44
  130. package/src/session-kit/primitives/PortalGlyph.tsx +0 -51
  131. package/src/session-kit/primitives/RiskPill.tsx +0 -95
  132. package/src/session-kit/primitives/ScopeDot.tsx +0 -47
  133. package/src/session-kit/primitives/Segmented.tsx +0 -71
  134. package/src/ui/branding/app-branding-context.ts +0 -20
  135. package/src/ui/branding/app-branding-provider.tsx +0 -123
  136. package/src/ui/branding/sanitize.ts +0 -89
  137. package/src/ui/breakpoints.ts +0 -14
  138. package/src/ui/capture/document-preview.tsx +0 -166
  139. package/src/ui/capture/field-spec-to-zod.test.ts +0 -81
  140. package/src/ui/capture/field-spec-to-zod.ts +0 -174
  141. package/src/ui/capture/field-spec-validate.ts +0 -236
  142. package/src/ui/capture/field-spec.test.ts +0 -49
  143. package/src/ui/capture/field-spec.ts +0 -274
  144. package/src/ui/capture/index.ts +0 -51
  145. package/src/ui/capture/schema-field-input.tsx +0 -620
  146. package/src/ui/capture/schema-form.tsx +0 -149
  147. package/src/ui/capture/submit-values.test.ts +0 -70
  148. package/src/ui/capture/submit-values.ts +0 -63
  149. package/src/ui/capture/use-prefill.ts +0 -186
  150. package/src/ui/capture/use-schema-validation.ts +0 -145
  151. package/src/ui/capture/use-wizard.ts +0 -180
  152. package/src/ui/capture/value-format.ts +0 -32
  153. package/src/ui/capture/wizard.tsx +0 -105
  154. package/src/ui/charts/bar-chart.tsx +0 -139
  155. package/src/ui/charts/chart-frame.tsx +0 -125
  156. package/src/ui/charts/chart-types.ts +0 -52
  157. package/src/ui/charts/chart-utils.ts +0 -190
  158. package/src/ui/charts/charts.test.tsx +0 -204
  159. package/src/ui/charts/donut-chart.tsx +0 -129
  160. package/src/ui/charts/index.ts +0 -27
  161. package/src/ui/charts/line-chart.tsx +0 -202
  162. package/src/ui/charts/sparkline.tsx +0 -109
  163. package/src/ui/chrome/AsyncBoundary.tsx +0 -74
  164. package/src/ui/chrome/EmptyState.tsx +0 -257
  165. package/src/ui/chrome/ErrorCard.tsx +0 -63
  166. package/src/ui/chrome/LoadingState.tsx +0 -61
  167. package/src/ui/chrome/PageHeader.tsx +0 -137
  168. package/src/ui/chrome/RecordCardList.tsx +0 -98
  169. package/src/ui/chrome/StateCard.tsx +0 -150
  170. package/src/ui/chrome/confirm-delete-dialog.tsx +0 -59
  171. package/src/ui/chrome/page-shell.spec.tsx +0 -117
  172. package/src/ui/chrome/page-shell.tsx +0 -221
  173. package/src/ui/chrome/scope-badge.tsx +0 -40
  174. package/src/ui/chrome/status-badge.tsx +0 -79
  175. package/src/ui/cn.ts +0 -32
  176. package/src/ui/design-tokens.ts +0 -372
  177. package/src/ui/hooks/use-debounced-value.ts +0 -16
  178. package/src/ui/hooks/use-is-mobile.ts +0 -28
  179. package/src/ui/index.ts +0 -110
  180. package/src/ui/pdf/pdf-preview.tsx +0 -122
  181. package/src/ui/primitives/alert-dialog.tsx +0 -104
  182. package/src/ui/primitives/async-combobox.tsx +0 -182
  183. package/src/ui/primitives/badge-variants.ts +0 -21
  184. package/src/ui/primitives/badge.tsx +0 -25
  185. package/src/ui/primitives/button-variants.ts +0 -31
  186. package/src/ui/primitives/button.tsx +0 -29
  187. package/src/ui/primitives/card.tsx +0 -43
  188. package/src/ui/primitives/checkbox.tsx +0 -26
  189. package/src/ui/primitives/collapsible.tsx +0 -9
  190. package/src/ui/primitives/dialog.tsx +0 -103
  191. package/src/ui/primitives/dropdown-menu.tsx +0 -179
  192. package/src/ui/primitives/form-stepper.tsx +0 -94
  193. package/src/ui/primitives/form-stepper.types.ts +0 -15
  194. package/src/ui/primitives/input.tsx +0 -22
  195. package/src/ui/primitives/label.tsx +0 -17
  196. package/src/ui/primitives/overflow-tabs.tsx +0 -331
  197. package/src/ui/primitives/popover.tsx +0 -33
  198. package/src/ui/primitives/preview-device-frame.tsx +0 -213
  199. package/src/ui/primitives/radio-group.tsx +0 -36
  200. package/src/ui/primitives/resizable.tsx +0 -67
  201. package/src/ui/primitives/scroll-area.tsx +0 -38
  202. package/src/ui/primitives/select.tsx +0 -143
  203. package/src/ui/primitives/separator.tsx +0 -20
  204. package/src/ui/primitives/sheet.tsx +0 -107
  205. package/src/ui/primitives/skeleton.tsx +0 -99
  206. package/src/ui/primitives/switch.tsx +0 -27
  207. package/src/ui/primitives/table.tsx +0 -72
  208. package/src/ui/primitives/tabs.tsx +0 -53
  209. package/src/ui/primitives/tag-multi-select.tsx +0 -261
  210. package/src/ui/primitives/textarea.tsx +0 -21
  211. package/src/ui/primitives/tooltip.tsx +0 -30
  212. package/src/ui/primitives/url-preview-chrome.tsx +0 -103
  213. package/src/vista/GenericCatalogueSurface.tsx +0 -179
  214. package/src/vista/SurfaceFallbackCard.tsx +0 -109
  215. package/src/vista/VistaPanel.tsx +0 -163
  216. package/src/vista/form-payload.spec.ts +0 -139
  217. package/src/vista/html/CustomUiFrame.spec.tsx +0 -127
  218. package/src/vista/html/CustomUiFrame.tsx +0 -206
  219. package/src/vista/html/custom-ui-frame-bridge.spec.ts +0 -191
  220. package/src/vista/html/custom-ui-frame-bridge.ts +0 -98
  221. package/src/vista/html/custom-ui-frame-contract.ts +0 -88
  222. package/src/vista/html/custom-ui-intents.spec.ts +0 -135
  223. package/src/vista/html/custom-ui-intents.ts +0 -155
  224. package/src/vista/html/custom-ui-srcdoc.spec.ts +0 -151
  225. package/src/vista/html/custom-ui-srcdoc.ts +0 -193
  226. package/src/vista/html/index.ts +0 -13
  227. package/src/vista/index.ts +0 -52
  228. package/src/vista/surface-catalogue.spec.ts +0 -140
  229. package/src/vista/surface-catalogue.ts +0 -90
  230. package/src/vista/surface-icons.ts +0 -57
  231. package/src/vista/surface-registry.ts +0 -56
  232. package/src/vista/surfaces/ChartSurface.tsx +0 -34
  233. package/src/vista/surfaces/FilesSurface.tsx +0 -202
  234. package/src/vista/surfaces/FormSurface.tsx +0 -380
  235. package/src/vista/surfaces/HtmlSurface.tsx +0 -43
  236. package/src/vista/surfaces/TableSurface.tsx +0 -100
  237. package/src/vista/surfaces/UrlSurface.tsx +0 -40
  238. package/src/vista/surfaces/blob-surfaces.tsx +0 -139
  239. package/src/vista/surfaces/elicitation-transport.spec.ts +0 -369
  240. package/src/vista/surfaces/elicitation-transport.ts +0 -367
  241. package/src/vista/surfaces/file-surface-utils.ts +0 -69
  242. package/src/vista/surfaces/index.ts +0 -24
  243. package/src/vista/surfaces/register-surfaces.tsx +0 -139
  244. package/src/vista/surfaces/text-surfaces.tsx +0 -132
  245. package/src/vista/use-vista-tabs.ts +0 -196
  246. package/src/vista/vista-code-renderer-context.ts +0 -16
  247. package/src/vista/vista-code-renderer.tsx +0 -35
  248. package/src/vista/vista-file-context.ts +0 -14
  249. package/src/vista/vista-file-source.tsx +0 -141
  250. package/src/vista/vista-form-context.ts +0 -20
  251. package/src/vista/vista-form-host.tsx +0 -48
  252. package/src/vista/vista-model.ts +0 -137
  253. package/src/vista/vista-open-context.tsx +0 -27
  254. package/src/vista/vista-open-state.ts +0 -11
  255. package/src/vista/vista-payloads.ts +0 -635
  256. package/src/vista/vista-tabs-manifest.ts +0 -138
  257. package/src/vista/vista-tools.ts +0 -232
@@ -1,82 +0,0 @@
1
- /**
2
- * Shared Orval-client wiring for frontend biomes.
3
- *
4
- * Every biome that calls a platform API through an Orval-generated client must
5
- * configure that client with the SAME auth/header/401 behavior the host shell
6
- * uses for its own clients. Hand-copying a `configureOrgDbClient`-style helper
7
- * into each biome (as `org-databases` did across three hook files) duplicates
8
- * the correlation-id + org-header + token logic and lets it drift from the
9
- * host. This factory centralizes it: a biome builds one config from its
10
- * `HostBridge` and feeds it straight into the client's `configureClient(...)`.
11
- *
12
- * Auth/refresh/redirect stay HOST-owned — this only READS the bridge's
13
- * host-implemented methods (`getActorToken`, `ensureFreshToken`,
14
- * `onUnauthorized`, `getOrgId`). It owns no auth state of its own.
15
- */
16
- import type { HostBridge } from './host-bridge';
17
-
18
- /**
19
- * The Orval client config shape every generated `configureClient(...)` accepts.
20
- * Kept structural (not imported from a client package) so the kernel stays free
21
- * of any concrete service-client dependency — biomes pass the result straight
22
- * into their own client's `configureClient`, which validates the shape.
23
- */
24
- export interface BiomeOrvalConfig {
25
- readonly baseUrl: string;
26
- readonly maxRetries: number;
27
- getAuthToken(): Promise<string>;
28
- getHeaders(): Promise<Record<string, string>>;
29
- onUnauthorized(): Promise<void>;
30
- }
31
-
32
- export interface CreateBiomeOrvalConfigOptions {
33
- /** Absolute base URL of the target service. */
34
- readonly baseUrl: string;
35
- /**
36
- * Retry budget for the underlying fetch wrapper. Defaults to 1 — a single
37
- * retry covers the `onUnauthorized` refresh-then-retry path without turning
38
- * a hard failure into a retry storm.
39
- */
40
- readonly maxRetries?: number;
41
- }
42
-
43
- /**
44
- * Build an Orval client config from a `HostBridge`. Wires:
45
- * - `getAuthToken` → `bridge.auth.ensureFreshToken()` (proactive refresh;
46
- * the host owns the refresh + login-redirect single-flight).
47
- * - `getHeaders` → a fresh `X-Correlation-Id` per call, plus `X-Xema-Org-Id`
48
- * (the canonical Xema-platform tenant header every platform service reads
49
- * via `RequestContext`) when an org is selected. Biome web clients only ever
50
- * call Xema-platform biome-apis, so this is the single correct tenant
51
- * header; the previously-sent legacy `X-Org-Id` was dead (the central
52
- * read-side was decommissioned) and left context-derived reads without an
53
- * org. Per-call headers set by the caller still win in the client's own
54
- * header merge.
55
- * - `onUnauthorized` → `bridge.auth.onUnauthorized()` (host force-refresh +
56
- * redirect-on-failure).
57
- *
58
- * @example
59
- * configureClient(createBiomeOrvalConfig(bridge, { baseUrl: API_BASE_URL }));
60
- */
61
- export function createBiomeOrvalConfig(
62
- bridge: HostBridge,
63
- opts: CreateBiomeOrvalConfigOptions,
64
- ): BiomeOrvalConfig {
65
- return {
66
- baseUrl: opts.baseUrl,
67
- maxRetries: opts.maxRetries ?? 1,
68
- getAuthToken: () => bridge.auth.ensureFreshToken(),
69
- getHeaders: () => {
70
- const headers: Record<string, string> = {
71
- 'X-Correlation-Id': crypto.randomUUID(),
72
- };
73
- const orgId = bridge.auth.getOrgId();
74
- if (orgId) {
75
- // Canonical header read by every Xema-platform service's RequestContext.
76
- headers['X-Xema-Org-Id'] = orgId;
77
- }
78
- return Promise.resolve(headers);
79
- },
80
- onUnauthorized: () => bridge.auth.onUnauthorized(),
81
- };
82
- }
@@ -1,221 +0,0 @@
1
- /**
2
- * `defineWebBiome` — define-time scope/access locking, the fail-closed
3
- * defaults, and closed-set completeness.
4
- *
5
- * This suite replaces an earlier `define-web-biome.test.js` written for
6
- * Node's test runner against `dist/`. That file ran NOWHERE: the package's
7
- * `test` script is a notice (the kernel carries no test runner by design) and
8
- * the host-web vitest `include` is `*.{test,spec}.{ts,tsx}` — so a `.js` file
9
- * was invisible to both. A security test that never executes reads as
10
- * coverage while proving nothing, so it is gone; this vitest spec is synced
11
- * into the host mirror by the frontend bridge and runs there with everything
12
- * else.
13
- *
14
- * The invariant under test is the security one: a route is public ONLY when
15
- * its author said so twice, explicitly. Nothing defaulted, inferred, or
16
- * coerced can produce a public route.
17
- */
18
- import { describe, expect, it } from 'vitest';
19
-
20
- import { defineWebBiome } from './define-web-biome';
21
- import {
22
- PAGE_CHROME_VALUES,
23
- ROUTE_ACCESS_VALUES,
24
- ROUTE_SCOPE_VALUES,
25
- type PageChrome,
26
- type RouteAccess,
27
- type RouteScope,
28
- } from './frontend-biome';
29
-
30
- import type { HostBridge } from './host-bridge';
31
-
32
- const load = () => Promise.resolve({ default: () => null });
33
- const bridge = {} as HostBridge;
34
-
35
- describe('defineWebBiome — public/chromeless scope', () => {
36
- it('rejects a public page whose access defaults to member', () => {
37
- expect(() =>
38
- defineWebBiome({
39
- id: 'test-biome',
40
- displayName: 'Test',
41
- pages: [{ slug: 'ext/:id', label: 'Ext', scope: 'public', load }],
42
- }),
43
- ).toThrow(/a 'public' scope page MUST declare access: 'public'/);
44
- });
45
-
46
- it('rejects a public page with an explicit non-public access tier', () => {
47
- expect(() =>
48
- defineWebBiome({
49
- id: 'test-biome',
50
- displayName: 'Test',
51
- pages: [
52
- {
53
- slug: 'ext/:id',
54
- label: 'Ext',
55
- scope: 'public',
56
- access: 'org-admin',
57
- load,
58
- },
59
- ],
60
- }),
61
- ).toThrow(/a 'public' scope page MUST declare access: 'public'/);
62
- });
63
-
64
- it('rejects a non-public page that declares access public', () => {
65
- expect(() =>
66
- defineWebBiome({
67
- id: 'test-biome',
68
- displayName: 'Test',
69
- pages: [{ slug: 'system/x', label: 'X', access: 'public', load }],
70
- }),
71
- ).toThrow(/access: 'public' is only valid on a 'public' scope page/);
72
- });
73
-
74
- it('accepts a public + access:public page and emits no nav item', () => {
75
- const biome = defineWebBiome({
76
- id: 'test-biome',
77
- displayName: 'Test',
78
- pages: [
79
- {
80
- slug: 'ext/:id',
81
- label: 'Ext',
82
- scope: 'public',
83
- access: 'public',
84
- load,
85
- },
86
- ],
87
- })(bridge);
88
-
89
- expect(biome.routes).toHaveLength(1);
90
- const route = biome.routes?.[0];
91
- expect(route?.path).toBe('ext/:id');
92
- expect(route?.access).toBe('public');
93
- expect(route?.projectScoped).toBe(false);
94
- // A public route is un-authenticated; it never belongs in the authed rail.
95
- expect(biome.navItems).toHaveLength(0);
96
- });
97
-
98
- it('chrome defaults to host and none passes through', () => {
99
- const biome = defineWebBiome({
100
- id: 'test-biome',
101
- displayName: 'Test',
102
- pages: [
103
- { slug: 'system/a', label: 'A', load },
104
- { slug: 'system/b', label: 'B', chrome: 'none', load },
105
- ],
106
- })(bridge);
107
-
108
- expect(biome.routes?.find((r) => r.path === 'system/a')?.chrome).toBe(
109
- 'host',
110
- );
111
- expect(biome.routes?.find((r) => r.path === 'system/b')?.chrome).toBe(
112
- 'none',
113
- );
114
- });
115
- });
116
-
117
- describe('defineWebBiome — public is opt-in, never a default', () => {
118
- /**
119
- * THE fail-closed invariant. An ordinarily-declared page (no `scope`, no
120
- * `access`) must emit a route that carries NO `access` marker at all — the
121
- * host then applies its own `member` default. If the kernel ever started
122
- * defaulting `access` (or `scope`) to `'public'`, every existing biome route
123
- * in the fleet would silently become reachable without a session.
124
- *
125
- * Mutation-proof (recorded 2026-07-22): flipping the emit in
126
- * `defineWebBiome` to `access: page.access ?? 'public'` turns this test RED
127
- * ("expected 'public' to be undefined"), and flipping the validator default
128
- * `page.access ?? 'member'` to `?? 'public'` makes the ordinary page throw
129
- * at define time — RED in this test and in the two below. There is no
130
- * mutation of a default that makes a route public and keeps the suite green.
131
- */
132
- it('an ordinary page emits NO access marker (host defaults to member)', () => {
133
- const biome = defineWebBiome({
134
- id: 'test-biome',
135
- displayName: 'Test',
136
- pages: [{ slug: 'system/a', label: 'A', load }],
137
- })(bridge);
138
-
139
- const route = biome.routes?.[0];
140
- expect(route?.access).toBeUndefined();
141
- expect(route?.projectScoped).toBe(false);
142
- // …and it DOES contribute a nav item, unlike a public route.
143
- expect(biome.navItems).toHaveLength(1);
144
- });
145
-
146
- it('an authenticated page can be chromeless without becoming public', () => {
147
- // Chrome and access are orthogonal axes: the embedded-app case.
148
- const biome = defineWebBiome({
149
- id: 'test-biome',
150
- displayName: 'Test',
151
- pages: [
152
- {
153
- slug: 'apps/:appId/embed',
154
- label: 'Embedded app',
155
- chrome: 'none',
156
- navHidden: true,
157
- load,
158
- },
159
- ],
160
- })(bridge);
161
-
162
- const route = biome.routes?.[0];
163
- expect(route?.chrome).toBe('none');
164
- expect(route?.access).toBeUndefined();
165
- });
166
-
167
- it('every non-public access tier is rejected on a public page', () => {
168
- for (const access of ROUTE_ACCESS_VALUES.filter((a) => a !== 'public')) {
169
- expect(() =>
170
- defineWebBiome({
171
- id: 'test-biome',
172
- displayName: 'Test',
173
- pages: [{ slug: 'ext', label: 'Ext', scope: 'public', access, load }],
174
- }),
175
- ).toThrow(/MUST declare access: 'public'/);
176
- }
177
- });
178
-
179
- it('every non-public scope rejects access public', () => {
180
- for (const scope of ROUTE_SCOPE_VALUES.filter((s) => s !== 'public')) {
181
- expect(() =>
182
- defineWebBiome({
183
- id: 'test-biome',
184
- displayName: 'Test',
185
- pages: [{ slug: 'x', label: 'X', scope, access: 'public', load }],
186
- }),
187
- ).toThrow(/only valid on a 'public' scope page/);
188
- }
189
- });
190
- });
191
-
192
- describe('closed-set completeness', () => {
193
- /**
194
- * The runtime tuples ARE the types (`RouteAccess = typeof
195
- * ROUTE_ACCESS_VALUES[number]`), so a new member cannot be added to the type
196
- * without appearing here — which is what makes the exhaustive iteration in
197
- * this file, and the host resolver's `default`-less switch, complete by
198
- * construction. These assertions are the compile-time proof made executable:
199
- * the `satisfies` widening below fails to compile if a tuple ever drifts
200
- * from its type.
201
- */
202
- it('the access tuple enumerates exactly the RouteAccess union', () => {
203
- const all: readonly RouteAccess[] =
204
- ROUTE_ACCESS_VALUES satisfies readonly RouteAccess[];
205
- expect([...all].sort()).toEqual(
206
- ['member', 'org-admin', 'platform-admin', 'public'].sort(),
207
- );
208
- });
209
-
210
- it('the scope tuple enumerates exactly the RouteScope union', () => {
211
- const all: readonly RouteScope[] =
212
- ROUTE_SCOPE_VALUES satisfies readonly RouteScope[];
213
- expect([...all].sort()).toEqual(['org', 'project', 'public'].sort());
214
- });
215
-
216
- it('the chrome tuple enumerates exactly the PageChrome union', () => {
217
- const all: readonly PageChrome[] =
218
- PAGE_CHROME_VALUES satisfies readonly PageChrome[];
219
- expect([...all].sort()).toEqual(['host', 'none'].sort());
220
- });
221
- });
@@ -1,222 +0,0 @@
1
- /**
2
- * `defineWebBiome` — the authoring helper for `target: 'web'` biomes.
3
- *
4
- * Every web biome used to hand-repeat the same ~30 lines: one
5
- * `lazy(() => import('./pages/X'))` per page, a `navItems` array, and a
6
- * parallel `routes` array — writing the slug TWICE (once as `navItem.route`,
7
- * once as `route.path`), the classic source of "nav points at a path no route
8
- * serves" bugs.
9
- *
10
- * `defineWebBiome` collapses that to one declarative page list. Each
11
- * {@link WebBiomePage} single-sources its `slug` for BOTH the nav route and the
12
- * route path, so they can never drift. The helper emits one nav item (unless
13
- * `navHidden`) and one route per page, wrapping the lazily-loaded page in a
14
- * `<Suspense fallback={null}>` boundary via {@link lazyRoute}.
15
- *
16
- * Authors declare WHAT each surface is (`category`) and let the HOST own WHERE
17
- * it renders in the global menu. `init`/`dispose`/`panels`/`session`/
18
- * `outputRenderers` pass through to the `FrontendBiome` untouched, so bespoke
19
- * biomes (one-shot registrations, slot fillers, session contributions) still
20
- * use this helper.
21
- *
22
- * Host-framework-agnostic: pure React (`lazy`/`Suspense`/`createElement` via
23
- * `lazyRoute`), no router and no Next.js.
24
- *
25
- * @example
26
- * export default defineWebBiome({
27
- * id: 'spaces-web',
28
- * displayName: 'Spaces',
29
- * pages: [
30
- * {
31
- * slug: 'system/spaces',
32
- * label: 'Spaces',
33
- * icon: Layers,
34
- * category: 'knowledge',
35
- * load: () => import('./pages/SpacesPage'),
36
- * },
37
- * ],
38
- * });
39
- */
40
-
41
- import { lazyRoute } from './biome-builders';
42
-
43
- import type {
44
- FrontendBiome,
45
- FrontendBiomeFactory,
46
- NavItemContribution,
47
- PageChrome,
48
- RouteAccess,
49
- RouteContribution,
50
- RouteScope,
51
- } from './frontend-biome';
52
- import type { ComponentType } from 'react';
53
-
54
- export interface WebBiomePage {
55
- /**
56
- * Single source of truth for BOTH the nav route AND the route path — the
57
- * helper sets `navItem.route === route.path === slug`, so they can never
58
- * diverge. May include `:param` segments (e.g. `system/concepts/:slug`).
59
- */
60
- readonly slug: string;
61
- /**
62
- * Stable nav-item id used for active-state matching + analytics. Defaults to
63
- * {@link slug} when omitted — pass it explicitly only to preserve a
64
- * pre-existing short id (e.g. `grants`) that differs from the route slug
65
- * (`system/grants`). Must be unique across the biome's pages.
66
- */
67
- readonly id?: string;
68
- /** Displayed nav label. */
69
- readonly label: string;
70
- /** Optional nav icon. Typed to match {@link NavItemContribution.icon}. */
71
- readonly icon?: ComponentType<{ className?: string | undefined }>;
72
- /**
73
- * Host menu category — the biome's INTENT (`primary` | `build` | `operate` |
74
- * `knowledge` | `admin` | `account`). The host maps it to a concrete rail
75
- * group + order.
76
- */
77
- readonly category?: string;
78
- /**
79
- * Route scope. `'org'` (default) mounts at the org root; `'project'` mounts
80
- * under the canonical project-scoped route prefix; `'public'` mounts OUTSIDE
81
- * the host's authenticated group (no Keycloak session, no login redirect, no
82
- * org switcher). Maps to {@link RouteContribution.projectScoped} for the
83
- * authenticated scopes; a `'public'` page MUST also declare
84
- * `access: 'public'` (enforced at define time).
85
- */
86
- readonly scope?: RouteScope;
87
- /**
88
- * Access tier required to render the route. Defaults to `'member'`. A
89
- * `'public'` page (scope `'public'`) MUST set this to `'public'`; a non-public
90
- * page MUST NOT — both contradictions are rejected at define time.
91
- */
92
- readonly access?: RouteAccess;
93
- /**
94
- * Whether the host wraps the page in its application shell. Defaults to
95
- * `'host'`; `'none'` renders with no app rail, topbar, or org switcher.
96
- */
97
- readonly chrome?: PageChrome;
98
- /** Dynamic import of the page module (`() => import('./pages/X')`). */
99
- readonly load: () => Promise<{ default: ComponentType<unknown> }>;
100
- /**
101
- * When true the route is registered but NO nav item is emitted — for hidden
102
- * surfaces such as a `:param` detail route reached only by deeplink.
103
- */
104
- readonly navHidden?: boolean;
105
- }
106
-
107
- export interface DefineWebBiomeOptions {
108
- /** Stable biome id; matches `xema-biome.json` `xema.id`. */
109
- readonly id: string;
110
- /** Displayed name. */
111
- readonly displayName: string;
112
- /** One-shot initializer; passed through to {@link FrontendBiome.init}. */
113
- readonly init?: FrontendBiome['init'];
114
- /** Teardown counterpart; passed through to {@link FrontendBiome.dispose}. */
115
- readonly dispose?: FrontendBiome['dispose'];
116
- /** The biome's pages — one nav item (unless `navHidden`) + one route each. */
117
- readonly pages: readonly WebBiomePage[];
118
- /** Slot fillers; passed through to {@link FrontendBiome.panels}. */
119
- readonly panels?: FrontendBiome['panels'];
120
- /** Session-shell contributions; passed through to {@link FrontendBiome.session}. */
121
- readonly session?: FrontendBiome['session'];
122
- /** Output renderers; passed through to {@link FrontendBiome.outputRenderers}. */
123
- readonly outputRenderers?: FrontendBiome['outputRenderers'];
124
- }
125
-
126
- /** Default host-shell mode a page renders in when it omits `chrome`. */
127
- const DEFAULT_PAGE_CHROME: PageChrome = 'host';
128
-
129
- /**
130
- * Reject the two `scope`/`access` contradictions at define time (module load,
131
- * when the biome calls `defineWebBiome(...)` at its top level) — fail fast, do
132
- * NOT silently coerce:
133
- *
134
- * 1. A `public` page with any `access` other than `'public'`: a public route
135
- * is un-authenticated, so a privilege tier is meaningless. The author must
136
- * set `access: 'public'` explicitly (an omitted `access` defaults to
137
- * `'member'`, which is exactly this contradiction).
138
- * 2. A non-`public` page declaring `access: 'public'`: `'public'` is the
139
- * no-auth marker; an org/project route is always authenticated.
140
- *
141
- * `scope` defaults to `'org'` and `access` defaults to `'member'` for the
142
- * purpose of this check, matching the emit defaults below.
143
- */
144
- function assertPageScopeAccessLocked(biomeId: string, page: WebBiomePage): void {
145
- const scope: RouteScope = page.scope ?? 'org';
146
- const access: RouteAccess = page.access ?? 'member';
147
- const where = `web biome '${biomeId}' page '${page.slug}'`;
148
-
149
- if (scope === 'public' && access !== 'public') {
150
- throw new Error(
151
- `${where}: a 'public' scope page MUST declare access: 'public' ` +
152
- `(got access: '${access}'). A public route is un-authenticated, so an ` +
153
- `authenticated privilege tier is a contradiction. Set access: 'public' ` +
154
- `or change the scope.`,
155
- );
156
- }
157
- if (scope !== 'public' && access === 'public') {
158
- throw new Error(
159
- `${where}: access: 'public' is only valid on a 'public' scope page ` +
160
- `(got scope: '${scope}'). '${access}' is the no-auth marker; an ` +
161
- `org/project route is always authenticated. Set scope: 'public' or ` +
162
- `change the access tier.`,
163
- );
164
- }
165
- }
166
-
167
- /**
168
- * Build the `FrontendBiomeFactory` a web biome default-exports. See the
169
- * module doc-comment for the rationale and an example.
170
- */
171
- export function defineWebBiome(
172
- options: DefineWebBiomeOptions,
173
- ): FrontendBiomeFactory {
174
- // Validate every page's scope/access pairing NOW, at module load, so a
175
- // contradiction surfaces the instant the biome module is imported — not
176
- // lazily when the host first calls the factory.
177
- for (const page of options.pages) {
178
- assertPageScopeAccessLocked(options.id, page);
179
- }
180
-
181
- return () => {
182
- const navItems: NavItemContribution[] = [];
183
- const routes: RouteContribution[] = [];
184
-
185
- for (const page of options.pages) {
186
- routes.push({
187
- path: page.slug,
188
- projectScoped: page.scope === 'project',
189
- element: lazyRoute(page.load),
190
- chrome: page.chrome ?? DEFAULT_PAGE_CHROME,
191
- ...(page.access ? { access: page.access } : {}),
192
- });
193
-
194
- // Public routes mount OUTSIDE the authenticated group and never belong in
195
- // the authenticated app rail — so they contribute no nav item regardless
196
- // of `navHidden`. Authenticated pages emit a nav item unless hidden.
197
- if (!page.navHidden && page.scope !== 'public') {
198
- navItems.push({
199
- id: page.id ?? page.slug,
200
- label: page.label,
201
- route: page.slug,
202
- ...(page.icon ? { icon: page.icon } : {}),
203
- ...(page.category !== undefined ? { category: page.category } : {}),
204
- });
205
- }
206
- }
207
-
208
- return {
209
- id: options.id,
210
- displayName: options.displayName,
211
- navItems,
212
- routes,
213
- ...(options.init ? { init: options.init } : {}),
214
- ...(options.dispose ? { dispose: options.dispose } : {}),
215
- ...(options.panels ? { panels: options.panels } : {}),
216
- ...(options.session ? { session: options.session } : {}),
217
- ...(options.outputRenderers
218
- ? { outputRenderers: options.outputRenderers }
219
- : {}),
220
- };
221
- };
222
- }