@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,8 @@
|
|
|
1
|
+
import { type JSX } from 'react';
|
|
2
|
+
export interface PdfPreviewProps {
|
|
3
|
+
readonly url: string;
|
|
4
|
+
readonly title?: string;
|
|
5
|
+
readonly className?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function PdfPreview({ url, title, className }: PdfPreviewProps): JSX.Element;
|
|
8
|
+
//# sourceMappingURL=pdf-preview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pdf-preview.d.ts","sourceRoot":"","sources":["../../../src/ui/pdf/pdf-preview.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAkC,KAAK,GAAG,EAAE,MAAM,OAAO,CAAC;AAsBjE,MAAM,WAAW,eAAe;IAE9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,wBAAgB,UAAU,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,eAAe,GAAG,GAAG,CAAC,OAAO,CA2ElF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.PdfPreview = PdfPreview;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const lucide_react_1 = require("lucide-react");
|
|
7
|
+
const react_1 = require("react");
|
|
8
|
+
const react_pdf_1 = require("react-pdf");
|
|
9
|
+
const cn_1 = require("../cn");
|
|
10
|
+
if (!react_pdf_1.pdfjs.GlobalWorkerOptions.workerSrc) {
|
|
11
|
+
react_pdf_1.pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs';
|
|
12
|
+
}
|
|
13
|
+
const MIN_SCALE = 0.5;
|
|
14
|
+
const MAX_SCALE = 3;
|
|
15
|
+
const STEP = 0.25;
|
|
16
|
+
const DEFAULT_SCALE = 1;
|
|
17
|
+
const clampScale = (s) => Math.min(MAX_SCALE, Math.max(MIN_SCALE, Math.round(s * 100) / 100));
|
|
18
|
+
function PdfPreview({ url, title, className }) {
|
|
19
|
+
const [numPages, setNumPages] = (0, react_1.useState)(0);
|
|
20
|
+
const [scale, setScale] = (0, react_1.useState)(DEFAULT_SCALE);
|
|
21
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
22
|
+
const file = (0, react_1.useMemo)(() => ({ url }), [url]);
|
|
23
|
+
const zoomOut = (0, react_1.useCallback)(() => setScale((s) => clampScale(s - STEP)), []);
|
|
24
|
+
const zoomIn = (0, react_1.useCallback)(() => setScale((s) => clampScale(s + STEP)), []);
|
|
25
|
+
const reset = (0, react_1.useCallback)(() => setScale(DEFAULT_SCALE), []);
|
|
26
|
+
const btn = 'inline-flex h-7 w-7 items-center justify-center rounded text-neutral-600 ' +
|
|
27
|
+
'hover:bg-neutral-200/70 disabled:pointer-events-none disabled:opacity-40 ' +
|
|
28
|
+
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1';
|
|
29
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)('flex h-[520px] flex-col overflow-hidden rounded border border-neutral-200 bg-white', className), children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-1 border-b border-neutral-200 bg-neutral-50 px-2 py-1", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", onClick: zoomOut, disabled: scale <= MIN_SCALE, "aria-label": "Zoom out", className: btn, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ZoomOut, { className: "h-4 w-4" }) }), (0, jsx_runtime_1.jsxs)("span", { className: "w-12 text-center text-xs tabular-nums text-neutral-500", children: [Math.round(scale * 100), "%"] }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: zoomIn, disabled: scale >= MAX_SCALE, "aria-label": "Zoom in", className: btn, children: (0, jsx_runtime_1.jsx)(lucide_react_1.ZoomIn, { className: "h-4 w-4" }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: reset, disabled: scale === DEFAULT_SCALE, "aria-label": "Reset zoom", className: btn, children: (0, jsx_runtime_1.jsx)(lucide_react_1.RotateCcw, { className: "h-4 w-4" }) }), title ? ((0, jsx_runtime_1.jsx)("span", { className: "ml-2 min-w-0 flex-1 truncate text-xs text-neutral-400", title: title, children: title })) : null] }), (0, jsx_runtime_1.jsx)("div", { className: "flex-1 overflow-auto bg-neutral-100 p-3", children: error ? ((0, jsx_runtime_1.jsxs)("div", { className: "p-4 text-xs text-neutral-500", children: ["Couldn't render this PDF (", error, "). Use the download affordance."] })) : ((0, jsx_runtime_1.jsx)(react_pdf_1.Document, { file: file, onLoadSuccess: ({ numPages: n }) => {
|
|
30
|
+
setNumPages(n);
|
|
31
|
+
setError(null);
|
|
32
|
+
}, onLoadError: (e) => setError(e.message), loading: (0, jsx_runtime_1.jsx)("div", { className: "p-4 text-xs text-neutral-500", children: "Loading PDF\u2026" }), className: "flex flex-col items-center gap-3", children: Array.from({ length: numPages }, (_, i) => ((0, jsx_runtime_1.jsx)(react_pdf_1.Page, { pageNumber: i + 1, scale: scale, renderTextLayer: false, renderAnnotationLayer: false, className: "shadow-sm" }, i))) })) })] }));
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=pdf-preview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pdf-preview.js","sourceRoot":"","sources":["../../../src/ui/pdf/pdf-preview.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export type PreviewDevice = 'desktop' | 'tablet' | 'mobile' | 'presentation' | 'a4';
|
|
3
|
+
interface PreviewDeviceFrameProps {
|
|
4
|
+
readonly device: PreviewDevice;
|
|
5
|
+
readonly children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
export declare function PreviewDeviceFrame({ device, children, }: PreviewDeviceFrameProps): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=preview-device-frame.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview-device-frame.d.ts","sourceRoot":"","sources":["../../../src/ui/primitives/preview-device-frame.tsx"],"names":[],"mappings":"AAmBA,OAAO,EAAqC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAS1E,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,cAAc,GACd,IAAI,CAAC;AAET,UAAU,uBAAuB;IAC/B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;CAC9B;AAoBD,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,QAAQ,GACT,EAAE,uBAAuB,+BAmBzB"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PreviewDeviceFrame = PreviewDeviceFrame;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const cn_1 = require("../cn");
|
|
7
|
+
const BEZELS = {
|
|
8
|
+
tablet: { width: 768, height: 1024, radius: 18 },
|
|
9
|
+
mobile: { width: 390, height: 844, radius: 36 },
|
|
10
|
+
};
|
|
11
|
+
const PRESENTATION_VIEWPORT = { width: 1280, height: 720, radius: 20 };
|
|
12
|
+
const A4_VIEWPORT = { width: 794, height: 1123, radius: 4 };
|
|
13
|
+
function PreviewDeviceFrame({ device, children, }) {
|
|
14
|
+
if (device === 'desktop') {
|
|
15
|
+
return (0, jsx_runtime_1.jsx)("div", { className: "h-full w-full", children: children });
|
|
16
|
+
}
|
|
17
|
+
if (device === 'presentation') {
|
|
18
|
+
return ((0, jsx_runtime_1.jsx)(ResponsiveViewport, { bezel: PRESENTATION_VIEWPORT, aspectRatio: 16 / 9, children: children }));
|
|
19
|
+
}
|
|
20
|
+
if (device === 'a4') {
|
|
21
|
+
return ((0, jsx_runtime_1.jsx)(ResponsiveViewport, { bezel: A4_VIEWPORT, aspectRatio: 794 / 1123, children: children }));
|
|
22
|
+
}
|
|
23
|
+
return (0, jsx_runtime_1.jsx)(ScaledBezel, { bezel: BEZELS[device], children: children });
|
|
24
|
+
}
|
|
25
|
+
function ScaledBezel({ bezel, children }) {
|
|
26
|
+
const containerRef = (0, react_1.useRef)(null);
|
|
27
|
+
const [scale, setScale] = (0, react_1.useState)(1);
|
|
28
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
29
|
+
const el = containerRef.current;
|
|
30
|
+
if (!el)
|
|
31
|
+
return;
|
|
32
|
+
const compute = () => {
|
|
33
|
+
const { clientWidth, clientHeight } = el;
|
|
34
|
+
if (clientWidth === 0 || clientHeight === 0)
|
|
35
|
+
return;
|
|
36
|
+
const margin = 24;
|
|
37
|
+
const availW = Math.max(0, clientWidth - margin);
|
|
38
|
+
const availH = Math.max(0, clientHeight - margin);
|
|
39
|
+
const next = Math.min(1, availW / bezel.width, availH / bezel.height);
|
|
40
|
+
setScale(next);
|
|
41
|
+
};
|
|
42
|
+
compute();
|
|
43
|
+
const observer = new ResizeObserver(compute);
|
|
44
|
+
observer.observe(el);
|
|
45
|
+
return () => observer.disconnect();
|
|
46
|
+
}, [bezel.width, bezel.height]);
|
|
47
|
+
return ((0, jsx_runtime_1.jsx)("div", { ref: containerRef, className: "flex h-full w-full items-center justify-center overflow-hidden bg-paper-elev/60 p-2", children: (0, jsx_runtime_1.jsx)("div", { className: (0, cn_1.cn)('relative shrink-0 overflow-hidden bg-paper shadow-2xl ring-1 ring-border/75'), style: {
|
|
48
|
+
width: bezel.width,
|
|
49
|
+
height: bezel.height,
|
|
50
|
+
borderRadius: bezel.radius,
|
|
51
|
+
transform: `scale(${scale})`,
|
|
52
|
+
transformOrigin: 'center center',
|
|
53
|
+
}, children: children }) }));
|
|
54
|
+
}
|
|
55
|
+
function ResponsiveViewport({ bezel, aspectRatio, children, }) {
|
|
56
|
+
const containerRef = (0, react_1.useRef)(null);
|
|
57
|
+
const [size, setSize] = (0, react_1.useState)(() => ({
|
|
58
|
+
width: bezel.width,
|
|
59
|
+
height: bezel.height,
|
|
60
|
+
}));
|
|
61
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
62
|
+
const el = containerRef.current;
|
|
63
|
+
if (!el)
|
|
64
|
+
return;
|
|
65
|
+
const compute = () => {
|
|
66
|
+
const { clientWidth, clientHeight } = el;
|
|
67
|
+
if (clientWidth === 0 || clientHeight === 0)
|
|
68
|
+
return;
|
|
69
|
+
const margin = 24;
|
|
70
|
+
const availW = Math.max(0, clientWidth - margin);
|
|
71
|
+
const availH = Math.max(0, clientHeight - margin);
|
|
72
|
+
const nextWidth = Math.min(availW, availH * aspectRatio);
|
|
73
|
+
const nextHeight = nextWidth / aspectRatio;
|
|
74
|
+
setSize({
|
|
75
|
+
width: Math.max(1, Math.round(nextWidth)),
|
|
76
|
+
height: Math.max(1, Math.round(nextHeight)),
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
compute();
|
|
80
|
+
const observer = new ResizeObserver(compute);
|
|
81
|
+
observer.observe(el);
|
|
82
|
+
return () => observer.disconnect();
|
|
83
|
+
}, [bezel.width, bezel.height]);
|
|
84
|
+
return ((0, jsx_runtime_1.jsx)("div", { ref: containerRef, className: "flex h-full w-full items-center justify-center overflow-hidden bg-paper-elev/60 p-2", children: (0, jsx_runtime_1.jsx)("div", { className: (0, cn_1.cn)('relative shrink-0 overflow-hidden bg-paper shadow-2xl ring-1 ring-border/75'), style: {
|
|
85
|
+
width: size.width,
|
|
86
|
+
height: size.height,
|
|
87
|
+
borderRadius: bezel.radius,
|
|
88
|
+
}, children: children }) }));
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=preview-device-frame.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview-device-frame.js","sourceRoot":"","sources":["../../../src/ui/primitives/preview-device-frame.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/ui-kernel",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
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,6 +53,7 @@
|
|
|
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
58
|
"tailwind-merge": "^2.5.4"
|
|
59
59
|
},
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"prettier": "3.6.2",
|
|
75
75
|
"react": "^18.3.1",
|
|
76
76
|
"typescript": "5.9.3",
|
|
77
|
-
"@xemahq/kernel-contracts": "^0.
|
|
77
|
+
"@xemahq/kernel-contracts": "^0.36.0"
|
|
78
78
|
},
|
|
79
79
|
"exports": {
|
|
80
80
|
".": {
|
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
"scripts": {
|
|
102
102
|
"clean": "rm -rf dist",
|
|
103
103
|
"build": "tsc -p tsconfig.json",
|
|
104
|
+
"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
105
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
105
106
|
"lint": "tsc -p tsconfig.lint.json --noEmit",
|
|
106
107
|
"typecheck": "tsc -p tsconfig.lint.json --noEmit"
|
|
@@ -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
|
+
});
|