@xemahq/ui-kernel 0.5.0 → 0.6.1

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 (143) hide show
  1. package/dist/lib/biome-host/define-web-biome.d.ts +3 -3
  2. package/dist/lib/biome-host/define-web-biome.d.ts.map +1 -1
  3. package/dist/lib/biome-host/define-web-biome.js +23 -2
  4. package/dist/lib/biome-host/define-web-biome.js.map +1 -1
  5. package/dist/lib/biome-host/frontend-biome.d.ts +4 -3
  6. package/dist/lib/biome-host/frontend-biome.d.ts.map +1 -1
  7. package/dist/lib/biome-host/host-bridge.d.ts +2 -0
  8. package/dist/lib/biome-host/host-bridge.d.ts.map +1 -1
  9. package/dist/lib/biome-host/host-bridge.js.map +1 -1
  10. package/dist/lib/biome-host/i18n-hooks.d.ts +4 -0
  11. package/dist/lib/biome-host/i18n-hooks.d.ts.map +1 -0
  12. package/dist/lib/biome-host/i18n-hooks.js +22 -0
  13. package/dist/lib/biome-host/i18n-hooks.js.map +1 -0
  14. package/dist/lib/biome-host/i18n-port.d.ts +15 -0
  15. package/dist/lib/biome-host/i18n-port.d.ts.map +1 -0
  16. package/dist/lib/biome-host/i18n-port.js +29 -0
  17. package/dist/lib/biome-host/i18n-port.js.map +1 -0
  18. package/dist/lib/biome-host/index.d.ts +2 -1
  19. package/dist/lib/biome-host/index.d.ts.map +1 -1
  20. package/dist/lib/biome-host/index.js +2 -1
  21. package/dist/lib/biome-host/index.js.map +1 -1
  22. package/dist/lib/biome-host/realtime-port.d.ts +1 -1
  23. package/dist/lib/biome-host/realtime-port.d.ts.map +1 -1
  24. package/dist/session-kit/approvals/approval-model.d.ts.map +1 -1
  25. package/dist/session-kit/approvals/approval-model.js.map +1 -1
  26. package/dist/ui/branding/app-branding-provider.d.ts +17 -0
  27. package/dist/ui/branding/app-branding-provider.d.ts.map +1 -0
  28. package/dist/ui/branding/app-branding-provider.js +81 -0
  29. package/dist/ui/branding/app-branding-provider.js.map +1 -0
  30. package/dist/ui/branding/sanitize.d.ts +4 -0
  31. package/dist/ui/branding/sanitize.d.ts.map +1 -0
  32. package/dist/ui/branding/sanitize.js +47 -0
  33. package/dist/ui/branding/sanitize.js.map +1 -0
  34. package/dist/ui/breakpoints.d.ts +2 -0
  35. package/dist/ui/breakpoints.d.ts.map +1 -0
  36. package/dist/ui/breakpoints.js +5 -0
  37. package/dist/ui/breakpoints.js.map +1 -0
  38. package/dist/ui/charts/bar-chart.d.ts +7 -0
  39. package/dist/ui/charts/bar-chart.d.ts.map +1 -0
  40. package/dist/ui/charts/bar-chart.js +47 -0
  41. package/dist/ui/charts/bar-chart.js.map +1 -0
  42. package/dist/ui/charts/chart-frame.d.ts +25 -0
  43. package/dist/ui/charts/chart-frame.d.ts.map +1 -0
  44. package/dist/ui/charts/chart-frame.js +23 -0
  45. package/dist/ui/charts/chart-frame.js.map +1 -0
  46. package/dist/ui/charts/chart-types.d.ts +19 -0
  47. package/dist/ui/charts/chart-types.d.ts.map +1 -0
  48. package/dist/ui/charts/chart-types.js +3 -0
  49. package/dist/ui/charts/chart-types.js.map +1 -0
  50. package/dist/ui/charts/chart-utils.d.ts +22 -0
  51. package/dist/ui/charts/chart-utils.d.ts.map +1 -0
  52. package/dist/ui/charts/chart-utils.js +97 -0
  53. package/dist/ui/charts/chart-utils.js.map +1 -0
  54. package/dist/ui/charts/donut-chart.d.ts +7 -0
  55. package/dist/ui/charts/donut-chart.d.ts.map +1 -0
  56. package/dist/ui/charts/donut-chart.js +55 -0
  57. package/dist/ui/charts/donut-chart.js.map +1 -0
  58. package/dist/ui/charts/index.d.ts +8 -0
  59. package/dist/ui/charts/index.d.ts.map +1 -0
  60. package/dist/ui/charts/index.js +21 -0
  61. package/dist/ui/charts/index.js.map +1 -0
  62. package/dist/ui/charts/line-chart.d.ts +7 -0
  63. package/dist/ui/charts/line-chart.d.ts.map +1 -0
  64. package/dist/ui/charts/line-chart.js +70 -0
  65. package/dist/ui/charts/line-chart.js.map +1 -0
  66. package/dist/ui/charts/sparkline.d.ts +13 -0
  67. package/dist/ui/charts/sparkline.d.ts.map +1 -0
  68. package/dist/ui/charts/sparkline.js +39 -0
  69. package/dist/ui/charts/sparkline.js.map +1 -0
  70. package/dist/ui/hooks/use-is-mobile.d.ts +2 -0
  71. package/dist/ui/hooks/use-is-mobile.d.ts.map +1 -0
  72. package/dist/ui/hooks/use-is-mobile.js +19 -0
  73. package/dist/ui/hooks/use-is-mobile.js.map +1 -0
  74. package/dist/ui/index.d.ts +6 -0
  75. package/dist/ui/index.d.ts.map +1 -1
  76. package/dist/ui/index.js +14 -1
  77. package/dist/ui/index.js.map +1 -1
  78. package/dist/ui/pdf/pdf-preview.d.ts +8 -0
  79. package/dist/ui/pdf/pdf-preview.d.ts.map +1 -0
  80. package/dist/ui/pdf/pdf-preview.js +34 -0
  81. package/dist/ui/pdf/pdf-preview.js.map +1 -0
  82. package/dist/ui/primitives/preview-device-frame.d.ts +9 -0
  83. package/dist/ui/primitives/preview-device-frame.d.ts.map +1 -0
  84. package/dist/ui/primitives/preview-device-frame.js +90 -0
  85. package/dist/ui/primitives/preview-device-frame.js.map +1 -0
  86. package/package.json +8 -7
  87. package/src/lib/biome-host/define-web-biome.test.js +93 -0
  88. package/src/lib/biome-host/define-web-biome.ts +76 -15
  89. package/src/lib/biome-host/frontend-biome.ts +47 -18
  90. package/src/lib/biome-host/host-bridge.ts +13 -0
  91. package/src/lib/biome-host/i18n-hooks.ts +50 -0
  92. package/src/lib/biome-host/i18n-port.test.ts +50 -0
  93. package/src/lib/biome-host/i18n-port.ts +96 -0
  94. package/src/lib/biome-host/index.ts +2 -1
  95. package/src/lib/biome-host/realtime-port.ts +9 -3
  96. package/src/lib/system-bus/enums.ts +1 -1
  97. package/src/lib/system-bus/index.ts +1 -1
  98. package/src/lib/system-bus/intents.ts +1 -1
  99. package/src/session-kit/approvals/ApprovalCard.tsx +1 -1
  100. package/src/session-kit/approvals/ApprovalsCenter.tsx +2 -2
  101. package/src/session-kit/approvals/CapabilityApprovalStickyBar.tsx +1 -1
  102. package/src/session-kit/approvals/Obligation.tsx +1 -1
  103. package/src/session-kit/approvals/ScopedBody.tsx +1 -1
  104. package/src/session-kit/approvals/approval-model.ts +7 -8
  105. package/src/session-kit/approvals/index.ts +1 -1
  106. package/src/session-kit/approvals/obligation-display.ts +1 -1
  107. package/src/session-kit/approvals/risk-accent.ts +1 -1
  108. package/src/session-kit/combobox/Combobox.tsx +1 -1
  109. package/src/session-kit/display/MessageTurn.tsx +1 -1
  110. package/src/session-kit/display/TokenMeter.tsx +1 -1
  111. package/src/session-kit/index.ts +3 -3
  112. package/src/session-kit/lib/enums.ts +1 -1
  113. package/src/session-kit/lib/status-dot.ts +1 -1
  114. package/src/session-kit/primitives/RiskPill.tsx +1 -1
  115. package/src/ui/branding/app-branding-provider.tsx +146 -0
  116. package/src/ui/branding/sanitize.ts +77 -0
  117. package/src/ui/breakpoints.ts +14 -0
  118. package/src/ui/charts/bar-chart.tsx +135 -0
  119. package/src/ui/charts/chart-frame.tsx +112 -0
  120. package/src/ui/charts/chart-types.ts +52 -0
  121. package/src/ui/charts/chart-utils.ts +158 -0
  122. package/src/ui/charts/charts.test.tsx +204 -0
  123. package/src/ui/charts/donut-chart.tsx +111 -0
  124. package/src/ui/charts/index.ts +27 -0
  125. package/src/ui/charts/line-chart.tsx +180 -0
  126. package/src/ui/charts/sparkline.tsx +101 -0
  127. package/src/ui/hooks/use-is-mobile.ts +28 -0
  128. package/src/ui/index.ts +22 -0
  129. package/src/ui/pdf/pdf-preview.tsx +122 -0
  130. package/src/ui/primitives/preview-device-frame.tsx +205 -0
  131. package/dist/lib/biome-host/composition-validation.d.ts +0 -22
  132. package/dist/lib/biome-host/composition-validation.d.ts.map +0 -1
  133. package/dist/lib/biome-host/composition-validation.js +0 -127
  134. package/dist/lib/biome-host/composition-validation.js.map +0 -1
  135. package/dist/lib/biome-host/nav.d.ts +0 -17
  136. package/dist/lib/biome-host/nav.d.ts.map +0 -1
  137. package/dist/lib/biome-host/nav.js +0 -52
  138. package/dist/lib/biome-host/nav.js.map +0 -1
  139. package/dist/registry/lib/composition-validation-host.d.ts +0 -3
  140. package/dist/registry/lib/composition-validation-host.d.ts.map +0 -1
  141. package/dist/registry/lib/composition-validation-host.js +0 -10
  142. package/dist/registry/lib/composition-validation-host.js.map +0 -1
  143. package/src/lib/biome-host/nav.ts +0 -83
