@xemahq/ui-kernel 0.11.0 → 0.11.2

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 (237) hide show
  1. package/dist/ui/chrome/page-shell.d.ts +1 -1
  2. package/dist/ui/chrome/page-shell.d.ts.map +1 -1
  3. package/dist/ui/chrome/page-shell.js +9 -4
  4. package/dist/ui/chrome/page-shell.js.map +1 -1
  5. package/package.json +6 -4
  6. package/src/index.ts +0 -17
  7. package/src/lib/biome-host/agent-validation.ts +0 -239
  8. package/src/lib/biome-host/biome-builders.ts +0 -112
  9. package/src/lib/biome-host/biome-mode.ts +0 -6
  10. package/src/lib/biome-host/biome-navigation.ts +0 -37
  11. package/src/lib/biome-host/biome-registry.ts +0 -299
  12. package/src/lib/biome-host/biome-scope-hooks.ts +0 -41
  13. package/src/lib/biome-host/biome-scope.tsx +0 -52
  14. package/src/lib/biome-host/biome-scoped-query.ts +0 -134
  15. package/src/lib/biome-host/create-biome-orval-config.ts +0 -82
  16. package/src/lib/biome-host/define-web-biome.spec.ts +0 -221
  17. package/src/lib/biome-host/define-web-biome.ts +0 -222
  18. package/src/lib/biome-host/errors.ts +0 -220
  19. package/src/lib/biome-host/frontend-biome.ts +0 -256
  20. package/src/lib/biome-host/host-bridge.ts +0 -281
  21. package/src/lib/biome-host/host-sources.ts +0 -54
  22. package/src/lib/biome-host/i18n-hooks.ts +0 -51
  23. package/src/lib/biome-host/i18n-port.test.ts +0 -50
  24. package/src/lib/biome-host/i18n-port.ts +0 -96
  25. package/src/lib/biome-host/index.ts +0 -35
  26. package/src/lib/biome-host/realtime-hooks.ts +0 -75
  27. package/src/lib/biome-host/realtime-port.ts +0 -115
  28. package/src/lib/biome-host/session-contributions.ts +0 -296
  29. package/src/lib/biome-host/use-mutation-with-error-toast.ts +0 -90
  30. package/src/lib/biome-host/use-page-state.ts +0 -248
  31. package/src/lib/capabilities/capability-context.ts +0 -25
  32. package/src/lib/capabilities/capability-provider.tsx +0 -73
  33. package/src/lib/capabilities/index.ts +0 -17
  34. package/src/lib/capabilities/types.ts +0 -69
  35. package/src/lib/capabilities/use-capability.ts +0 -79
  36. package/src/lib/contribution-registry/contribution-registry.spec.ts +0 -141
  37. package/src/lib/contribution-registry/contribution-registry.ts +0 -204
  38. package/src/lib/contribution-registry/index.ts +0 -9
  39. package/src/lib/contribution-registry/renderable-registry.ts +0 -142
  40. package/src/lib/system-bus/capability-invoke.ts +0 -92
  41. package/src/lib/system-bus/deeplink.ts +0 -210
  42. package/src/lib/system-bus/enums.ts +0 -86
  43. package/src/lib/system-bus/host-ports.ts +0 -93
  44. package/src/lib/system-bus/index.ts +0 -16
  45. package/src/lib/system-bus/intent-registry.ts +0 -109
  46. package/src/lib/system-bus/intents.ts +0 -113
  47. package/src/lib/system-bus/palette.ts +0 -78
  48. package/src/lib/system-bus/system-bus-builder.ts +0 -159
  49. package/src/lib/system-bus/system-bus.ts +0 -37
  50. package/src/lib/system-bus/windows.ts +0 -51
  51. package/src/offline/id.ts +0 -16
  52. package/src/offline/index.ts +0 -54
  53. package/src/offline/mutation.ts +0 -75
  54. package/src/offline/offline-status-badge.tsx +0 -51
  55. package/src/offline/online-controller.ts +0 -105
  56. package/src/offline/outbox.test.ts +0 -172
  57. package/src/offline/outbox.ts +0 -348
  58. package/src/offline/react-hooks.ts +0 -40
  59. package/src/offline/react.tsx +0 -32
  60. package/src/offline/storage.ts +0 -112
  61. package/src/offline/types.ts +0 -77
  62. package/src/org-db/components/DbResultTable.tsx +0 -176
  63. package/src/org-db/index.ts +0 -1
  64. package/src/pdf/index.ts +0 -11
  65. package/src/registry/index.ts +0 -9
  66. package/src/registry/lib/agent-validation-host.ts +0 -36
  67. package/src/registry/lib/biome-slot.tsx +0 -50
  68. package/src/registry/lib/biomes-enabled-context.ts +0 -20
  69. package/src/registry/lib/extension-points.ts +0 -134
  70. package/src/registry/lib/primitives/SessionEventCard.tsx +0 -164
  71. package/src/registry/lib/primitives/SessionEventTimeline.tsx +0 -95
  72. package/src/registry/lib/primitives/SessionMutationBar.tsx +0 -76
  73. package/src/registry/lib/primitives/SessionTabbedDrawer.tsx +0 -165
  74. package/src/registry/lib/primitives/index.ts +0 -18
  75. package/src/registry/lib/session-context-builder.ts +0 -84
  76. package/src/registry/lib/session-context-provider.tsx +0 -33
  77. package/src/registry/lib/session-context.ts +0 -21
  78. package/src/registry/lib/session-selectors.ts +0 -272
  79. package/src/session/comments/CommentRail.tsx +0 -164
  80. package/src/session/index.ts +0 -3
  81. package/src/session/lib/cn.ts +0 -11
  82. package/src/session/shell/SessionWorkspaceShell.tsx +0 -147
  83. package/src/session-kit/approvals/ApprovalButton.tsx +0 -89
  84. package/src/session-kit/approvals/ApprovalCard.tsx +0 -341
  85. package/src/session-kit/approvals/ApprovalsCenter.tsx +0 -336
  86. package/src/session-kit/approvals/CapabilityApprovalStickyBar.tsx +0 -118
  87. package/src/session-kit/approvals/Obligation.tsx +0 -111
  88. package/src/session-kit/approvals/ScopedBody.tsx +0 -392
  89. package/src/session-kit/approvals/approval-icons.ts +0 -31
  90. package/src/session-kit/approvals/approval-model.ts +0 -204
  91. package/src/session-kit/approvals/index.ts +0 -22
  92. package/src/session-kit/approvals/obligation-display.ts +0 -100
  93. package/src/session-kit/approvals/risk-accent.ts +0 -47
  94. package/src/session-kit/approvals/scope-icons.ts +0 -19
  95. package/src/session-kit/combobox/Combobox.tsx +0 -358
  96. package/src/session-kit/combobox/use-click-outside.ts +0 -21
  97. package/src/session-kit/display/ContextHeader.tsx +0 -148
  98. package/src/session-kit/display/FileDiffCard.tsx +0 -140
  99. package/src/session-kit/display/MD.tsx +0 -162
  100. package/src/session-kit/display/MessageTurn.tsx +0 -157
  101. package/src/session-kit/display/ThinkingPanel.tsx +0 -81
  102. package/src/session-kit/display/TodoChecklist.tsx +0 -120
  103. package/src/session-kit/display/TokenMeter.tsx +0 -89
  104. package/src/session-kit/display/ToolStrip.tsx +0 -278
  105. package/src/session-kit/display/TypingDots.tsx +0 -24
  106. package/src/session-kit/index.ts +0 -44
  107. package/src/session-kit/lib/enums.ts +0 -66
  108. package/src/session-kit/lib/portal-accent.ts +0 -30
  109. package/src/session-kit/lib/status-dot.ts +0 -68
  110. package/src/session-kit/primitives/Avatar.tsx +0 -44
  111. package/src/session-kit/primitives/PortalGlyph.tsx +0 -51
  112. package/src/session-kit/primitives/RiskPill.tsx +0 -95
  113. package/src/session-kit/primitives/ScopeDot.tsx +0 -47
  114. package/src/session-kit/primitives/Segmented.tsx +0 -71
  115. package/src/ui/branding/app-branding-context.ts +0 -20
  116. package/src/ui/branding/app-branding-provider.tsx +0 -123
  117. package/src/ui/branding/sanitize.ts +0 -89
  118. package/src/ui/breakpoints.ts +0 -14
  119. package/src/ui/capture/document-preview.tsx +0 -166
  120. package/src/ui/capture/field-spec-to-zod.test.ts +0 -81
  121. package/src/ui/capture/field-spec-to-zod.ts +0 -174
  122. package/src/ui/capture/field-spec-validate.ts +0 -236
  123. package/src/ui/capture/field-spec.test.ts +0 -49
  124. package/src/ui/capture/field-spec.ts +0 -274
  125. package/src/ui/capture/index.ts +0 -51
  126. package/src/ui/capture/schema-field-input.tsx +0 -620
  127. package/src/ui/capture/schema-form.tsx +0 -149
  128. package/src/ui/capture/submit-values.test.ts +0 -70
  129. package/src/ui/capture/submit-values.ts +0 -63
  130. package/src/ui/capture/use-prefill.ts +0 -186
  131. package/src/ui/capture/use-schema-validation.ts +0 -145
  132. package/src/ui/capture/use-wizard.ts +0 -180
  133. package/src/ui/capture/value-format.ts +0 -32
  134. package/src/ui/capture/wizard.tsx +0 -105
  135. package/src/ui/charts/bar-chart.tsx +0 -139
  136. package/src/ui/charts/chart-frame.tsx +0 -125
  137. package/src/ui/charts/chart-types.ts +0 -52
  138. package/src/ui/charts/chart-utils.ts +0 -190
  139. package/src/ui/charts/charts.test.tsx +0 -204
  140. package/src/ui/charts/donut-chart.tsx +0 -129
  141. package/src/ui/charts/index.ts +0 -27
  142. package/src/ui/charts/line-chart.tsx +0 -202
  143. package/src/ui/charts/sparkline.tsx +0 -109
  144. package/src/ui/chrome/AsyncBoundary.tsx +0 -74
  145. package/src/ui/chrome/EmptyState.tsx +0 -257
  146. package/src/ui/chrome/ErrorCard.tsx +0 -63
  147. package/src/ui/chrome/LoadingState.tsx +0 -61
  148. package/src/ui/chrome/PageHeader.tsx +0 -137
  149. package/src/ui/chrome/RecordCardList.tsx +0 -98
  150. package/src/ui/chrome/StateCard.tsx +0 -150
  151. package/src/ui/chrome/confirm-delete-dialog.tsx +0 -59
  152. package/src/ui/chrome/page-shell.tsx +0 -189
  153. package/src/ui/chrome/scope-badge.tsx +0 -40
  154. package/src/ui/chrome/status-badge.tsx +0 -79
  155. package/src/ui/cn.ts +0 -32
  156. package/src/ui/design-tokens.ts +0 -372
  157. package/src/ui/hooks/use-debounced-value.ts +0 -16
  158. package/src/ui/hooks/use-is-mobile.ts +0 -28
  159. package/src/ui/index.ts +0 -110
  160. package/src/ui/pdf/pdf-preview.tsx +0 -122
  161. package/src/ui/primitives/alert-dialog.tsx +0 -104
  162. package/src/ui/primitives/async-combobox.tsx +0 -182
  163. package/src/ui/primitives/badge-variants.ts +0 -21
  164. package/src/ui/primitives/badge.tsx +0 -25
  165. package/src/ui/primitives/button-variants.ts +0 -31
  166. package/src/ui/primitives/button.tsx +0 -29
  167. package/src/ui/primitives/card.tsx +0 -43
  168. package/src/ui/primitives/checkbox.tsx +0 -26
  169. package/src/ui/primitives/collapsible.tsx +0 -9
  170. package/src/ui/primitives/dialog.tsx +0 -103
  171. package/src/ui/primitives/dropdown-menu.tsx +0 -179
  172. package/src/ui/primitives/form-stepper.tsx +0 -94
  173. package/src/ui/primitives/form-stepper.types.ts +0 -15
  174. package/src/ui/primitives/input.tsx +0 -22
  175. package/src/ui/primitives/label.tsx +0 -17
  176. package/src/ui/primitives/overflow-tabs.tsx +0 -331
  177. package/src/ui/primitives/popover.tsx +0 -33
  178. package/src/ui/primitives/preview-device-frame.tsx +0 -213
  179. package/src/ui/primitives/radio-group.tsx +0 -36
  180. package/src/ui/primitives/resizable.tsx +0 -67
  181. package/src/ui/primitives/scroll-area.tsx +0 -38
  182. package/src/ui/primitives/select.tsx +0 -143
  183. package/src/ui/primitives/separator.tsx +0 -20
  184. package/src/ui/primitives/sheet.tsx +0 -107
  185. package/src/ui/primitives/skeleton.tsx +0 -99
  186. package/src/ui/primitives/switch.tsx +0 -27
  187. package/src/ui/primitives/table.tsx +0 -72
  188. package/src/ui/primitives/tabs.tsx +0 -53
  189. package/src/ui/primitives/tag-multi-select.tsx +0 -261
  190. package/src/ui/primitives/textarea.tsx +0 -21
  191. package/src/ui/primitives/tooltip.tsx +0 -30
  192. package/src/ui/primitives/url-preview-chrome.tsx +0 -103
  193. package/src/vista/GenericCatalogueSurface.tsx +0 -179
  194. package/src/vista/SurfaceFallbackCard.tsx +0 -109
  195. package/src/vista/VistaPanel.tsx +0 -163
  196. package/src/vista/form-payload.spec.ts +0 -139
  197. package/src/vista/html/CustomUiFrame.spec.tsx +0 -127
  198. package/src/vista/html/CustomUiFrame.tsx +0 -206
  199. package/src/vista/html/custom-ui-frame-bridge.spec.ts +0 -191
  200. package/src/vista/html/custom-ui-frame-bridge.ts +0 -98
  201. package/src/vista/html/custom-ui-frame-contract.ts +0 -88
  202. package/src/vista/html/custom-ui-intents.spec.ts +0 -135
  203. package/src/vista/html/custom-ui-intents.ts +0 -155
  204. package/src/vista/html/custom-ui-srcdoc.spec.ts +0 -151
  205. package/src/vista/html/custom-ui-srcdoc.ts +0 -193
  206. package/src/vista/html/index.ts +0 -13
  207. package/src/vista/index.ts +0 -52
  208. package/src/vista/surface-catalogue.spec.ts +0 -140
  209. package/src/vista/surface-catalogue.ts +0 -90
  210. package/src/vista/surface-icons.ts +0 -57
  211. package/src/vista/surface-registry.ts +0 -56
  212. package/src/vista/surfaces/ChartSurface.tsx +0 -34
  213. package/src/vista/surfaces/FilesSurface.tsx +0 -202
  214. package/src/vista/surfaces/FormSurface.tsx +0 -380
  215. package/src/vista/surfaces/HtmlSurface.tsx +0 -43
  216. package/src/vista/surfaces/TableSurface.tsx +0 -100
  217. package/src/vista/surfaces/UrlSurface.tsx +0 -40
  218. package/src/vista/surfaces/blob-surfaces.tsx +0 -139
  219. package/src/vista/surfaces/elicitation-transport.spec.ts +0 -369
  220. package/src/vista/surfaces/elicitation-transport.ts +0 -367
  221. package/src/vista/surfaces/file-surface-utils.ts +0 -69
  222. package/src/vista/surfaces/index.ts +0 -24
  223. package/src/vista/surfaces/register-surfaces.tsx +0 -139
  224. package/src/vista/surfaces/text-surfaces.tsx +0 -132
  225. package/src/vista/use-vista-tabs.ts +0 -196
  226. package/src/vista/vista-code-renderer-context.ts +0 -16
  227. package/src/vista/vista-code-renderer.tsx +0 -35
  228. package/src/vista/vista-file-context.ts +0 -14
  229. package/src/vista/vista-file-source.tsx +0 -141
  230. package/src/vista/vista-form-context.ts +0 -20
  231. package/src/vista/vista-form-host.tsx +0 -48
  232. package/src/vista/vista-model.ts +0 -137
  233. package/src/vista/vista-open-context.tsx +0 -27
  234. package/src/vista/vista-open-state.ts +0 -11
  235. package/src/vista/vista-payloads.ts +0 -635
  236. package/src/vista/vista-tabs-manifest.ts +0 -138
  237. package/src/vista/vista-tools.ts +0 -232
