@xemahq/ui-kernel 0.6.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.
- 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/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 +6 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +14 -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/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/package.json +8 -7
- 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/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 +22 -0
- package/src/ui/pdf/pdf-preview.tsx +122 -0
- package/src/ui/primitives/preview-device-frame.tsx +205 -0
|
@@ -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';
|
|
@@ -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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Risk-tier → colour expressions for the approval frame's accent rail and
|
|
3
|
-
* the sticky bar
|
|
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.
|
|
@@ -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 (
|
|
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
|
|
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';
|
package/src/session-kit/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
*/
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// Branding DOM-boundary sanitizers
|
|
3
|
+
//
|
|
4
|
+
// A `BrandingConfig` is validated server-side (hex/rgb(a) `primaryColor`, a
|
|
5
|
+
// URL `logoUrl`), but the runtime applier writes those values into the live
|
|
6
|
+
// DOM — a CSS custom-property value and an `<img src>`. This module is the
|
|
7
|
+
// fail-closed boundary: a value that could break out of the property it is
|
|
8
|
+
// written into, or a URL scheme that could execute, is DROPPED (returns
|
|
9
|
+
// `null` / `undefined`), never written raw. Pure functions, no React — unit
|
|
10
|
+
// tested directly.
|
|
11
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Characters that could terminate a CSS declaration/value and inject a new
|
|
15
|
+
* declaration or rule (`;`, `{`, `}`), close a surrounding context (`<`, `>`),
|
|
16
|
+
* escape (`\`), or open/close a CSS comment. React writes custom properties
|
|
17
|
+
* through the CSSOM, which already treats the string as a single value — this
|
|
18
|
+
* is defence in depth so a crafted value is refused at the seam.
|
|
19
|
+
*/
|
|
20
|
+
const CSS_VALUE_UNSAFE = /[;{}<>\\]|\/\*|\*\//;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A CSS custom-property name (without the leading `--`): a CSS identifier. A
|
|
24
|
+
* `cssTokens` key becomes `--<key>`, so a key with breakout characters would be
|
|
25
|
+
* a foothold; anything that is not a plain ident is refused.
|
|
26
|
+
*/
|
|
27
|
+
const CSS_CUSTOM_PROPERTY_NAME = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
|
|
28
|
+
|
|
29
|
+
/** True when the string contains any ASCII control character (0x00–0x1f, 0x7f). */
|
|
30
|
+
function hasControlChar(value: string): boolean {
|
|
31
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
32
|
+
const code = value.charCodeAt(i);
|
|
33
|
+
if (code < 0x20 || code === 0x7f) return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sanitize a value destined for a CSS custom property (`--app-primary-color`
|
|
40
|
+
* or a `cssTokens` value). Returns the trimmed value, or `null` to DROP it.
|
|
41
|
+
*/
|
|
42
|
+
export function sanitizeCssValue(raw: string): string | null {
|
|
43
|
+
const value = raw.trim();
|
|
44
|
+
if (value.length === 0) return null;
|
|
45
|
+
if (CSS_VALUE_UNSAFE.test(value)) return null;
|
|
46
|
+
if (hasControlChar(value)) return null;
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Sanitize a `cssTokens` KEY into a `--<name>` custom-property name. Returns
|
|
52
|
+
* the full property name (`--<name>`), or `null` to DROP the token.
|
|
53
|
+
*/
|
|
54
|
+
export function sanitizeCssCustomPropertyName(rawKey: string): string | null {
|
|
55
|
+
const name = rawKey.trim();
|
|
56
|
+
if (!CSS_CUSTOM_PROPERTY_NAME.test(name)) return null;
|
|
57
|
+
return `--${name}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Sanitize a logo URL destined for an `<img src>`. Only absolute `http(s)`
|
|
62
|
+
* URLs pass; a `javascript:`, `data:`, or otherwise-schemed / unparseable value
|
|
63
|
+
* is refused (returns `undefined`) so it never reaches `src`.
|
|
64
|
+
*/
|
|
65
|
+
export function sanitizeLogoUrl(raw: string | undefined): string | undefined {
|
|
66
|
+
if (raw === undefined) return undefined;
|
|
67
|
+
let parsed: URL;
|
|
68
|
+
try {
|
|
69
|
+
parsed = new URL(raw);
|
|
70
|
+
} catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return parsed.href;
|
|
77
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsive breakpoint token — the single source of truth shared by the
|
|
3
|
+
* JS side (`useIsMobile`) and Tailwind's `md` utility.
|
|
4
|
+
*
|
|
5
|
+
* Tailwind's default `md` breakpoint is `min-width: 768px`. Anything narrower
|
|
6
|
+
* than that is "mobile". Binding the JS constant to that exact value is what
|
|
7
|
+
* lets a component decide table-vs-cards in JS and stay in lockstep with the
|
|
8
|
+
* `md:` classes the same component (or its siblings) use — there is exactly
|
|
9
|
+
* ONE breakpoint system, not two.
|
|
10
|
+
*
|
|
11
|
+
* If the Tailwind `md` screen is ever re-tuned, change it HERE and in the
|
|
12
|
+
* Tailwind theme in the same commit — they are two faces of one token.
|
|
13
|
+
*/
|
|
14
|
+
export const MOBILE_BREAKPOINT = 768;
|