@@ -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
+ }
@@ -24,11 +24,12 @@ export * from './biome-registry';
24
24
  export * from './session-contributions';
25
25
  export * from './session-profiles';
26
26
  export * from './host-sources';
27
- export * from './nav';
28
27
  export * from './biome-mode';
29
28
  export * from './agent-validation';
30
29
  export * from './realtime-port';
31
30
  export * from './realtime-hooks';
31
+ export * from './i18n-port';
32
+ export * from './i18n-hooks';
32
33
  export * from './errors';
33
34
  export * from './response-envelope';
34
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 session, rather than the full org firehose. The
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,5 +1,5 @@
1
1
  /**
2
- * Closed sets for the `SystemBus` contract (Phase 6b).
2
+ * Closed sets for the `SystemBus` contract.
3
3
  *
4
4
  * Every closed domain the SystemBus exposes is a real TypeScript enum so
5
5
  * the contract is stable across hosts and serialises predictably. The
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * SystemBus — the framework-agnostic OS-orchestration contract plane
3
- * (Phase 6b). Pure types + a small framework-agnostic runtime
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
- * plan-of-record (§12.3).
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 (HANDOFF §6) — identical platform-wide.
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 (HANDOFF §6) — the cross-portal batch-review list.
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 Pillar 4 can group by portal without a reshape.
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';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Sticky CAPABILITY-approval bar (HANDOFF §6) — pinned above the composer
2
+ * Sticky CAPABILITY-approval bar — pinned above the composer
3
3
  * so a pending capability approval is always actionable without scrolling
4
4
  * back to the inline card.
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Obligation chip + quorum dots (HANDOFF §6).
2
+ * Obligation chip + quorum dots.
3
3
  *
4
4
  * `Obligation` renders one obligation as a chip: host-supplied icon (via
5
5
  * `renderIcon` from the stable `ObligationIcon` key) + label + optional
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Scoped approval bodies (HANDOFF §6) — the biome-supplied typed detail
2
+ * Scoped approval bodies — the biome-supplied typed detail
3
3
  * that renders UNDER the generic frame.
4
4
  *
5
5
  * `ScopedBody` is an EXHAUSTIVE switch over `ApprovalScopeKind`. A scope
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Approval primitive — generic frame model + scoped-body payloads
3
- * (Pillar 3 of the Xema Portals design handoff; HANDOFF §6).
2
+ * Approval primitive — generic frame model + scoped-body payloads.
4
3
  *
5
4
  * The approval is the runtime face of a kernel policy decision: a
6
5
  * capability call returns `allow | deny | needs_approval`
@@ -9,7 +8,7 @@
9
8
  * (risk pill + capability ref + obligations + quorum + actions); each
10
9
  * biome supplies a typed `scope` body for rich detail — the same
11
10
  * generic-frame + biome-body pattern as the in-chat widget registry
12
- * (HANDOFF §3.2) and the preview-tab kinds (HANDOFF §5).
11
+ * and the preview-tab kinds.
13
12
  *
14
13
  * This module REUSES the kernel contracts rather than redefining them:
15
14
  * • `CapabilityRiskTier` (low/medium/high/critical) — risk pill + accent
@@ -24,7 +23,7 @@ import type { CapabilityRiskTier } from '@xemahq/kernel-contracts/capability';
24
23
  import type { PolicyObligationKind } from '@xemahq/kernel-contracts/policy';
25
24
 
26
25
  /**
27
- * Closed set of biome-supplied scoped-body kinds (HANDOFF §6). This is the
26
+ * Closed set of biome-supplied scoped-body kinds. This is the
28
27
  * approval analogue of `ChatWidgetKind` — a typed, versioned catalogue of
29
28
  * rich bodies that render UNDER the generic frame. The renderer
30
29
  * (`ScopedBody`) switches on `kind`; an unknown kind fails SAFE to a
@@ -165,7 +164,7 @@ export interface ApprovalRequest {
165
164
  * "allow once" vs "approve & remember" distinction is a UI affordance on
166
165
  * top of the same `Approved` decision (the remember flag is carried
167
166
  * separately when the backend supports it); both resolve the suspended
168
- * call. The denied/override-rationale path is DEFERRED (HANDOFF §6).
167
+ * call. The denied/override-rationale path is not yet implemented.
169
168
  */