@@ -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
- }
@@ -1,220 +0,0 @@
1
- /**
2
- * Canonical user-facing error decoder — ONE implementation biomes, the host
3
- * shell, and the kernel's {@link ErrorCard} default all share.
4
- *
5
- * Biomes were each re-implementing "pull a human message off an error"
6
- * (envelope shapes, status-code copy, "failed to fetch"). This is the single
7
- * host-agnostic decoder. It is a PURE function: no fetch, no i18n, no config,
8
- * no `instanceof` against host classes. It recognises the two error shapes the
9
- * Xema frontend produces by their structural signature:
10
- *
11
- * - the host's `ApiClientError` (`name === 'ApiClientError'`, `statusCode`,
12
- * `message`, optional `errorType`), thrown by the hand-rolled HTTP client;
13
- * - Orval's generated `ClientError` (`name === 'ClientError'`, `status`,
14
- * `message`, `body`), thrown by the generated clients. Its `body` carries
15
- * the backend `{ message, code, details }` envelope (and the workflow
16
- * wallet-missing / `session_busy` special cases).
17
- *
18
- * Hosts that previously kept their own `getUserFacingErrorMessage` should
19
- * re-export THIS one (or delete theirs and import it) so there is a single
20
- * decoder. {@link ErrorCard} defaults to it, so biomes get envelope-aware copy
21
- * without wiring anything.
22
- */
23
-
24
- function collapseWhitespace(value: string): string {
25
- return value.split(/\s+/).join(' ').trim();
26
- }
27
-
28
- function getGenericErrorMessage(message: string, fallback: string): string {
29
- const normalized = collapseWhitespace(message);
30
- if (/failed to fetch/i.test(normalized)) {
31
- return 'Could not reach the service. Check your connection and try again.';
32
- }
33
- return normalized || fallback;
34
- }
35
-
36
- function normalizeValidationMessage(rawMessage: string): string {
37
- const parts = rawMessage
38
- .split(',')
39
- .map((part) => collapseWhitespace(part))
40
- .filter(Boolean);
41
-
42
- if (parts.length === 0) {
43
- return 'Some required fields are missing or invalid. Please review your input and try again.';
44
- }
45
-
46
- const preview = parts.slice(0, 2).join('; ');
47
- const suffix = parts.length > 2 ? '; and more.' : '.';
48
- return `Some required fields are missing or invalid (${preview}${suffix})`;
49
- }
50
-
51
- /** Structural signature of the host's `ApiClientError`. */
52
- interface ApiClientErrorLike {
53
- readonly name: string;
54
- readonly statusCode: number;
55
- readonly message: string;
56
- readonly errorType?: string;
57
- }
58
-
59
- function isApiClientErrorLike(error: unknown): error is ApiClientErrorLike {
60
- if (!error || typeof error !== 'object') {
61
- return false;
62
- }
63
- const record = error as Record<string, unknown>;
64
- return (
65
- record['name'] === 'ApiClientError' &&
66
- typeof record['statusCode'] === 'number' &&
67
- typeof record['message'] === 'string'
68
- );
69
- }
70
-
71
- /** Structural signature of an Orval-generated `ClientError`. */
72
- interface OrvalClientErrorLike {
73
- readonly name: string;
74
- readonly status: number;
75
- readonly message: string;
76
- readonly body: unknown;
77
- }
78
-
79
- function isOrvalClientErrorLike(error: unknown): error is OrvalClientErrorLike {
80
- if (!error || typeof error !== 'object') {
81
- return false;
82
- }
83
- const record = error as Record<string, unknown>;
84
- return (
85
- record['name'] === 'ClientError' &&
86
- typeof record['status'] === 'number' &&
87
- typeof record['message'] === 'string' &&
88
- Object.hasOwn(record, 'body')
89
- );
90
- }
91
-
92
- interface WorkflowErrorEnvelope {
93
- readonly code?: string;
94
- readonly message?: string;
95
- readonly details?: Record<string, unknown>;
96
- }
97
-
98
- function readStringList(value: unknown): string[] | null {
99
- if (!Array.isArray(value)) {
100
- return null;
101
- }
102
- if (value.some((entry) => typeof entry !== 'string')) {
103
- return null;
104
- }
105
- return value;
106
- }
107
-
108
- function normalizeWorkflowWalletMissingMessage(envelope: WorkflowErrorEnvelope): string | null {
109
- if (envelope.code !== 'WORKFLOW_WALLET_NOT_FOUND') {
110
- return null;
111
- }
112
- const missing = readStringList(envelope.details?.['missing']);
113
- if (missing === null || missing.length === 0) {
114
- return 'One or more required wallets are missing for this project.';
115
- }
116
- return `Missing wallet${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}. Add ${
117
- missing.length > 1 ? 'them' : 'it'
118
- } in Project Settings -> Wallets.`;
119
- }
120
-
121
- function getApiClientErrorMessage(error: ApiClientErrorLike, fallback: string): string {
122
- if (error.statusCode === 0) {
123
- return collapseWhitespace(error.message) || fallback;
124
- }
125
- if (error.statusCode === 400) {
126
- const raw = collapseWhitespace(error.message);
127
- if (raw.includes(' must ') || raw.includes('regular expression')) {
128
- return normalizeValidationMessage(raw);
129
- }
130
- return raw || 'The request is invalid. Please check your input and try again.';
131
- }
132
- if (error.statusCode === 401) {
133
- return 'Your session expired. Please sign in again.';
134
- }
135
- if (error.statusCode === 403) {
136
- return 'You do not have permission to perform this action.';
137
- }
138
- if (error.statusCode === 404) {
139
- return 'The requested resource was not found.';
140
- }
141
- if (error.statusCode >= 500) {
142
- return 'The service is temporarily unavailable. Please try again in a moment.';
143
- }
144
- return collapseWhitespace(error.message) || fallback;
145
- }
146
-
147
- function getOrvalClientErrorMessage(error: OrvalClientErrorLike, fallback: string): string {
148
- if (error.status >= 500) {
149
- return 'The service is temporarily unavailable. Please try again in a moment.';
150
- }
151
- // session-api refuses mutations while a prompt turn is in flight; the 409
152
- // body is `{ error: 'session_busy', retryAfterTurnDone: true }`. Surface a
153
- // clear retry hint instead of the generic "Conflict" copy.
154
- if (error.status === 409) {
155
- const body = error.body;
156
- if (body && typeof body === 'object') {
157
- const record = body as Record<string, unknown>;
158
- if (record['error'] === 'session_busy') {
159
- return 'Your last message is still running. Wait for the agent to finish, then try again.';
160
- }
161
- }
162
- }
163
- const body = error.body;
164
- if (body && typeof body === 'object') {
165
- const record = body as Record<string, unknown>;
166
- const payload: WorkflowErrorEnvelope = (() => {
167
- const message = record['message'];
168
- if (message && typeof message === 'object') {
169
- return message;
170
- }
171
- return {
172
- code: typeof record['code'] === 'string' ? record['code'] : undefined,
173
- message: typeof record['message'] === 'string' ? record['message'] : undefined,
174
- details:
175
- record['details'] && typeof record['details'] === 'object'
176
- ? (record['details'] as Record<string, unknown>)
177
- : undefined,
178
- };
179
- })();
180
-
181
- const workflowWalletMessage = normalizeWorkflowWalletMissingMessage(payload);
182
- if (workflowWalletMessage !== null) {
183
- return workflowWalletMessage;
184
- }
185
- if (typeof payload.message === 'string' && payload.message.trim().length > 0) {
186
- return collapseWhitespace(payload.message);
187
- }
188
- if (typeof record['message'] === 'string' && record['message'].trim().length > 0) {
189
- return collapseWhitespace(record['message']);
190
- }
191
- }
192
- return getGenericErrorMessage(error.message, fallback);
193
- }
194
-
195
- /**
196
- * Decode any thrown value into a human, user-facing message. The single decoder
197
- * for biomes, the host shell, and {@link ErrorCard}'s default.
198
- *
199
- * @param error Any thrown value (string, `Error`, `ApiClientError`, Orval
200
- * `ClientError`, or unknown).
201
- * @param fallback Copy returned when nothing better can be extracted.
202
- */
203
- export function getUserFacingErrorMessage(
204
- error: unknown,
205
- fallback = 'Something went wrong. Please try again.',
206
- ): string {
207
- if (typeof error === 'string') {
208
- return collapseWhitespace(error) || fallback;
209
- }
210
- if (isApiClientErrorLike(error)) {
211
- return getApiClientErrorMessage(error, fallback);
212
- }
213
- if (isOrvalClientErrorLike(error)) {
214
- return getOrvalClientErrorMessage(error, fallback);
215
- }
216
- if (error instanceof Error) {
217
- return getGenericErrorMessage(error.message, fallback);
218
- }
219
- return fallback;
220
- }