@contractspec/lib.presentation-runtime-core 3.8.3 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -517,9 +517,88 @@ function withPresentationMetroAliases(config, opts = {}) {
517
517
  };
518
518
  return config;
519
519
  }
520
+ var tech_presentation_runtime_DocBlocks = [
521
+ {
522
+ id: "docs.tech.presentation-runtime",
523
+ title: "Presentation Runtime",
524
+ summary: "Cross-platform runtime for list pages, presentation flows, and headless ContractSpec tables.",
525
+ kind: "reference",
526
+ visibility: "public",
527
+ route: "/docs/tech/presentation-runtime",
528
+ tags: ["tech", "presentation-runtime"],
529
+ body: `## Presentation Runtime
530
+
531
+ Cross-platform runtime for list pages, presentation flows, and headless ContractSpec tables.
532
+
533
+ ### Packages
534
+
535
+ - \`@contractspec/lib.presentation-runtime-core\`: shared state/types for URL state, list coordination, and headless table controllers
536
+ - \`@contractspec/lib.presentation-runtime-react\`: React hooks (web/native-compatible API)
537
+ - \`@contractspec/lib.presentation-runtime-react-native\`: Native entrypoint (re-exports the shared React table hooks and keeps native form/list helpers)
538
+
539
+ ### Next.js config helper
540
+
541
+ \`\`\`ts
542
+ // next.config.mjs
543
+ import { withPresentationNextAliases } from '@contractspec/lib.presentation-runtime-core/next';
544
+
545
+ const nextConfig = {
546
+ webpack: (config) => withPresentationNextAliases(config),
547
+ };
548
+
549
+ export default nextConfig;
550
+ \`\`\`
551
+
552
+ ### Metro config helper
553
+
554
+ \`\`\`js
555
+ // metro.config.js (CJS)
556
+ const { getDefaultConfig } = require('expo/metro-config');
557
+ const {
558
+ withPresentationMetroAliases,
559
+ } = require('@contractspec/lib.presentation-runtime-core/src/metro.cjs');
560
+
561
+ const projectRoot = __dirname;
562
+ const config = getDefaultConfig(projectRoot);
563
+
564
+ module.exports = withPresentationMetroAliases(config);
565
+ \`\`\`
566
+
567
+ ### React hooks
568
+
569
+ - \`useListCoordinator\`: URL + RHF + derived variables (no fetching)
570
+ - \`usePresentationController\`: Same plus \`fetcher\` integration
571
+ - \`useContractTable\`: headless TanStack-backed controller for sorting, pagination, selection, visibility, pinning, resizing, and expansion
572
+ - \`useDataViewTable\`: adapter that maps \`DataViewSpec\` table contracts onto the generic controller
573
+ - \`DataViewRenderer\` (design-system): render \`DataViewSpec\` projections (\`list\`, \`table\`, \`detail\`, \`grid\`) using shared UI atoms
574
+
575
+ Both list hooks accept a \`useUrlState\` adapter. On web, use \`useListUrlState\` (design-system) or a Next adapter.
576
+
577
+ ### Table layering
578
+
579
+ - \`contracts-spec\`: declarative table contract (\`DataViewTableConfig\`)
580
+ - \`presentation-runtime-*\`: headless controller state and TanStack integration
581
+ - \`ui-kit\` / \`ui-kit-web\`: platform renderers that consume controller output
582
+ - \`design-system\`: opinionated \`DataTable\`, \`DataViewTable\`, \`ApprovalQueue\`, and \`ListTablePage\`
583
+
584
+ ### KYC molecules (bundle)
585
+
586
+ - \`ComplianceBadge\` in \`@contractspec/bundle.strit/presentation/components/kyc\` renders a status badge for KYC/compliance snapshots. It accepts a \`state\` (missing_core | incomplete | complete | expiring | unknown) and optional localized \`labels\`. Prefer consuming apps to pass translated labels (e.g., via \`useT('appPlatformAdmin')\`).
587
+
588
+ ### Markdown routes and llms.txt
589
+
590
+ - Each web app exposes \`/llms\` (and \`/llms.txt\`, \`/llms.md\`) via rewrites. See [llmstxt.org](https://llmstxt.org/).
591
+ - Catch‑all markdown handler lives at \`app/[...slug].md/route.ts\`. It resolves a page descriptor from \`app/.presentations.manifest.json\` and renders via the \`presentations.v2\` engine (target: \`markdown\`).
592
+ - Per‑page companion convention: add \`app/<route>/ai.ts\` exporting a \`PresentationSpec\`.
593
+ - Build‑time tool: \`tools/generate-presentations-manifest.mjs <app-root>\` populates the manifest.
594
+ - CI check: \`bunllms:check\` verifies coverage (% of pages with descriptors) and fails if below threshold.
595
+ `
596
+ }
597
+ ];
520
598
  export {
521
599
  withPresentationNextAliases,
522
600
  withPresentationMetroAliases,
601
+ tech_presentation_runtime_DocBlocks,
523
602
  formatVisualizationValue,
524
603
  createVisualizationModel,
525
604
  createEmptyTableState,
@@ -0,0 +1,314 @@
1
+ // src/transform-engine.markdown.ts
2
+ import TurndownService from "turndown";
3
+ function renderTextNode(node) {
4
+ const text = node.text ?? "";
5
+ if (!node.marks || node.marks.length === 0)
6
+ return text;
7
+ return node.marks.reduce((acc, mark) => {
8
+ switch (mark.type) {
9
+ case "bold":
10
+ return `**${acc}**`;
11
+ case "italic":
12
+ return `*${acc}*`;
13
+ case "underline":
14
+ return `__${acc}__`;
15
+ case "strike":
16
+ return `~~${acc}~~`;
17
+ case "code":
18
+ return `\`${acc}\``;
19
+ case "link": {
20
+ const href = mark.attrs?.href ?? "";
21
+ return href ? `[${acc}](${href})` : acc;
22
+ }
23
+ default:
24
+ return acc;
25
+ }
26
+ }, text);
27
+ }
28
+ function renderInline(nodes) {
29
+ if (!nodes?.length)
30
+ return "";
31
+ return nodes.map((child) => renderNode(child)).join("");
32
+ }
33
+ function renderList(nodes, ordered = false) {
34
+ if (!nodes?.length)
35
+ return "";
36
+ let counter = 1;
37
+ return nodes.map((item) => {
38
+ const body = renderInline(item.content ?? []);
39
+ if (!body)
40
+ return "";
41
+ const prefix = ordered ? `${counter++}. ` : "- ";
42
+ return `${prefix}${body}`;
43
+ }).filter(Boolean).join(`
44
+ `);
45
+ }
46
+ function renderNode(node) {
47
+ switch (node.type) {
48
+ case "doc":
49
+ return renderInline(node.content);
50
+ case "paragraph": {
51
+ const text = renderInline(node.content);
52
+ return text.trim().length ? text : "";
53
+ }
54
+ case "heading": {
55
+ const levelAttr = node.attrs?.level;
56
+ const levelVal = typeof levelAttr === "number" ? levelAttr : 1;
57
+ const level = Math.min(Math.max(levelVal, 1), 6);
58
+ return `${"#".repeat(level)} ${renderInline(node.content)}`.trim();
59
+ }
60
+ case "bullet_list":
61
+ return renderList(node.content, false);
62
+ case "ordered_list":
63
+ return renderList(node.content, true);
64
+ case "list_item":
65
+ return renderInline(node.content);
66
+ case "blockquote": {
67
+ const body = renderInline(node.content);
68
+ return body.split(`
69
+ `).map((line) => `> ${line}`).join(`
70
+ `);
71
+ }
72
+ case "code_block": {
73
+ const body = renderInline(node.content);
74
+ return body ? `\`\`\`
75
+ ${body}
76
+ \`\`\`` : "";
77
+ }
78
+ case "horizontal_rule":
79
+ return "---";
80
+ case "hard_break":
81
+ return `
82
+ `;
83
+ case "text":
84
+ return renderTextNode(node);
85
+ default:
86
+ if (node.text)
87
+ return renderTextNode(node);
88
+ return "";
89
+ }
90
+ }
91
+ function createMarkdownTurndownService() {
92
+ const turndownService = new TurndownService({
93
+ headingStyle: "atx",
94
+ codeBlockStyle: "fenced",
95
+ bulletListMarker: "-"
96
+ });
97
+ turndownService.addRule("link", {
98
+ filter: "a",
99
+ replacement: (content, node) => {
100
+ const candidate = node;
101
+ const href = candidate.getAttribute?.("href") ?? (typeof candidate.href === "string" ? candidate.href : "");
102
+ if (href && content) {
103
+ return `[${content}](${href})`;
104
+ }
105
+ return content || "";
106
+ }
107
+ });
108
+ return turndownService;
109
+ }
110
+ var defaultTurndown = createMarkdownTurndownService();
111
+ function htmlToMarkdown(html) {
112
+ return defaultTurndown.turndown(html);
113
+ }
114
+ function blockNoteToMarkdown(docJson) {
115
+ if (typeof docJson === "string")
116
+ return docJson;
117
+ if (docJson && typeof docJson === "object" && "html" in docJson) {
118
+ const html = String(docJson.html);
119
+ return htmlToMarkdown(html);
120
+ }
121
+ const root = docJson;
122
+ if (root?.type === "doc" || root?.content) {
123
+ const blocks = (root.content ?? []).map((node) => renderNode(node)).filter(Boolean);
124
+ return blocks.join(`
125
+
126
+ `).trim();
127
+ }
128
+ try {
129
+ return JSON.stringify(docJson, null, 2);
130
+ } catch {
131
+ return String(docJson);
132
+ }
133
+ }
134
+
135
+ // src/transform-engine.ts
136
+ import { schemaToMarkdown } from "@contractspec/lib.contracts-spec/schema-to-markdown";
137
+ function applyPii(desc, obj) {
138
+ let clone;
139
+ try {
140
+ clone = JSON.parse(JSON.stringify(obj));
141
+ } catch {
142
+ clone = obj;
143
+ }
144
+ const paths = desc.policy?.pii ?? [];
145
+ const setAtPath = (root, path) => {
146
+ const segments = path.replace(/^\//, "").replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
147
+ let current = root;
148
+ for (let i = 0;i < segments.length - 1; i++) {
149
+ const key = segments[i];
150
+ if (!key || !current || typeof current !== "object" || !(key in current)) {
151
+ return;
152
+ }
153
+ current = current[key];
154
+ }
155
+ const last = segments[segments.length - 1];
156
+ if (current && typeof current === "object" && last && last in current) {
157
+ current[last] = "[REDACTED]";
158
+ }
159
+ };
160
+ for (const path of paths) {
161
+ setAtPath(clone, path);
162
+ }
163
+ return clone;
164
+ }
165
+
166
+ class TransformEngine {
167
+ renderers = new Map;
168
+ validators = [];
169
+ register(renderer) {
170
+ const renderers = this.renderers.get(renderer.target) ?? [];
171
+ renderers.push(renderer);
172
+ this.renderers.set(renderer.target, renderers);
173
+ return this;
174
+ }
175
+ prependRegister(renderer) {
176
+ const renderers = this.renderers.get(renderer.target) ?? [];
177
+ renderers.unshift(renderer);
178
+ this.renderers.set(renderer.target, renderers);
179
+ return this;
180
+ }
181
+ addValidator(validator) {
182
+ this.validators.push(validator);
183
+ return this;
184
+ }
185
+ async render(target, desc, ctx) {
186
+ if (!desc.targets.includes(target)) {
187
+ throw new Error(`Target ${target} not declared for ${desc.meta.key}.v${desc.meta.version}`);
188
+ }
189
+ for (const validator of this.validators) {
190
+ await validator.validate(desc, target, ctx);
191
+ }
192
+ const renderers = this.renderers.get(target) ?? [];
193
+ for (const renderer of renderers) {
194
+ try {
195
+ return await renderer.render(desc, ctx);
196
+ } catch {}
197
+ }
198
+ throw new Error(`No renderer available for ${target}`);
199
+ }
200
+ }
201
+ function createDefaultTransformEngine() {
202
+ const engine = new TransformEngine;
203
+ engine.register({
204
+ target: "markdown",
205
+ async render(desc, ctx) {
206
+ let data = ctx?.data;
207
+ if (!data && ctx?.fetchData) {
208
+ data = await ctx.fetchData();
209
+ }
210
+ if (desc.source.type === "component" && desc.source.props && data !== undefined) {
211
+ const isArray = Array.isArray(data);
212
+ const isSimpleObject = !isArray && typeof data === "object" && data !== null && !Object.values(data).some((value) => Array.isArray(value) || typeof value === "object" && value !== null);
213
+ if (isArray || isSimpleObject) {
214
+ return {
215
+ mimeType: "text/markdown",
216
+ body: schemaToMarkdown(desc.source.props, data, {
217
+ title: desc.meta.description ?? desc.meta.key,
218
+ description: `${desc.meta.key} v${desc.meta.version}`
219
+ })
220
+ };
221
+ }
222
+ throw new Error(`Complex data structure for ${desc.meta.key} - expecting custom renderer`);
223
+ }
224
+ if (desc.source.type === "blocknotejs") {
225
+ const redacted = applyPii(desc, {
226
+ text: blockNoteToMarkdown(desc.source.docJson)
227
+ });
228
+ return {
229
+ mimeType: "text/markdown",
230
+ body: String(redacted.text ?? "")
231
+ };
232
+ }
233
+ if (desc.source.type === "component" && data !== undefined) {
234
+ throw new Error(`No schema (source.props) available for ${desc.meta.key} - expecting custom renderer`);
235
+ }
236
+ if (desc.source.type !== "component") {
237
+ throw new Error("unsupported");
238
+ }
239
+ const header = `# ${desc.meta.key} v${desc.meta.version}`;
240
+ const about = desc.meta.description ? `
241
+
242
+ ${desc.meta.description}` : "";
243
+ const tags = desc.meta.tags && desc.meta.tags.length ? `
244
+
245
+ Tags: ${desc.meta.tags.join(", ")}` : "";
246
+ const owners = desc.meta.owners && desc.meta.owners.length ? `
247
+
248
+ Owners: ${desc.meta.owners.join(", ")}` : "";
249
+ const component = `
250
+
251
+ Component: \`${desc.source.componentKey}\``;
252
+ const policy = desc.policy?.pii?.length ? `
253
+
254
+ Redacted paths: ${desc.policy.pii.map((path) => `\`${path}\``).join(", ")}` : "";
255
+ return {
256
+ mimeType: "text/markdown",
257
+ body: `${header}${about}${tags}${owners}${component}${policy}`
258
+ };
259
+ }
260
+ });
261
+ engine.register({
262
+ target: "application/json",
263
+ async render(desc) {
264
+ const payload = applyPii(desc, { meta: desc.meta, source: desc.source });
265
+ let body;
266
+ try {
267
+ body = JSON.stringify(payload, null, 2);
268
+ } catch {
269
+ body = JSON.stringify({
270
+ meta: { key: desc.meta.key, version: desc.meta.version },
271
+ source: "[non-serializable]"
272
+ }, null, 2);
273
+ }
274
+ return { mimeType: "application/json", body };
275
+ }
276
+ });
277
+ engine.register({
278
+ target: "application/xml",
279
+ async render(desc) {
280
+ const payload = applyPii(desc, { meta: desc.meta, source: desc.source });
281
+ let json;
282
+ try {
283
+ json = JSON.stringify(payload);
284
+ } catch {
285
+ json = JSON.stringify({
286
+ meta: { key: desc.meta.key, version: desc.meta.version },
287
+ source: "[non-serializable]"
288
+ });
289
+ }
290
+ return {
291
+ mimeType: "application/xml",
292
+ body: `<presentation name="${desc.meta.key}" version="${desc.meta.version}"><json>${encodeURIComponent(json)}</json></presentation>`
293
+ };
294
+ }
295
+ });
296
+ return engine;
297
+ }
298
+ function registerBasicValidation(engine) {
299
+ engine.addValidator({
300
+ validate(desc) {
301
+ if (!desc.meta.description || desc.meta.description.length < 3) {
302
+ throw new Error(`Presentation ${desc.meta.key}.v${desc.meta.version} missing meta.description`);
303
+ }
304
+ }
305
+ });
306
+ return engine;
307
+ }
308
+ export {
309
+ registerBasicValidation,
310
+ htmlToMarkdown,
311
+ createDefaultTransformEngine,
312
+ blockNoteToMarkdown,
313
+ TransformEngine
314
+ };
@@ -0,0 +1,138 @@
1
+ // src/transform-engine.markdown.ts
2
+ import TurndownService from "turndown";
3
+ function renderTextNode(node) {
4
+ const text = node.text ?? "";
5
+ if (!node.marks || node.marks.length === 0)
6
+ return text;
7
+ return node.marks.reduce((acc, mark) => {
8
+ switch (mark.type) {
9
+ case "bold":
10
+ return `**${acc}**`;
11
+ case "italic":
12
+ return `*${acc}*`;
13
+ case "underline":
14
+ return `__${acc}__`;
15
+ case "strike":
16
+ return `~~${acc}~~`;
17
+ case "code":
18
+ return `\`${acc}\``;
19
+ case "link": {
20
+ const href = mark.attrs?.href ?? "";
21
+ return href ? `[${acc}](${href})` : acc;
22
+ }
23
+ default:
24
+ return acc;
25
+ }
26
+ }, text);
27
+ }
28
+ function renderInline(nodes) {
29
+ if (!nodes?.length)
30
+ return "";
31
+ return nodes.map((child) => renderNode(child)).join("");
32
+ }
33
+ function renderList(nodes, ordered = false) {
34
+ if (!nodes?.length)
35
+ return "";
36
+ let counter = 1;
37
+ return nodes.map((item) => {
38
+ const body = renderInline(item.content ?? []);
39
+ if (!body)
40
+ return "";
41
+ const prefix = ordered ? `${counter++}. ` : "- ";
42
+ return `${prefix}${body}`;
43
+ }).filter(Boolean).join(`
44
+ `);
45
+ }
46
+ function renderNode(node) {
47
+ switch (node.type) {
48
+ case "doc":
49
+ return renderInline(node.content);
50
+ case "paragraph": {
51
+ const text = renderInline(node.content);
52
+ return text.trim().length ? text : "";
53
+ }
54
+ case "heading": {
55
+ const levelAttr = node.attrs?.level;
56
+ const levelVal = typeof levelAttr === "number" ? levelAttr : 1;
57
+ const level = Math.min(Math.max(levelVal, 1), 6);
58
+ return `${"#".repeat(level)} ${renderInline(node.content)}`.trim();
59
+ }
60
+ case "bullet_list":
61
+ return renderList(node.content, false);
62
+ case "ordered_list":
63
+ return renderList(node.content, true);
64
+ case "list_item":
65
+ return renderInline(node.content);
66
+ case "blockquote": {
67
+ const body = renderInline(node.content);
68
+ return body.split(`
69
+ `).map((line) => `> ${line}`).join(`
70
+ `);
71
+ }
72
+ case "code_block": {
73
+ const body = renderInline(node.content);
74
+ return body ? `\`\`\`
75
+ ${body}
76
+ \`\`\`` : "";
77
+ }
78
+ case "horizontal_rule":
79
+ return "---";
80
+ case "hard_break":
81
+ return `
82
+ `;
83
+ case "text":
84
+ return renderTextNode(node);
85
+ default:
86
+ if (node.text)
87
+ return renderTextNode(node);
88
+ return "";
89
+ }
90
+ }
91
+ function createMarkdownTurndownService() {
92
+ const turndownService = new TurndownService({
93
+ headingStyle: "atx",
94
+ codeBlockStyle: "fenced",
95
+ bulletListMarker: "-"
96
+ });
97
+ turndownService.addRule("link", {
98
+ filter: "a",
99
+ replacement: (content, node) => {
100
+ const candidate = node;
101
+ const href = candidate.getAttribute?.("href") ?? (typeof candidate.href === "string" ? candidate.href : "");
102
+ if (href && content) {
103
+ return `[${content}](${href})`;
104
+ }
105
+ return content || "";
106
+ }
107
+ });
108
+ return turndownService;
109
+ }
110
+ var defaultTurndown = createMarkdownTurndownService();
111
+ function htmlToMarkdown(html) {
112
+ return defaultTurndown.turndown(html);
113
+ }
114
+ function blockNoteToMarkdown(docJson) {
115
+ if (typeof docJson === "string")
116
+ return docJson;
117
+ if (docJson && typeof docJson === "object" && "html" in docJson) {
118
+ const html = String(docJson.html);
119
+ return htmlToMarkdown(html);
120
+ }
121
+ const root = docJson;
122
+ if (root?.type === "doc" || root?.content) {
123
+ const blocks = (root.content ?? []).map((node) => renderNode(node)).filter(Boolean);
124
+ return blocks.join(`
125
+
126
+ `).trim();
127
+ }
128
+ try {
129
+ return JSON.stringify(docJson, null, 2);
130
+ } catch {
131
+ return String(docJson);
132
+ }
133
+ }
134
+ export {
135
+ htmlToMarkdown,
136
+ createMarkdownTurndownService,
137
+ blockNoteToMarkdown
138
+ };
@@ -0,0 +1,27 @@
1
+ import type { PresentationSpec, PresentationTarget } from '@contractspec/lib.contracts-spec/presentations';
2
+ import { blockNoteToMarkdown, htmlToMarkdown } from './transform-engine.markdown';
3
+ export interface RenderContext {
4
+ locale?: string;
5
+ featureFlags?: string[];
6
+ redact?: (path: string, value: unknown) => unknown;
7
+ data?: unknown;
8
+ fetchData?: () => Promise<unknown>;
9
+ }
10
+ export interface PresentationRenderer<TOut> {
11
+ target: PresentationTarget;
12
+ render: (desc: PresentationSpec, ctx?: RenderContext) => Promise<TOut>;
13
+ }
14
+ export interface PresentationValidator {
15
+ validate: (desc: PresentationSpec, target: PresentationTarget, ctx?: RenderContext) => Promise<void> | void;
16
+ }
17
+ export declare class TransformEngine {
18
+ private renderers;
19
+ private validators;
20
+ register<TOut>(renderer: PresentationRenderer<TOut>): this;
21
+ prependRegister<TOut>(renderer: PresentationRenderer<TOut>): this;
22
+ addValidator(validator: PresentationValidator): this;
23
+ render<TOut = unknown>(target: PresentationTarget, desc: PresentationSpec, ctx?: RenderContext): Promise<TOut>;
24
+ }
25
+ export declare function createDefaultTransformEngine(): TransformEngine;
26
+ export declare function registerBasicValidation(engine: TransformEngine): TransformEngine;
27
+ export { blockNoteToMarkdown, htmlToMarkdown };