@xemahq/ui-kernel 0.6.0 → 0.7.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.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/biome-host/define-web-biome.d.ts +3 -2
- package/dist/lib/biome-host/define-web-biome.d.ts.map +1 -1
- package/dist/lib/biome-host/define-web-biome.js +23 -1
- package/dist/lib/biome-host/define-web-biome.js.map +1 -1
- package/dist/lib/biome-host/frontend-biome.d.ts +4 -1
- package/dist/lib/biome-host/frontend-biome.d.ts.map +1 -1
- package/dist/lib/biome-host/host-bridge.d.ts +2 -0
- package/dist/lib/biome-host/host-bridge.d.ts.map +1 -1
- package/dist/lib/biome-host/host-bridge.js.map +1 -1
- package/dist/lib/biome-host/i18n-hooks.d.ts +4 -0
- package/dist/lib/biome-host/i18n-hooks.d.ts.map +1 -0
- package/dist/lib/biome-host/i18n-hooks.js +22 -0
- package/dist/lib/biome-host/i18n-hooks.js.map +1 -0
- package/dist/lib/biome-host/i18n-port.d.ts +15 -0
- package/dist/lib/biome-host/i18n-port.d.ts.map +1 -0
- package/dist/lib/biome-host/i18n-port.js +29 -0
- package/dist/lib/biome-host/i18n-port.js.map +1 -0
- package/dist/lib/biome-host/index.d.ts +2 -0
- package/dist/lib/biome-host/index.d.ts.map +1 -1
- package/dist/lib/biome-host/index.js +2 -0
- package/dist/lib/biome-host/index.js.map +1 -1
- package/dist/lib/biome-host/realtime-port.d.ts +1 -1
- package/dist/lib/biome-host/realtime-port.d.ts.map +1 -1
- package/dist/session-kit/approvals/approval-model.d.ts.map +1 -1
- package/dist/session-kit/approvals/approval-model.js.map +1 -1
- package/dist/ui/branding/app-branding-provider.d.ts +17 -0
- package/dist/ui/branding/app-branding-provider.d.ts.map +1 -0
- package/dist/ui/branding/app-branding-provider.js +81 -0
- package/dist/ui/branding/app-branding-provider.js.map +1 -0
- package/dist/ui/branding/sanitize.d.ts +4 -0
- package/dist/ui/branding/sanitize.d.ts.map +1 -0
- package/dist/ui/branding/sanitize.js +47 -0
- package/dist/ui/branding/sanitize.js.map +1 -0
- package/dist/ui/breakpoints.d.ts +2 -0
- package/dist/ui/breakpoints.d.ts.map +1 -0
- package/dist/ui/breakpoints.js +5 -0
- package/dist/ui/breakpoints.js.map +1 -0
- package/dist/ui/capture/document-preview.d.ts +12 -0
- package/dist/ui/capture/document-preview.d.ts.map +1 -0
- package/dist/ui/capture/document-preview.js +31 -0
- package/dist/ui/capture/document-preview.js.map +1 -0
- package/dist/ui/capture/field-spec-to-zod.d.ts +5 -0
- package/dist/ui/capture/field-spec-to-zod.d.ts.map +1 -0
- package/dist/ui/capture/field-spec-to-zod.js +71 -0
- package/dist/ui/capture/field-spec-to-zod.js.map +1 -0
- package/dist/ui/capture/field-spec.d.ts +49 -0
- package/dist/ui/capture/field-spec.d.ts.map +1 -0
- package/dist/ui/capture/field-spec.js +59 -0
- package/dist/ui/capture/field-spec.js.map +1 -0
- package/dist/ui/capture/index.d.ts +11 -0
- package/dist/ui/capture/index.d.ts.map +1 -0
- package/dist/ui/capture/index.js +31 -0
- package/dist/ui/capture/index.js.map +1 -0
- package/dist/ui/capture/schema-field-input.d.ts +20 -0
- package/dist/ui/capture/schema-field-input.d.ts.map +1 -0
- package/dist/ui/capture/schema-field-input.js +64 -0
- package/dist/ui/capture/schema-field-input.js.map +1 -0
- package/dist/ui/capture/schema-form.d.ts +18 -0
- package/dist/ui/capture/schema-form.d.ts.map +1 -0
- package/dist/ui/capture/schema-form.js +26 -0
- package/dist/ui/capture/schema-form.js.map +1 -0
- package/dist/ui/capture/use-prefill.d.ts +18 -0
- package/dist/ui/capture/use-prefill.d.ts.map +1 -0
- package/dist/ui/capture/use-prefill.js +94 -0
- package/dist/ui/capture/use-prefill.js.map +1 -0
- package/dist/ui/capture/use-schema-validation.d.ts +11 -0
- package/dist/ui/capture/use-schema-validation.d.ts.map +1 -0
- package/dist/ui/capture/use-schema-validation.js +69 -0
- package/dist/ui/capture/use-schema-validation.js.map +1 -0
- package/dist/ui/capture/use-wizard.d.ts +28 -0
- package/dist/ui/capture/use-wizard.d.ts.map +1 -0
- package/dist/ui/capture/use-wizard.js +63 -0
- package/dist/ui/capture/use-wizard.js.map +1 -0
- package/dist/ui/capture/value-format.d.ts +4 -0
- package/dist/ui/capture/value-format.d.ts.map +1 -0
- package/dist/ui/capture/value-format.js +23 -0
- package/dist/ui/capture/value-format.js.map +1 -0
- package/dist/ui/capture/wizard.d.ts +19 -0
- package/dist/ui/capture/wizard.d.ts.map +1 -0
- package/dist/ui/capture/wizard.js +34 -0
- package/dist/ui/capture/wizard.js.map +1 -0
- package/dist/ui/charts/bar-chart.d.ts +7 -0
- package/dist/ui/charts/bar-chart.d.ts.map +1 -0
- package/dist/ui/charts/bar-chart.js +47 -0
- package/dist/ui/charts/bar-chart.js.map +1 -0
- package/dist/ui/charts/chart-frame.d.ts +25 -0
- package/dist/ui/charts/chart-frame.d.ts.map +1 -0
- package/dist/ui/charts/chart-frame.js +23 -0
- package/dist/ui/charts/chart-frame.js.map +1 -0
- package/dist/ui/charts/chart-types.d.ts +19 -0
- package/dist/ui/charts/chart-types.d.ts.map +1 -0
- package/dist/ui/charts/chart-types.js +3 -0
- package/dist/ui/charts/chart-types.js.map +1 -0
- package/dist/ui/charts/chart-utils.d.ts +22 -0
- package/dist/ui/charts/chart-utils.d.ts.map +1 -0
- package/dist/ui/charts/chart-utils.js +97 -0
- package/dist/ui/charts/chart-utils.js.map +1 -0
- package/dist/ui/charts/donut-chart.d.ts +7 -0
- package/dist/ui/charts/donut-chart.d.ts.map +1 -0
- package/dist/ui/charts/donut-chart.js +55 -0
- package/dist/ui/charts/donut-chart.js.map +1 -0
- package/dist/ui/charts/index.d.ts +8 -0
- package/dist/ui/charts/index.d.ts.map +1 -0
- package/dist/ui/charts/index.js +21 -0
- package/dist/ui/charts/index.js.map +1 -0
- package/dist/ui/charts/line-chart.d.ts +7 -0
- package/dist/ui/charts/line-chart.d.ts.map +1 -0
- package/dist/ui/charts/line-chart.js +70 -0
- package/dist/ui/charts/line-chart.js.map +1 -0
- package/dist/ui/charts/sparkline.d.ts +13 -0
- package/dist/ui/charts/sparkline.d.ts.map +1 -0
- package/dist/ui/charts/sparkline.js +39 -0
- package/dist/ui/charts/sparkline.js.map +1 -0
- package/dist/ui/hooks/use-is-mobile.d.ts +2 -0
- package/dist/ui/hooks/use-is-mobile.d.ts.map +1 -0
- package/dist/ui/hooks/use-is-mobile.js +19 -0
- package/dist/ui/hooks/use-is-mobile.js.map +1 -0
- package/dist/ui/index.d.ts +7 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +15 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/pdf/pdf-preview.d.ts +8 -0
- package/dist/ui/pdf/pdf-preview.d.ts.map +1 -0
- package/dist/ui/pdf/pdf-preview.js +34 -0
- package/dist/ui/pdf/pdf-preview.js.map +1 -0
- package/dist/ui/primitives/badge.d.ts +1 -1
- package/dist/ui/primitives/button.d.ts +1 -1
- package/dist/ui/primitives/preview-device-frame.d.ts +9 -0
- package/dist/ui/primitives/preview-device-frame.d.ts.map +1 -0
- package/dist/ui/primitives/preview-device-frame.js +90 -0
- package/dist/ui/primitives/preview-device-frame.js.map +1 -0
- package/dist/ui/primitives/sheet.d.ts +1 -1
- package/package.json +10 -8
- package/src/index.ts +5 -0
- package/src/lib/biome-host/define-web-biome.test.js +93 -0
- package/src/lib/biome-host/define-web-biome.ts +71 -4
- package/src/lib/biome-host/frontend-biome.ts +45 -4
- package/src/lib/biome-host/host-bridge.ts +13 -0
- package/src/lib/biome-host/i18n-hooks.ts +50 -0
- package/src/lib/biome-host/i18n-port.test.ts +50 -0
- package/src/lib/biome-host/i18n-port.ts +96 -0
- package/src/lib/biome-host/index.ts +2 -0
- package/src/lib/biome-host/realtime-port.ts +9 -3
- package/src/lib/system-bus/enums.ts +1 -1
- package/src/lib/system-bus/index.ts +1 -1
- package/src/lib/system-bus/intents.ts +1 -1
- package/src/session-kit/approvals/ApprovalCard.tsx +1 -1
- package/src/session-kit/approvals/ApprovalsCenter.tsx +2 -2
- package/src/session-kit/approvals/CapabilityApprovalStickyBar.tsx +1 -1
- package/src/session-kit/approvals/Obligation.tsx +1 -1
- package/src/session-kit/approvals/ScopedBody.tsx +1 -1
- package/src/session-kit/approvals/approval-model.ts +7 -8
- package/src/session-kit/approvals/index.ts +1 -1
- package/src/session-kit/approvals/obligation-display.ts +1 -1
- package/src/session-kit/approvals/risk-accent.ts +1 -1
- package/src/session-kit/combobox/Combobox.tsx +1 -1
- package/src/session-kit/display/MessageTurn.tsx +1 -1
- package/src/session-kit/display/TokenMeter.tsx +1 -1
- package/src/session-kit/index.ts +3 -3
- package/src/session-kit/lib/enums.ts +1 -1
- package/src/session-kit/lib/status-dot.ts +1 -1
- package/src/session-kit/primitives/RiskPill.tsx +1 -1
- package/src/ui/branding/app-branding-provider.tsx +146 -0
- package/src/ui/branding/sanitize.ts +77 -0
- package/src/ui/breakpoints.ts +14 -0
- package/src/ui/capture/document-preview.tsx +101 -0
- package/src/ui/capture/field-spec-to-zod.test.ts +59 -0
- package/src/ui/capture/field-spec-to-zod.ts +93 -0
- package/src/ui/capture/field-spec.test.ts +49 -0
- package/src/ui/capture/field-spec.ts +160 -0
- package/src/ui/capture/index.ts +43 -0
- package/src/ui/capture/schema-field-input.tsx +186 -0
- package/src/ui/capture/schema-form.tsx +108 -0
- package/src/ui/capture/use-prefill.ts +150 -0
- package/src/ui/capture/use-schema-validation.ts +113 -0
- package/src/ui/capture/use-wizard.ts +134 -0
- package/src/ui/capture/value-format.ts +22 -0
- package/src/ui/capture/wizard.tsx +100 -0
- package/src/ui/charts/bar-chart.tsx +135 -0
- package/src/ui/charts/chart-frame.tsx +112 -0
- package/src/ui/charts/chart-types.ts +52 -0
- package/src/ui/charts/chart-utils.ts +158 -0
- package/src/ui/charts/charts.test.tsx +204 -0
- package/src/ui/charts/donut-chart.tsx +111 -0
- package/src/ui/charts/index.ts +27 -0
- package/src/ui/charts/line-chart.tsx +180 -0
- package/src/ui/charts/sparkline.tsx +101 -0
- package/src/ui/hooks/use-is-mobile.ts +28 -0
- package/src/ui/index.ts +25 -0
- package/src/ui/pdf/pdf-preview.tsx +122 -0
- package/src/ui/primitives/preview-device-frame.tsx +205 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/ui-kernel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Host-framework-agnostic UI kernel for the Xema OS. Defines the SystemBus orchestration contract (capability.invoke, cross-biome intents, command palette, xema:// deeplinks, window manager) AND the biome-host contract surface (FrontendBiome/FrontendBiomeFactory, HostBridge, the singleton biomeRegistry, session contributions) that every frontend biome composes against. No Vite, Next.js, or React-Router — React itself IS allowed as the shared component model (the contracts traffic in ReactNode/ComponentType and a React context). Concrete host adapters (router/auth/toast wiring) live in separate packages that consume this kernel. The SystemBus is pure orchestration: it never authorizes, the backend capability-router enforces all policy.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"xema",
|
|
@@ -12,16 +12,15 @@
|
|
|
12
12
|
"deeplink",
|
|
13
13
|
"intents"
|
|
14
14
|
],
|
|
15
|
+
"license": "Apache-2.0",
|
|
16
|
+
"author": "Neuralchowder Inc. <developer@xema.dev> (https://xema.dev)",
|
|
15
17
|
"homepage": "https://docs.xema.dev/biomes",
|
|
16
|
-
"bugs": {
|
|
17
|
-
"url": "https://github.com/xema-dev/xema-monorepo/issues"
|
|
18
|
-
},
|
|
19
18
|
"repository": {
|
|
20
19
|
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/xema-dev/xema-
|
|
20
|
+
"url": "git+https://github.com/xema-dev/xema-kernel-sdk.git",
|
|
22
21
|
"directory": "packages/ui/xema-ui-kernel"
|
|
23
22
|
},
|
|
24
|
-
"
|
|
23
|
+
"bugs": "https://github.com/xema-dev/xema-kernel-sdk/issues",
|
|
25
24
|
"publishConfig": {
|
|
26
25
|
"access": "public",
|
|
27
26
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -54,8 +53,10 @@
|
|
|
54
53
|
"class-variance-authority": "^0.7.1",
|
|
55
54
|
"clsx": "^2.1.1",
|
|
56
55
|
"lucide-react": "^0.462.0",
|
|
56
|
+
"react-pdf": "^9.1.1",
|
|
57
57
|
"react-resizable-panels": "^4.9.0",
|
|
58
|
-
"tailwind-merge": "^2.5.4"
|
|
58
|
+
"tailwind-merge": "^2.5.4",
|
|
59
|
+
"zod": "^4.4.3"
|
|
59
60
|
},
|
|
60
61
|
"peerDependencies": {
|
|
61
62
|
"@tanstack/react-query": "^5.0.0",
|
|
@@ -74,7 +75,7 @@
|
|
|
74
75
|
"prettier": "3.6.2",
|
|
75
76
|
"react": "^18.3.1",
|
|
76
77
|
"typescript": "5.9.3",
|
|
77
|
-
"@xemahq/kernel-contracts": "^0.
|
|
78
|
+
"@xemahq/kernel-contracts": "^0.39.0"
|
|
78
79
|
},
|
|
79
80
|
"exports": {
|
|
80
81
|
".": {
|
|
@@ -101,6 +102,7 @@
|
|
|
101
102
|
"scripts": {
|
|
102
103
|
"clean": "rm -rf dist",
|
|
103
104
|
"build": "tsc -p tsconfig.json",
|
|
105
|
+
"test": "echo '[ui-kernel] *.{test,spec} files run under host-web vitest (this package carries no vitest dep by design — see tsconfig exclude). Write tests with vitest; the frontend-bridge sync fails on a node:test file.'",
|
|
104
106
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
105
107
|
"lint": "tsc -p tsconfig.lint.json --noEmit",
|
|
106
108
|
"typecheck": "tsc -p tsconfig.lint.json --noEmit"
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export * from './lib/system-bus';
|
|
2
2
|
export * from './lib/biome-host';
|
|
3
3
|
export * from './lib/capabilities';
|
|
4
|
+
|
|
5
|
+
// Guided-capture UX kit (B5). Also available at the `@xemahq/ui-kernel/ui`
|
|
6
|
+
// subpath alongside the rest of the shared UI surface; re-exported here so the
|
|
7
|
+
// capture primitives are reachable from the package root too.
|
|
8
|
+
export * from './ui/capture';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Test suite for defineWebBiome's define-time scope/access validation and the
|
|
2
|
+
// chrome default. Runs on Node's native test runner against the COMPILED dist
|
|
3
|
+
// (CommonJS), so the package needs no jest/vitest/ts-jest toolchain — the
|
|
4
|
+
// `test` script builds first, then runs this file with `node --test`.
|
|
5
|
+
//
|
|
6
|
+
// Covered:
|
|
7
|
+
// 1. A `public` page with access `'member'` (or omitted) throws at define time.
|
|
8
|
+
// 2. A `public` + access `'public'` page is accepted; its route carries no
|
|
9
|
+
// nav item and mounts with access `'public'`.
|
|
10
|
+
// 3. A non-public page declaring access `'public'` throws at define time.
|
|
11
|
+
// 4. `chrome` defaults to `'host'` when a page omits it; `'none'` passes through.
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const test = require('node:test');
|
|
16
|
+
const assert = require('node:assert/strict');
|
|
17
|
+
|
|
18
|
+
const { defineWebBiome } = require('../../../dist/lib/biome-host/define-web-biome.js');
|
|
19
|
+
|
|
20
|
+
const load = () => Promise.resolve({ default: () => null });
|
|
21
|
+
const bridge = /** @type {any} */ ({});
|
|
22
|
+
|
|
23
|
+
test('rejects a public page whose access is not public (member default)', () => {
|
|
24
|
+
assert.throws(
|
|
25
|
+
() =>
|
|
26
|
+
defineWebBiome({
|
|
27
|
+
id: 'test-biome',
|
|
28
|
+
displayName: 'Test',
|
|
29
|
+
pages: [{ slug: 'ext/:id', label: 'Ext', scope: 'public', load }],
|
|
30
|
+
}),
|
|
31
|
+
/a 'public' scope page MUST declare access: 'public'/,
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('rejects a public page with an explicit non-public access tier', () => {
|
|
36
|
+
assert.throws(
|
|
37
|
+
() =>
|
|
38
|
+
defineWebBiome({
|
|
39
|
+
id: 'test-biome',
|
|
40
|
+
displayName: 'Test',
|
|
41
|
+
pages: [
|
|
42
|
+
{ slug: 'ext/:id', label: 'Ext', scope: 'public', access: 'org-admin', load },
|
|
43
|
+
],
|
|
44
|
+
}),
|
|
45
|
+
/a 'public' scope page MUST declare access: 'public'/,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('rejects a non-public page that declares access public', () => {
|
|
50
|
+
assert.throws(
|
|
51
|
+
() =>
|
|
52
|
+
defineWebBiome({
|
|
53
|
+
id: 'test-biome',
|
|
54
|
+
displayName: 'Test',
|
|
55
|
+
pages: [{ slug: 'system/x', label: 'X', access: 'public', load }],
|
|
56
|
+
}),
|
|
57
|
+
/access: 'public' is only valid on a 'public' scope page/,
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('accepts a public + access:public page and emits no nav item', () => {
|
|
62
|
+
const factory = defineWebBiome({
|
|
63
|
+
id: 'test-biome',
|
|
64
|
+
displayName: 'Test',
|
|
65
|
+
pages: [
|
|
66
|
+
{ slug: 'ext/:id', label: 'Ext', scope: 'public', access: 'public', load },
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
const biome = factory(bridge);
|
|
70
|
+
assert.equal(biome.routes.length, 1);
|
|
71
|
+
const route = biome.routes[0];
|
|
72
|
+
assert.equal(route.path, 'ext/:id');
|
|
73
|
+
assert.equal(route.access, 'public');
|
|
74
|
+
assert.equal(route.projectScoped, false);
|
|
75
|
+
// Public routes never enter the authenticated app rail.
|
|
76
|
+
assert.equal(biome.navItems.length, 0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('chrome defaults to host and none passes through', () => {
|
|
80
|
+
const factory = defineWebBiome({
|
|
81
|
+
id: 'test-biome',
|
|
82
|
+
displayName: 'Test',
|
|
83
|
+
pages: [
|
|
84
|
+
{ slug: 'system/a', label: 'A', load },
|
|
85
|
+
{ slug: 'system/b', label: 'B', chrome: 'none', load },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
const biome = factory(bridge);
|
|
89
|
+
const a = biome.routes.find((r) => r.path === 'system/a');
|
|
90
|
+
const b = biome.routes.find((r) => r.path === 'system/b');
|
|
91
|
+
assert.equal(a.chrome, 'host');
|
|
92
|
+
assert.equal(b.chrome, 'none');
|
|
93
|
+
});
|
|
@@ -44,8 +44,10 @@ import type {
|
|
|
44
44
|
FrontendBiome,
|
|
45
45
|
FrontendBiomeFactory,
|
|
46
46
|
NavItemContribution,
|
|
47
|
+
PageChrome,
|
|
47
48
|
RouteAccess,
|
|
48
49
|
RouteContribution,
|
|
50
|
+
RouteScope,
|
|
49
51
|
} from './frontend-biome';
|
|
50
52
|
import type { ComponentType } from 'react';
|
|
51
53
|
|
|
@@ -75,11 +77,24 @@ export interface WebBiomePage {
|
|
|
75
77
|
readonly category?: string;
|
|
76
78
|
/**
|
|
77
79
|
* Route scope. `'org'` (default) mounts at the org root; `'project'` mounts
|
|
78
|
-
* under
|
|
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.
|
|
79
91
|
*/
|
|
80
|
-
readonly scope?: 'org' | 'project';
|
|
81
|
-
/** Access tier required to render the route. Defaults to `'member'`. */
|
|
82
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;
|
|
83
98
|
/** Dynamic import of the page module (`() => import('./pages/X')`). */
|
|
84
99
|
readonly load: () => Promise<{ default: ComponentType<unknown> }>;
|
|
85
100
|
/**
|
|
@@ -108,6 +123,47 @@ export interface DefineWebBiomeOptions {
|
|
|
108
123
|
readonly outputRenderers?: FrontendBiome['outputRenderers'];
|
|
109
124
|
}
|
|
110
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
|
+
|
|
111
167
|
/**
|
|
112
168
|
* Build the `FrontendBiomeFactory` a web biome default-exports. See the
|
|
113
169
|
* module doc-comment for the rationale and an example.
|
|
@@ -115,6 +171,13 @@ export interface DefineWebBiomeOptions {
|
|
|
115
171
|
export function defineWebBiome(
|
|
116
172
|
options: DefineWebBiomeOptions,
|
|
117
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
|
+
|
|
118
181
|
return () => {
|
|
119
182
|
const navItems: NavItemContribution[] = [];
|
|
120
183
|
const routes: RouteContribution[] = [];
|
|
@@ -124,10 +187,14 @@ export function defineWebBiome(
|
|
|
124
187
|
path: page.slug,
|
|
125
188
|
projectScoped: page.scope === 'project',
|
|
126
189
|
element: lazyRoute(page.load),
|
|
190
|
+
chrome: page.chrome ?? DEFAULT_PAGE_CHROME,
|
|
127
191
|
...(page.access ? { access: page.access } : {}),
|
|
128
192
|
});
|
|
129
193
|
|
|
130
|
-
|
|
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') {
|
|
131
198
|
navItems.push({
|
|
132
199
|
id: page.id ?? page.slug,
|
|
133
200
|
label: page.label,
|
|
@@ -44,8 +44,42 @@ export interface NavItemContribution {
|
|
|
44
44
|
* declaratively — nav-hide is NOT a security boundary, a deeplink bypasses it.
|
|
45
45
|
* Precedence (least → most privileged): `member` < `org-admin` <
|
|
46
46
|
* `platform-admin`. Defaults to `member` when a route omits the field.
|
|
47
|
+
*
|
|
48
|
+
* `public` is NOT a privilege tier — it is the *no-auth* marker: a `public`
|
|
49
|
+
* route requires no Keycloak session at all and mounts OUTSIDE the host's
|
|
50
|
+
* authenticated route group. It is value-locked to `RouteScope` `'public'`
|
|
51
|
+
* (`defineWebBiome` rejects any other pairing at define time), so a route is
|
|
52
|
+
* public in BOTH senses or in neither. Closed set — a new member is a
|
|
53
|
+
* one-line change here and one `case` in the host resolver's exhaustive
|
|
54
|
+
* `hasRouteAccess` switch.
|
|
55
|
+
*/
|
|
56
|
+
export type RouteAccess = 'member' | 'org-admin' | 'platform-admin' | 'public';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Where a route mounts in the host's URL/auth topology. Closed set — adding a
|
|
60
|
+
* member is a one-line change here (and the single mapping in `defineWebBiome`
|
|
61
|
+
* plus one resolver partition in the host).
|
|
62
|
+
*
|
|
63
|
+
* - `org` — mounts at the org root, inside the authenticated group.
|
|
64
|
+
* - `project` — mounts under the project-scoped route prefix, inside the
|
|
65
|
+
* authenticated group.
|
|
66
|
+
* - `public` — mounts OUTSIDE the authenticated group; no Keycloak session,
|
|
67
|
+
* no login redirect, no org switcher. Locked to `RouteAccess` `'public'`.
|
|
47
68
|
*/
|
|
48
|
-
export type
|
|
69
|
+
export type RouteScope = 'org' | 'project' | 'public';
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether a page renders wrapped in the host application shell (app rail,
|
|
73
|
+
* topbar, org switcher) or standalone. Closed set — a new member is a one-line
|
|
74
|
+
* change here. Defaults to `'host'` when a page omits it.
|
|
75
|
+
*
|
|
76
|
+
* - `host` — the default; the host renders its chrome around the page.
|
|
77
|
+
* - `none` — the host renders the page with NO app rail, topbar, or org
|
|
78
|
+
* switcher (a full-bleed surface). `public` routes are structurally
|
|
79
|
+
* chromeless (their route group ships no shell); `none` also lets an
|
|
80
|
+
* authenticated route opt out of the shell.
|
|
81
|
+
*/
|
|
82
|
+
export type PageChrome = 'host' | 'none';
|
|
49
83
|
|
|
50
84
|
export interface RouteContribution {
|
|
51
85
|
/** React-router path. May include params. */
|
|
@@ -63,9 +97,16 @@ export interface RouteContribution {
|
|
|
63
97
|
readonly projectScoped: boolean;
|
|
64
98
|
/**
|
|
65
99
|
* Access tier required to render this route. The host resolver enforces it
|
|
66
|
-
* declaratively. Defaults to `'member'` when omitted.
|
|
100
|
+
* declaratively. Defaults to `'member'` when omitted. `'public'` marks a
|
|
101
|
+
* no-auth route the host mounts outside its authenticated group.
|
|
67
102
|
*/
|
|
68
103
|
readonly access?: RouteAccess;
|
|
104
|
+
/**
|
|
105
|
+
* Whether the host wraps this route in its application shell. Defaults to
|
|
106
|
+
* `'host'` when omitted; `'none'` renders the page with no app rail,
|
|
107
|
+
* topbar, or org switcher. See {@link PageChrome}.
|
|
108
|
+
*/
|
|
109
|
+
readonly chrome?: PageChrome;
|
|
69
110
|
}
|
|
70
111
|
|
|
71
112
|
/**
|
|
@@ -182,7 +223,7 @@ export interface OutputRendererContribution {
|
|
|
182
223
|
*
|
|
183
224
|
* This factory shape keeps biome code free of any direct dependency
|
|
184
225
|
* on host-shell primitives (router, auth client, toast library), which
|
|
185
|
-
* is what allows the same biome package to load into
|
|
186
|
-
* today and a
|
|
226
|
+
* is what allows the same biome package to load into one host runtime
|
|
227
|
+
* today and a different host runtime tomorrow.
|
|
187
228
|
*/
|
|
188
229
|
export type FrontendBiomeFactory = (bridge: HostBridge) => FrontendBiome;
|
|
@@ -5,6 +5,7 @@ import { createContext, useContext, type ReactNode } from 'react';
|
|
|
5
5
|
import type { CapabilityPort } from '../capabilities';
|
|
6
6
|
import type { SystemBus } from '../system-bus';
|
|
7
7
|
|
|
8
|
+
import type { I18nSource } from './i18n-port';
|
|
8
9
|
import type { RealtimeSource } from './realtime-port';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -235,6 +236,18 @@ export interface HostBridge {
|
|
|
235
236
|
* actionable error if a biome uses them before the host wires `bridge.realtime`.
|
|
236
237
|
*/
|
|
237
238
|
readonly realtime?: RealtimeSource;
|
|
239
|
+
/**
|
|
240
|
+
* Localization port. The HOST injects a concrete {@link I18nSource} backed by
|
|
241
|
+
* its locale provider (authoritative locale from org/app/user settings +
|
|
242
|
+
* catalog loading); biomes reach it through the kernel `useT` / `useLocale`
|
|
243
|
+
* hooks so biome code never imports an i18n library directly — same decoupling
|
|
244
|
+
* as `navigation` / `realtime`.
|
|
245
|
+
*
|
|
246
|
+
* OPTIONAL: a host without localization omits it; the kernel `useT`/`useLocale`
|
|
247
|
+
* hooks fail fast with an actionable error if a biome uses them before the host
|
|
248
|
+
* wires `bridge.i18n`.
|
|
249
|
+
*/
|
|
250
|
+
readonly i18n?: I18nSource;
|
|
238
251
|
/**
|
|
239
252
|
* Error-decoding surface — the canonical user-facing error decoder. Defaults
|
|
240
253
|
* (in the kernel default bridge wiring) to {@link getUserFacingErrorMessage};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel-side i18n hooks. Biomes call these INSTEAD of importing an i18n library
|
|
3
|
+
* or the host's locale provider directly, so biome code stays decoupled from the
|
|
4
|
+
* i18n implementation — the same decoupling `useHostBridge` provides for
|
|
5
|
+
* navigation / auth / realtime / capabilities.
|
|
6
|
+
*
|
|
7
|
+
* Each hook reads the host-injected {@link I18nSource} off `bridge.i18n` and
|
|
8
|
+
* delegates straight through. The host owns the authoritative locale + catalog
|
|
9
|
+
* loading; the kernel owns only the contract + this thin delegation.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useHostBridge } from './host-bridge';
|
|
13
|
+
import type { I18nSource, Translator } from './i18n-port';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the host-injected i18n source, failing fast with an actionable message
|
|
17
|
+
* if the host has not wired `bridge.i18n`. `bridge.i18n` is an optional port (a
|
|
18
|
+
* host without localization omits it), so the kernel hooks must not silently
|
|
19
|
+
* no-op — a biome that calls them expects translated copy.
|
|
20
|
+
*/
|
|
21
|
+
function requireI18n(i18n: I18nSource | undefined): I18nSource {
|
|
22
|
+
if (!i18n) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
'useT/useLocale require an i18n source, but the host has not wired ' +
|
|
25
|
+
'`bridge.i18n`. The host must provide an I18nSource (locale + catalogs) ' +
|
|
26
|
+
'on the HostBridge to use kernel i18n hooks.',
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return i18n;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A {@link Translator} for the active locale, scoped to an optional namespace.
|
|
34
|
+
* Call at render; the returned `t(key, params?)` interpolates `{name}` tokens.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* const t = useT('radar');
|
|
38
|
+
* return <h1>{t('title')}</h1>; // radar.title
|
|
39
|
+
* return <p>{t('greeting', { name })}</p>; // "Hello, {name}"
|
|
40
|
+
*/
|
|
41
|
+
export function useT(namespace?: string): Translator {
|
|
42
|
+
const bridge = useHostBridge();
|
|
43
|
+
return requireI18n(bridge.i18n).useTranslator(namespace);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The active BCP-47 locale tag (e.g. `"pt-PT"`) for date/number formatting. */
|
|
47
|
+
export function useLocale(): string {
|
|
48
|
+
const bridge = useHostBridge();
|
|
49
|
+
return requireI18n(bridge.i18n).useLocale();
|
|
50
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { makeTranslator } from './i18n-port';
|
|
4
|
+
|
|
5
|
+
const en = {
|
|
6
|
+
title: 'Dashboard',
|
|
7
|
+
greeting: 'Hello, {name}',
|
|
8
|
+
radar: { title: 'Radar', empty: 'No leads yet' },
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
describe('makeTranslator', () => {
|
|
12
|
+
it('resolves flat and nested dot-path keys', () => {
|
|
13
|
+
const t = makeTranslator(en);
|
|
14
|
+
expect(t('title')).toBe('Dashboard');
|
|
15
|
+
expect(t('radar.title')).toBe('Radar');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('interpolates {name} placeholders', () => {
|
|
19
|
+
const t = makeTranslator(en);
|
|
20
|
+
expect(t('greeting', { name: 'Ada' })).toBe('Hello, Ada');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('leaves an unknown placeholder verbatim (visible authoring bug)', () => {
|
|
24
|
+
const t = makeTranslator({ msg: 'Hi {who}' });
|
|
25
|
+
expect(t('msg', {})).toBe('Hi {who}');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('applies a namespace prefix', () => {
|
|
29
|
+
const t = makeTranslator(en, { namespace: 'radar' });
|
|
30
|
+
expect(t('title')).toBe('Radar');
|
|
31
|
+
expect(t('empty')).toBe('No leads yet');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns the raw key for a missing message (never blank, never throws)', () => {
|
|
35
|
+
const t = makeTranslator(en);
|
|
36
|
+
expect(t('does.not.exist')).toBe('does.not.exist');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('falls back message-by-message to the default-locale catalog', () => {
|
|
40
|
+
const pt = { title: 'Painel' }; // partial locale — no greeting
|
|
41
|
+
const t = makeTranslator(pt, { fallback: en });
|
|
42
|
+
expect(t('title')).toBe('Painel'); // primary wins
|
|
43
|
+
expect(t('greeting', { name: 'Ada' })).toBe('Hello, Ada'); // fallback fills the gap
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('coerces numeric params to strings', () => {
|
|
47
|
+
const t = makeTranslator({ count: '{n} items' });
|
|
48
|
+
expect(t('count', { n: 3 })).toBe('3 items');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kernel i18n contract + a framework-free translator core.
|
|
3
|
+
*
|
|
4
|
+
* Xema's locale is DATA-driven (per-org / per-app / per-user settings), not
|
|
5
|
+
* URL-driven, so the kernel deliberately does NOT depend on next-intl / react-
|
|
6
|
+
* intl. A biome localises its UI through the host-injected {@link I18nSource}
|
|
7
|
+
* (reached via the `useT` / `useLocale` kernel hooks) exactly the way it reaches
|
|
8
|
+
* realtime / navigation — the biome never imports an i18n library, and the host
|
|
9
|
+
* owns catalog loading + the authoritative locale source.
|
|
10
|
+
*
|
|
11
|
+
* The {@link makeTranslator} core here is pure and host-agnostic: a host (or a
|
|
12
|
+
* biome, or a test) builds a `Translator` from a nested-JSON catalog with no
|
|
13
|
+
* framework in the loop.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Interpolation values for a message's `{name}` placeholders. */
|
|
17
|
+
export type TranslateParams = Readonly<Record<string, string | number>>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve one message key to a localized string, interpolating `{name}`
|
|
21
|
+
* placeholders. Missing keys return the key itself (visible, never a throw or a
|
|
22
|
+
* blank) — the standard i18n contract, not a silent business-logic fallback.
|
|
23
|
+
*/
|
|
24
|
+
export type Translator = (key: string, params?: TranslateParams) => string;
|
|
25
|
+
|
|
26
|
+
/** A nested message catalog: dot-path keys map to strings (or sub-catalogs). */
|
|
27
|
+
export interface Catalog {
|
|
28
|
+
readonly [segment: string]: string | Catalog;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MakeTranslatorOptions {
|
|
32
|
+
/** Prefix every lookup with this namespace path (e.g. `"radar"`). */
|
|
33
|
+
readonly namespace?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Default-locale catalog consulted per-key when the primary catalog lacks a
|
|
36
|
+
* message — so a partially-translated locale falls back message-by-message
|
|
37
|
+
* instead of dropping to the raw key.
|
|
38
|
+
*/
|
|
39
|
+
readonly fallback?: Catalog;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function lookup(catalog: Catalog, path: readonly string[]): string | undefined {
|
|
43
|
+
let node: string | Catalog | undefined = catalog;
|
|
44
|
+
for (const segment of path) {
|
|
45
|
+
if (node === undefined || typeof node === 'string') {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
node = node[segment];
|
|
49
|
+
}
|
|
50
|
+
return typeof node === 'string' ? node : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function interpolate(message: string, params?: TranslateParams): string {
|
|
54
|
+
if (!params) {
|
|
55
|
+
return message;
|
|
56
|
+
}
|
|
57
|
+
// Replace `{name}` tokens; an unknown token is left verbatim (visible), never
|
|
58
|
+
// blanked — a missing param is an authoring bug that should be obvious.
|
|
59
|
+
return message.replaceAll(/\{(\w+)\}/g, (whole, name: string) =>
|
|
60
|
+
Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : whole,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build a {@link Translator} over a catalog. Pure — no React, no framework, no
|
|
66
|
+
* global state. Resolution order per key: primary catalog → fallback catalog →
|
|
67
|
+
* the key itself.
|
|
68
|
+
*/
|
|
69
|
+
export function makeTranslator(
|
|
70
|
+
catalog: Catalog,
|
|
71
|
+
options?: MakeTranslatorOptions,
|
|
72
|
+
): Translator {
|
|
73
|
+
const prefix = options?.namespace ? options.namespace.split('.') : [];
|
|
74
|
+
const fallback = options?.fallback;
|
|
75
|
+
return (key, params) => {
|
|
76
|
+
const path = [...prefix, ...key.split('.')];
|
|
77
|
+
const message =
|
|
78
|
+
lookup(catalog, path) ?? (fallback ? lookup(fallback, path) : undefined) ?? key;
|
|
79
|
+
return interpolate(message, params);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Host-implemented i18n source. The host wires these against its locale provider
|
|
85
|
+
* (authoritative locale from org/app/user settings + catalog loading); biomes
|
|
86
|
+
* reach them through the kernel {@link useT} / {@link useLocale} hooks.
|
|
87
|
+
*
|
|
88
|
+
* Every method is a React hook (reads locale/catalog context), so it MUST be
|
|
89
|
+
* called from a component's render.
|
|
90
|
+
*/
|
|
91
|
+
export interface I18nSource {
|
|
92
|
+
/** A {@link Translator} for the active locale, optionally namespaced. */
|
|
93
|
+
useTranslator(namespace?: string): Translator;
|
|
94
|
+
/** The active BCP-47 locale tag (e.g. `"pt-PT"`). */
|
|
95
|
+
useLocale(): string;
|
|
96
|
+
}
|
|
@@ -28,6 +28,8 @@ export * from './biome-mode';
|
|
|
28
28
|
export * from './agent-validation';
|
|
29
29
|
export * from './realtime-port';
|
|
30
30
|
export * from './realtime-hooks';
|
|
31
|
+
export * from './i18n-port';
|
|
32
|
+
export * from './i18n-hooks';
|
|
31
33
|
export * from './errors';
|
|
32
34
|
export * from './response-envelope';
|
|
33
35
|
export * from './use-mutation-with-error-toast';
|
|
@@ -73,11 +73,17 @@ export interface RealtimeConnectionState {
|
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* A server-side scope a connection can subscribe to so it only receives the
|
|
76
|
-
* events for one project or
|
|
77
|
-
* host's transport reference-counts identical scopes across mounts.
|
|
76
|
+
* events for one project, session, or app, rather than the full org firehose.
|
|
77
|
+
* The host's transport reference-counts identical scopes across mounts.
|
|
78
|
+
*
|
|
79
|
+
* `app` is the app-surface scope: an external/app-facing biome subscribes to
|
|
80
|
+
* its app's event stream (delivery is authz-gated server-side by the subject's
|
|
81
|
+
* app membership). It is value-identical to the server `ScopeKind.app` and the
|
|
82
|
+
* host transport's `EventScopeOptions` — the kernel union is the last hop to
|
|
83
|
+
* carry it, so a decoupled/remote biome can type an app subscription.
|
|
78
84
|
*/
|
|
79
85
|
export interface RealtimeScope {
|
|
80
|
-
readonly kind: 'project' | 'session';
|
|
86
|
+
readonly kind: 'project' | 'session' | 'app';
|
|
81
87
|
readonly id: string;
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SystemBus — the framework-agnostic OS-orchestration contract plane
|
|
3
|
-
*
|
|
3
|
+
* Pure types + a small framework-agnostic runtime
|
|
4
4
|
* ({@link IntentRegistry} + {@link parseDeeplink}). No React, no Vite, no
|
|
5
5
|
* Next.js, no React-Router, no transport client.
|
|
6
6
|
*/
|
|
@@ -10,7 +10,7 @@ import { ActionRefKind } from './enums';
|
|
|
10
10
|
* `intents.resolve(kind)` returns every action any biome registered for
|
|
11
11
|
* that kind. This is how one biome hands an object to another WITHOUT
|
|
12
12
|
* importing it — the macOS "Open With" / Share-Sheet analogy from the
|
|
13
|
-
*
|
|
13
|
+
* platform design.
|
|
14
14
|
*
|
|
15
15
|
* The registry is pure orchestration: it indexes contributions and
|
|
16
16
|
* resolves them. It performs NO authorization — whether the actor may
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The GENERIC approval frame
|
|
2
|
+
* The GENERIC approval frame — identical platform-wide.
|
|
3
3
|
*
|
|
4
4
|
* Header: "Approval required" + RiskPill (reused from the kit) + mono
|
|
5
5
|
* capability ref + serif title + summary. Then the biome-supplied
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Approvals Center body
|
|
2
|
+
* Approvals Center body — the cross-portal batch-review list.
|
|
3
3
|
*
|
|
4
4
|
* This is the GENERIC, prop-driven body (toolbar + rows + empty state). It
|
|
5
5
|
* does NOT own the drawer chrome: the host wraps it in its own `Sheet`
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* portal + biome + quorum and an "Open session" affordance. "Select
|
|
12
12
|
* low-risk" selects every low/medium item; bulk "Approve N" resolves the
|
|
13
13
|
* selection. The row model (`ApprovalCenterItem`) already carries
|
|
14
|
-
* portalId/name/accent so
|
|
14
|
+
* portalId/name/accent so host portals can group by portal without a reshape.
|
|
15
15
|
*/
|
|
16
16
|
import { CapabilityRiskTier } from '@xemahq/kernel-contracts/capability';
|
|
17
17
|
import { useCallback, useState, type ReactElement } from 'react';
|