170
169
  export enum ApprovalDecision {
171
170
  Approved = 'approved',
@@ -175,14 +174,14 @@ export enum ApprovalDecision {
175
174
  /**
176
175
  * A pending approval as it appears in the cross-portal Approvals Center —
177
176
  * the request plus the portal/session context it was raised in. The
178
- * portal fields are first-class so Pillar 4 (Portals) can batch and group
177
+ * portal fields are first-class so the Portals surface can batch and group
179
178
  * approvals across portals without reshaping this model.
180
179
  */
181
180
  export interface ApprovalCenterItem {
182
181
  /** Mirrors `request.id`; kept distinct so a Center may track its own row id. */
183
182
  readonly id: string;
184
183
  readonly request: ApprovalRequest;
185
- /** Portal the approval was raised in (Pillar 4 cross-portal context). */
184
+ /** Portal the approval was raised in (cross-portal context). */
186
185
  readonly portalId?: string;
187
186
  readonly portalName?: string;
188
187
  /** Portal accent token (`var(--p-*)`) for the row glyph; optional. */
@@ -194,7 +193,7 @@ export interface ApprovalCenterItem {
194
193
 
195
194
  /** True when the tier always demands an explicit Approve (no allow-once). */
196
195
  export function isExplicitApprovalTier(tier: CapabilityRiskTier): boolean {
197
- // `critical` and `high` are explicit-only (HANDOFF §6: critical always
196
+ // `critical` and `high` are explicit-only (critical always
198
197
  // explicit; high uses the warning accent and skips allow-once-remember).
199
198
  return tier === 'critical' || tier === 'high';
200
199
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `@xemahq/ui-kernel/session-kit` approvals — the generic approval
3
- * primitive (Pillar 3 of the Xema Portals design handoff; HANDOFF §6).
3
+ * primitive.
4
4
  *
5
5
  * Generic frame + scoped bodies + sticky bar + Approvals Center. Models on
6
6
  * the kernel contracts (`CapabilityRiskTier`, `PolicyObligationKind`) and
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Obligation → display metadata map (HANDOFF §6).
2
+ * Obligation → display metadata map.
3
3
  *
4
4
  * Covers ALL NINE obligation kinds in the kernel closed taxonomy —
5
5
  * `audit`, `redact-secrets`, `require-runner-kind`,
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Risk-tier → colour expressions for the approval frame's accent rail and
3
- * the sticky bar (HANDOFF §6). RiskPill owns the PILL styling; this map is
3
+ * the sticky bar. RiskPill owns the PILL styling; this map is
4
4
  * the matching surface accent (foreground / soft background / border) the
5
5
  * frame and bar paint with. Both read the SAME kernel `CapabilityRiskTier`
6
6
  * enum so the pill and the rail can never disagree on a tier's colour.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Generic searchable picker (HANDOFF §7). Use it wherever a closed set
2
+ * Generic searchable picker. Use it wherever a closed set
3
3
  * is selected (agents, models, skills/tools, …).
4
4
  *
5
5
  * - Shows a search field when `items.length > searchThreshold` (default
@@ -2,7 +2,7 @@
2
2
  * A single message turn. User turns render a right-aligned soft bubble;
3
3
  * agent turns render a thin gutter + role caption + flowing body.
4
4
  *
5
- * The body is rendered by `renderMarkdown` when supplied (Pillar 2 wires
5
+ * The body is rendered by `renderMarkdown` when supplied (host shells wire
6
6
  * the host's full `MarkdownRenderer` here) and falls back to the kit's
7
7
  * built-in `MD` otherwise — so the kit is self-contained but never
8
8
  * forces its tiny renderer on a host that has a richer one.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Token/cost meter — input & output token counts plus a running dollar
3
- * cost. Used both inside the ContextHeader and (in Pillar 2) absolute
3
+ * cost. Used both inside the ContextHeader and (in host shells) absolute
4
4
  * top-right of the composer.
5
5
  */
6
6
  import { SessionSkin } from '../lib/enums';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `@xemahq/ui-kernel/session-kit` — the thin, prop-driven generic
3
3
  * component substrate for agent-session surfaces and the Portals
4
- * chrome (Pillar 1 of the Xema Portals design handoff).
4
+ * chrome.
5
5
  *
6
6
  * Everything here is presentational and icon-agnostic: components take
7
7
  * data + host-supplied icon nodes and emit inline-styled markup against
@@ -9,7 +9,7 @@
9
9
  * subscriptions, no framework imports — so the same kit renders inside
10
10
  * the host shell and inside a biome's own surfaces.
11
11
  *
12
- * Pillar 2 consumes these via `import { … } from '@xemahq/ui-kernel/session-kit'`.
12
+ * Host shells consume these via `import { … } from '@xemahq/ui-kernel/session-kit'`.
13
13
  */
14
14
 
15
15
  // Closed-set enums + the authoritative status-dot map.
@@ -39,6 +39,6 @@ export * from './display/TodoChecklist';
39
39
  export * from './combobox/Combobox';
40
40
  export * from './combobox/use-click-outside';
41
41
 
42
- // Approvals primitive (Pillar 3 — generic frame + scoped bodies + sticky
42
+ // Approvals primitive — generic frame + scoped bodies + sticky
43
43
  // bar + cross-portal Approvals Center).
44
44
  export * from './approvals';
@@ -17,7 +17,7 @@ export enum ToolStatus {
17
17
  }
18
18
 
19
19
  /**
20
- * The four authoritative transcript status-dot states (HANDOFF §3.1).
20
+ * The four authoritative transcript status-dot states.
21
21
  * `Narration` is the hollow dot used by a `say` event — it is not a
22
22
  * tool, so it sits outside `ToolStatus`.
23
23
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Authoritative transcript status-dot colour map (HANDOFF §3.1).
2
+ * Authoritative transcript status-dot colour map.
3
3
  *
4
4
  * pending / running → amber, PULSING (--warning)
5
5
  * done / success → green (--success)
@@ -4,7 +4,7 @@
4
4
  * string union. Critical renders an optional caller-supplied alert icon
5
5
  * (the kit carries no icon dependency, matching the rest of ui-kernel).
6
6
  *
7
- * Risk → colour (HANDOFF §6):
7
+ * Risk → colour:
8
8
  * low → ink / muted
9
9
  * medium → --info
10
10
  * high → --warning
@@ -0,0 +1,146 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // AppBrandingProvider — the runtime branding applier
3
+ //
4
+ // Takes a kernel `BrandingConfig` (plus an optional per-sub-tenant override)
5
+ // and APPLIES it at runtime:
6
+ //
7
+ // • writes `primaryColor` → `--app-primary-color` and every `cssTokens`
8
+ // entry → `--<key>` as CSS custom properties on its OWN wrapper element,
9
+ // so they cascade ONLY to the app subtree — never onto
10
+ // `document.documentElement`, so an app's branding cannot leak into the
11
+ // host chrome;
12
+ // • exposes `displayName` / `logoUrl` / `primaryColor` to children through
13
+ // context (`useAppBranding`) for a branded header to render.
14
+ //
15
+ // Every value crosses `sanitize.ts` first: an unsafe CSS value or a
16
+ // non-`http(s)` logo URL is dropped, never written raw.
17
+ //
18
+ // This is the reusable half of Wave 8.4 — a biome imports it from
19
+ // `@xemahq/ui-kernel/ui`, like `useIsMobile` / `Sheet`.
20
+ // ═══════════════════════════════════════════════════════════════════════════
21
+ import * as React from 'react';
22
+
23
+ import type { BrandingConfig } from '@xemahq/kernel-contracts/app-runtime';
24
+
25
+ import { cn } from '../cn';
26
+
27
+ import {
28
+ sanitizeCssCustomPropertyName,
29
+ sanitizeCssValue,
30
+ sanitizeLogoUrl,
31
+ } from './sanitize';
32
+
33
+ /** CSS custom property carrying the app's resolved primary brand color. */
34
+ export const APP_PRIMARY_COLOR_VAR = '--app-primary-color';
35
+
36
+ /** Values a branded header needs — sanitized, safe to render. */
37
+ export interface AppBrandingContextValue {
38
+ readonly displayName: string;
39
+ readonly logoUrl?: string;
40
+ readonly primaryColor?: string;
41
+ }
42
+
43
+ const AppBrandingContext = React.createContext<AppBrandingContextValue | null>(
44
+ null,
45
+ );
46
+
47
+ /**
48
+ * Resolved branding for the nearest enclosing {@link AppBrandingProvider}.
49
+ * Throws when used outside one — a header that assumes branding must not
50
+ * silently render an unbranded surface.
51
+ */
52
+ export function useAppBranding(): AppBrandingContextValue {
53
+ const ctx = React.useContext(AppBrandingContext);
54
+ if (ctx === null) {
55
+ throw new Error(
56
+ 'useAppBranding must be used within an <AppBrandingProvider>.',
57
+ );
58
+ }
59
+ return ctx;
60
+ }
61
+
62
+ export interface AppBrandingProviderProps {
63
+ /** The app's base branding (from the App definition / release). */
64
+ readonly branding: BrandingConfig;
65
+ /**
66
+ * Per-sub-tenant override, resolved from `AppRoleBinding.tenantId`. Each
67
+ * present field shadows the base; absent fields fall through to `branding`.
68
+ * Optional — apps with no sub-tenant branding pass nothing.
69
+ */
70
+ readonly tenantBranding?: Partial<BrandingConfig>;
71
+ /** Extra classes for the wrapper element. */
72
+ readonly className?: string;
73
+ readonly children: React.ReactNode;
74
+ }
75
+
76
+ /**
77
+ * Merge base branding with an optional per-sub-tenant override (override wins
78
+ * per field), then sanitize every applied value.
79
+ */
80
+ function resolveBranding(
81
+ branding: BrandingConfig,
82
+ tenantBranding: Partial<BrandingConfig> | undefined,
83
+ ): {
84
+ readonly context: AppBrandingContextValue;
85
+ readonly cssVars: Record<string, string>;
86
+ } {
87
+ const displayName = tenantBranding?.displayName ?? branding.displayName;
88
+ const rawPrimary = tenantBranding?.primaryColor ?? branding.primaryColor;
89
+ const rawLogo = tenantBranding?.logoUrl ?? branding.logoUrl;
90
+ const cssTokens = { ...branding.cssTokens, ...tenantBranding?.cssTokens };
91
+
92
+ const primaryColor =
93
+ rawPrimary !== undefined
94
+ ? (sanitizeCssValue(rawPrimary) ?? undefined)
95
+ : undefined;
96
+ const logoUrl = sanitizeLogoUrl(rawLogo);
97
+
98
+ const cssVars: Record<string, string> = {};
99
+ if (primaryColor !== undefined) {
100
+ cssVars[APP_PRIMARY_COLOR_VAR] = primaryColor;
101
+ }
102
+ for (const [rawKey, rawValue] of Object.entries(cssTokens)) {
103
+ const name = sanitizeCssCustomPropertyName(rawKey);
104
+ const value = sanitizeCssValue(rawValue);
105
+ if (name !== null && value !== null) {
106
+ cssVars[name] = value;
107
+ }
108
+ }
109
+
110
+ return {
111
+ context: { displayName, logoUrl, primaryColor },
112
+ cssVars,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Scopes an app's branding to its subtree. Renders a wrapper element carrying
118
+ * the CSS custom properties inline, so they cascade to the app content and
119
+ * nowhere else. On unmount the element (and its properties) simply disappear —
120
+ * no cleanup, no global mutation.
121
+ */
122
+ export function AppBrandingProvider({
123
+ branding,
124
+ tenantBranding,
125
+ className,
126
+ children,
127
+ }: AppBrandingProviderProps): React.ReactElement {
128
+ const { context, cssVars } = React.useMemo(
129
+ () => resolveBranding(branding, tenantBranding),
130
+ [branding, tenantBranding],
131
+ );
132
+
133
+ return (
134
+ <AppBrandingContext.Provider value={context}>
135
+ <div
136
+ data-app-branding=""
137
+ className={cn(className)}
138
+ // Custom properties are not part of `React.CSSProperties`; the cast is
139
+ // the standard way to set `--*` vars inline.
140
+ style={cssVars as React.CSSProperties}
141
+ >
142
+ {children}
143
+ </div>
144
+ </AppBrandingContext.Provider>
145
+ );
146
+ }