@esheet/renderer 0.0.3 → 0.0.4-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +3 -3
  2. package/dist/index.d.ts +9 -102
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +5284 -4315
  5. package/dist/lib/EsheetRenderer.d.ts +116 -0
  6. package/dist/lib/EsheetRenderer.d.ts.map +1 -0
  7. package/dist/lib/EsheetRenderer.js +115 -0
  8. package/dist/lib/components/FieldNode.d.ts +17 -0
  9. package/dist/lib/components/FieldNode.d.ts.map +1 -0
  10. package/dist/lib/components/FieldNode.js +96 -0
  11. package/dist/lib/components/RendererBody.d.ts +14 -0
  12. package/dist/lib/components/RendererBody.d.ts.map +1 -0
  13. package/dist/lib/components/RendererBody.js +45 -0
  14. package/dist/lib/components/index.d.ts +3 -0
  15. package/dist/lib/components/index.d.ts.map +1 -0
  16. package/dist/lib/components/index.js +2 -0
  17. package/dist/lib/hooks/index.d.ts +2 -0
  18. package/dist/lib/hooks/index.d.ts.map +1 -0
  19. package/dist/lib/hooks/index.js +1 -0
  20. package/dist/lib/hooks/useRendererInit.d.ts +12 -0
  21. package/dist/lib/hooks/useRendererInit.d.ts.map +1 -0
  22. package/dist/lib/hooks/useRendererInit.js +87 -0
  23. package/dist/lib/mcp/index.d.ts +7 -0
  24. package/dist/lib/mcp/index.d.ts.map +1 -0
  25. package/dist/lib/mcp/index.js +4 -0
  26. package/dist/lib/mcp/system-prompt.d.ts +2 -0
  27. package/dist/lib/mcp/system-prompt.d.ts.map +1 -0
  28. package/dist/lib/mcp/system-prompt.js +24 -0
  29. package/dist/lib/mcp/tool-definitions.d.ts +10 -0
  30. package/dist/lib/mcp/tool-definitions.d.ts.map +1 -0
  31. package/dist/lib/mcp/tool-definitions.js +66 -0
  32. package/dist/lib/mcp/tool-executor.d.ts +5 -0
  33. package/dist/lib/mcp/tool-executor.d.ts.map +1 -0
  34. package/dist/lib/mcp/tool-executor.js +55 -0
  35. package/dist/lib/mcp/useRendererToolBridge.d.ts +28 -0
  36. package/dist/lib/mcp/useRendererToolBridge.d.ts.map +1 -0
  37. package/dist/lib/mcp/useRendererToolBridge.js +53 -0
  38. package/dist/lib/register-defaults.d.ts +2 -0
  39. package/dist/lib/register-defaults.d.ts.map +1 -0
  40. package/dist/lib/register-defaults.js +36 -0
  41. package/dist/lib/renderer-tools.d.ts +56 -0
  42. package/dist/lib/renderer-tools.d.ts.map +1 -0
  43. package/dist/lib/renderer-tools.js +305 -0
  44. package/dist/lib/renderer.d.ts +28 -0
  45. package/dist/lib/renderer.d.ts.map +1 -0
  46. package/dist/lib/renderer.js +33 -0
  47. package/package.json +5 -5
@@ -0,0 +1,66 @@
1
+ export const RENDERER_TOOL_DEFINITIONS = [
2
+ {
3
+ type: 'function',
4
+ function: {
5
+ name: 'get_form',
6
+ description: 'Returns visible fields split into filled vs unfilled. Response: {formId, totalFields, filledCount, unfilledFields (full details), filledFieldIds (just IDs)}. Use unfilledFields to see what needs filling.\n\nFILL LOOP PATTERN:\n1. Call get_form — fill every field in unfilledFields using fill_field.\n2. Call get_form again. If new fields appeared in unfilledFields (revealed by conditional logic), fill those too.\n3. Repeat until unfilledFields is empty.\n\nNever assume you are done after a single pass — conditional logic frequently reveals new fields.',
7
+ parameters: { type: 'object', properties: {} },
8
+ },
9
+ },
10
+ {
11
+ type: 'function',
12
+ function: {
13
+ name: 'get_form_raw',
14
+ description: 'Returns ALL fields in the form regardless of visibility, conditional rules, or enabled state. Use this to see the full form schema.',
15
+ parameters: { type: 'object', properties: {} },
16
+ },
17
+ },
18
+ {
19
+ type: 'function',
20
+ function: {
21
+ name: 'get_responses',
22
+ description: 'Get the current raw response values for all fields.',
23
+ parameters: { type: 'object', properties: {} },
24
+ },
25
+ },
26
+ {
27
+ type: 'function',
28
+ function: {
29
+ name: 'get_valid_response',
30
+ description: 'Run form validation. Returns the full response object if all required fields pass, or null plus field-level errors if validation fails. Call this before submitting.',
31
+ parameters: { type: 'object', properties: {} },
32
+ },
33
+ },
34
+ {
35
+ type: 'function',
36
+ function: {
37
+ name: 'fill_field',
38
+ description: 'Set a response value for ONE visible field. Returns { result, filledCount, unfilledFields } where unfilledFields contains ONLY the fields still needing values (with full details including options/valueFormat). You are done when unfilledFields is empty.\n\nField type rules:\n- radio/dropdown/boolean/rating/slider: pass a single string matching one of the options values\n- check/multiselectdropdown: pass an array of strings matching option values\n- ranking: pass an ordered array of option value strings representing the desired rank order\n- multitext: pass an array of strings, one per option slot in order\n- singlematrix: pass an object { "Row Label": "Column Label" } for each row\n- multimatrix: pass an object { "Row Label": ["Col1", "Col2"] } for each row\n- text: use the valueFormat from the field\'s schema (e.g. YYYY-MM-DD for date, YYYY-MM-DDTHH:mm for datetime-local, YYYY-MM for month, HH:mm for time). Wrong formats are rejected with an error.',
39
+ parameters: {
40
+ type: 'object',
41
+ properties: {
42
+ fieldId: { type: 'string' },
43
+ fieldQuestion: { type: 'string' },
44
+ value: {},
45
+ },
46
+ required: ['value'],
47
+ },
48
+ },
49
+ },
50
+ {
51
+ type: 'function',
52
+ function: {
53
+ name: 'clear_responses',
54
+ description: 'Clear all current form responses.',
55
+ parameters: { type: 'object', properties: {} },
56
+ },
57
+ },
58
+ {
59
+ type: 'function',
60
+ function: {
61
+ name: 'get_form_tree',
62
+ description: 'Get the full rendered field tree including visibility, enabled, and required state for each field given current responses.',
63
+ parameters: { type: 'object', properties: {} },
64
+ },
65
+ },
66
+ ];
@@ -0,0 +1,5 @@
1
+ import type { RendererTools } from '../renderer-tools.js';
2
+ type ToolArgs = Record<string, unknown>;
3
+ export declare function executeToolCall(toolName: string, args: ToolArgs, tools: RendererTools): string | Record<string, unknown>;
4
+ export {};
5
+ //# sourceMappingURL=tool-executor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-executor.d.ts","sourceRoot":"","sources":["../../../src/lib/mcp/tool-executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAExC,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,aAAa,GACnB,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAoBlC"}
@@ -0,0 +1,55 @@
1
+ export function executeToolCall(toolName, args, tools) {
2
+ switch (toolName) {
3
+ case 'get_form':
4
+ return getFormOptimized(tools);
5
+ case 'get_form_raw':
6
+ return tools.getFormRaw();
7
+ case 'get_responses':
8
+ return tools.getResponses();
9
+ case 'get_valid_response':
10
+ return tools.getValidResponse();
11
+ case 'fill_field':
12
+ return fillField(args, tools);
13
+ case 'clear_responses':
14
+ tools.clearResponses();
15
+ return 'Responses cleared';
16
+ case 'get_form_tree':
17
+ return { fields: tools.getFormTree() };
18
+ default:
19
+ return `Unknown tool: ${toolName}`;
20
+ }
21
+ }
22
+ /** Optimized get_form: returns full details only for unfilled fields to reduce token usage. */
23
+ function getFormOptimized(tools) {
24
+ const form = tools.getForm();
25
+ const filledFields = form.fields.filter((f) => f.hasValue);
26
+ const unfilledFields = form.fields.filter((f) => !f.hasValue);
27
+ return {
28
+ formId: form.formId,
29
+ totalFields: form.fieldCount,
30
+ filledCount: filledFields.length,
31
+ // Only unfilled fields need full details (options, questions, etc.)
32
+ unfilledFields,
33
+ // Filled fields: just IDs for reference
34
+ filledFieldIds: filledFields.map((f) => f.id),
35
+ };
36
+ }
37
+ function fillField(args, tools) {
38
+ const fieldId = tools.resolveFieldId(args['fieldId'], args['fieldQuestion']);
39
+ if (!fieldId)
40
+ return 'Error: field not found — provide fieldId or fieldQuestion';
41
+ const result = tools.fillField(fieldId, args['value']);
42
+ if (typeof result === 'string')
43
+ return result; // format validation error
44
+ if (!result)
45
+ return `Error: field "${fieldId}" is not currently visible — call get_form to see the current visible fields`;
46
+ const form = tools.getForm();
47
+ // Return only unfilled fields with full details (token optimization)
48
+ const filledFields = form.fields.filter((f) => f.hasValue);
49
+ const unfilledFields = form.fields.filter((f) => !f.hasValue);
50
+ return {
51
+ result: `Field "${fieldId}" updated`,
52
+ filledCount: filledFields.length,
53
+ unfilledFields,
54
+ };
55
+ }
@@ -0,0 +1,28 @@
1
+ import type { RendererTools } from '../renderer-tools.js';
2
+ export interface UseRendererMcpToolHandlerOptions {
3
+ /**
4
+ * The DOM element to listen on for tool-call events.
5
+ * Pass `document` to listen globally, or a specific element to scope it.
6
+ */
7
+ target?: EventTarget;
8
+ /**
9
+ * The event name to listen for, e.g. `'ozwell-tool-call'`.
10
+ */
11
+ eventName: string;
12
+ }
13
+ /**
14
+ * Listens for AI tool-call events on a target element and dispatches them to
15
+ * the renderer's MCP tool executor. Returns the `onRendererToolsReady` callback
16
+ * to pass to `<EsheetRenderer>`.
17
+ *
18
+ * @example – default (listens on document)
19
+ * const onRendererToolsReady = useRendererMcpToolHandler({ eventName: 'ozwell-tool-call' });
20
+ * <EsheetRenderer onRendererToolsReady={onRendererToolsReady} ... />
21
+ *
22
+ * @example – scoped to a container element
23
+ * const ref = React.useRef<HTMLDivElement>(null);
24
+ * const onRendererToolsReady = useRendererMcpToolHandler({ target: ref.current ?? undefined, eventName: 'ozwell-tool-call' });
25
+ * <div ref={ref}><EsheetRenderer onRendererToolsReady={onRendererToolsReady} ... /></div>
26
+ */
27
+ export declare function useRendererMcpToolHandler(options: UseRendererMcpToolHandlerOptions): (tools: RendererTools) => void;
28
+ //# sourceMappingURL=useRendererToolBridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRendererToolBridge.d.ts","sourceRoot":"","sources":["../../../src/lib/mcp/useRendererToolBridge.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAG1D,MAAM,WAAW,gCAAgC;IAC/C;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,gCAAgC,GACxC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAgDhC"}
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import { executeToolCall } from './tool-executor.js';
3
+ /**
4
+ * Listens for AI tool-call events on a target element and dispatches them to
5
+ * the renderer's MCP tool executor. Returns the `onRendererToolsReady` callback
6
+ * to pass to `<EsheetRenderer>`.
7
+ *
8
+ * @example – default (listens on document)
9
+ * const onRendererToolsReady = useRendererMcpToolHandler({ eventName: 'ozwell-tool-call' });
10
+ * <EsheetRenderer onRendererToolsReady={onRendererToolsReady} ... />
11
+ *
12
+ * @example – scoped to a container element
13
+ * const ref = React.useRef<HTMLDivElement>(null);
14
+ * const onRendererToolsReady = useRendererMcpToolHandler({ target: ref.current ?? undefined, eventName: 'ozwell-tool-call' });
15
+ * <div ref={ref}><EsheetRenderer onRendererToolsReady={onRendererToolsReady} ... /></div>
16
+ */
17
+ export function useRendererMcpToolHandler(options) {
18
+ const { target, eventName } = options;
19
+ const toolsRef = React.useRef(null);
20
+ const onRendererToolsReady = React.useCallback((tools) => {
21
+ toolsRef.current = tools;
22
+ }, []);
23
+ React.useEffect(() => {
24
+ const el = target ?? document;
25
+ function onToolCall(e) {
26
+ const { name: rawName, arguments: args, respond, } = e.detail;
27
+ // Strip 'postMessage:' prefix if present (added by Ozwell widget internally)
28
+ const name = rawName.replace(/^postMessage:/, '');
29
+ if (!toolsRef.current) {
30
+ respond({ success: false, message: 'Renderer not ready' });
31
+ return;
32
+ }
33
+ const toResponse = (r) => typeof r === 'string' ? { result: r } : r;
34
+ try {
35
+ const result = executeToolCall(name, args, toolsRef.current);
36
+ if (result instanceof Promise) {
37
+ result
38
+ .then((r) => respond(toResponse(r)))
39
+ .catch((err) => respond({ error: String(err) }));
40
+ }
41
+ else {
42
+ respond(toResponse(result));
43
+ }
44
+ }
45
+ catch (err) {
46
+ respond({ error: String(err) });
47
+ }
48
+ }
49
+ el.addEventListener(eventName, onToolCall);
50
+ return () => el.removeEventListener(eventName, onToolCall);
51
+ }, [target, eventName]);
52
+ return onRendererToolsReady;
53
+ }
@@ -0,0 +1,2 @@
1
+ export declare function ensureDefaultFieldComponentsRegistered(): void;
2
+ //# sourceMappingURL=register-defaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-defaults.d.ts","sourceRoot":"","sources":["../../src/lib/register-defaults.ts"],"names":[],"mappings":"AAgCA,wBAAgB,sCAAsC,IAAI,IAAI,CA4B7D"}
@@ -0,0 +1,36 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Built-in field component registration for the renderer
3
+ // ---------------------------------------------------------------------------
4
+ // Called from EsheetRenderer runtime path so consumers get built-in defaults
5
+ // without relying on package entry side-effect imports.
6
+ // ---------------------------------------------------------------------------
7
+ import { registerFieldComponents } from '@esheet/fields';
8
+ import { TextField, LongTextField, MultiTextField, RadioField, CheckField, BooleanField, DropdownField, MultiSelectDropdownField, RatingField, RankingField, SliderField, SingleMatrixField, MultiMatrixField, SectionField, SignatureField, DiagramField, ImageField, HtmlField, DisplayField, } from '@esheet/fields';
9
+ let defaultsRegistered = false;
10
+ export function ensureDefaultFieldComponentsRegistered() {
11
+ if (defaultsRegistered) {
12
+ return;
13
+ }
14
+ registerFieldComponents({
15
+ text: TextField,
16
+ longtext: LongTextField,
17
+ multitext: MultiTextField,
18
+ radio: RadioField,
19
+ check: CheckField,
20
+ boolean: BooleanField,
21
+ dropdown: DropdownField,
22
+ multiselectdropdown: MultiSelectDropdownField,
23
+ rating: RatingField,
24
+ ranking: RankingField,
25
+ slider: SliderField,
26
+ singlematrix: SingleMatrixField,
27
+ multimatrix: MultiMatrixField,
28
+ section: SectionField,
29
+ signature: SignatureField,
30
+ diagram: DiagramField,
31
+ image: ImageField,
32
+ html: HtmlField,
33
+ display: DisplayField,
34
+ });
35
+ defaultsRegistered = true;
36
+ }
@@ -0,0 +1,56 @@
1
+ import { type FormStore, type ValidationError } from '@esheet/core';
2
+ import type { RenderFieldNode } from './renderer.js';
3
+ /** Summary of a single field, suitable for AI context. */
4
+ export interface RendererFieldSummary {
5
+ id: string;
6
+ fieldType: string;
7
+ /** For text fields: the HTML input type (e.g. 'date', 'datetime-local', 'month', 'time', 'email', 'number'). Use the correct format when filling this field. */
8
+ inputType?: string;
9
+ /** Required value format for structured input types. Use this exact format when calling fill_field. */
10
+ valueFormat?: string;
11
+ question: string | undefined;
12
+ required: boolean;
13
+ /** True if this field already has a response value. False means it is still empty and needs to be filled. */
14
+ hasValue: boolean;
15
+ /** Available option values for selection fields (radio, check, dropdown, etc.). Only present when the field has options. */
16
+ options?: string[];
17
+ /** Available row labels for matrix fields. Only present for singlematrix / multimatrix. */
18
+ rows?: string[];
19
+ /** Available column labels for matrix fields. Only present for singlematrix / multimatrix. */
20
+ columns?: string[];
21
+ }
22
+ /**
23
+ * Narrow interface exposed to MCP / AI tools via `onRendererToolsReady`.
24
+ * Read-oriented: inspect responses, fill responses, and query the render tree.
25
+ */
26
+ export interface RendererTools {
27
+ /** Currently visible, fillable fields with their available options. */
28
+ getForm: () => {
29
+ formId: string;
30
+ fieldCount: number;
31
+ fields: RendererFieldSummary[];
32
+ };
33
+ /** All fields in the form regardless of visibility, rules, or enabled state. */
34
+ getFormRaw: () => {
35
+ formId: string;
36
+ fieldCount: number;
37
+ fields: RendererFieldSummary[];
38
+ };
39
+ /** Get current raw responses keyed by field ID. */
40
+ getResponses: () => Record<string, unknown>;
41
+ /** Get validated responses — returns null if validation fails. */
42
+ getValidResponse: () => {
43
+ response: Record<string, unknown> | null;
44
+ errors: ValidationError[];
45
+ };
46
+ /** Set a response value for one visible field. Returns true on success, false if field not found/visible, or an error string if the value format is invalid. */
47
+ fillField: (fieldId: string, value: unknown) => boolean | string;
48
+ /** Clear all responses. */
49
+ clearResponses: () => void;
50
+ /** Full render tree with visibility/enabled/required per field. */
51
+ getFormTree: () => RenderFieldNode[];
52
+ /** Find a field ID by exact ID or partial question match. */
53
+ resolveFieldId: (fieldId?: string, fieldQuestion?: string) => string | undefined;
54
+ }
55
+ export declare function createRendererTools(formStore: FormStore): RendererTools;
56
+ //# sourceMappingURL=renderer-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer-tools.d.ts","sourceRoot":"","sources":["../../src/lib/renderer-tools.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,0DAA0D;AAC1D,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,gKAAgK;IAChK,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uGAAuG;IACvG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,QAAQ,EAAE,OAAO,CAAC;IAClB,6GAA6G;IAC7G,QAAQ,EAAE,OAAO,CAAC;IAClB,4HAA4H;IAC5H,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,2FAA2F;IAC3F,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,OAAO,EAAE,MAAM;QACb,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,oBAAoB,EAAE,CAAC;KAChC,CAAC;IACF,gFAAgF;IAChF,UAAU,EAAE,MAAM;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,oBAAoB,EAAE,CAAC;KAChC,CAAC;IACF,mDAAmD;IACnD,YAAY,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5C,kEAAkE;IAClE,gBAAgB,EAAE,MAAM;QACtB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QACzC,MAAM,EAAE,eAAe,EAAE,CAAC;KAC3B,CAAC;IACF,gKAAgK;IAChK,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,GAAG,MAAM,CAAC;IACjE,2BAA2B;IAC3B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,mEAAmE;IACnE,WAAW,EAAE,MAAM,eAAe,EAAE,CAAC;IACrC,6DAA6D;IAC7D,cAAc,EAAE,CACd,OAAO,CAAC,EAAE,MAAM,EAChB,aAAa,CAAC,EAAE,MAAM,KACnB,MAAM,GAAG,SAAS,CAAC;CACzB;AAkBD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,SAAS,GAAG,aAAa,CA4XvE"}
@@ -0,0 +1,305 @@
1
+ // ---------------------------------------------------------------------------
2
+ // RendererTools — narrow facade for MCP / AI tool callers only.
3
+ // ---------------------------------------------------------------------------
4
+ import { validateForm, } from '@esheet/core';
5
+ import { renderer } from './renderer.js';
6
+ const NON_INPUT = new Set(['section', 'html', 'display', 'image', 'diagram']);
7
+ /** Flatten the render tree into only visible, fillable leaf nodes. */
8
+ function flattenVisible(nodes) {
9
+ const result = [];
10
+ for (const node of nodes) {
11
+ if (!node.visible)
12
+ continue;
13
+ if (node.children.length > 0) {
14
+ result.push(...flattenVisible(node.children));
15
+ }
16
+ else if (!NON_INPUT.has(node.definition.fieldType)) {
17
+ result.push(node);
18
+ }
19
+ }
20
+ return result;
21
+ }
22
+ export function createRendererTools(formStore) {
23
+ function resolveFieldId(fieldId, fieldQuestion) {
24
+ if (fieldId)
25
+ return fieldId;
26
+ if (fieldQuestion) {
27
+ const q = fieldQuestion.toLowerCase();
28
+ const { normalized } = formStore.getState();
29
+ for (const id of Object.keys(normalized.byId)) {
30
+ const question = normalized.byId[id].definition.question;
31
+ if (question?.toLowerCase().includes(q))
32
+ return id;
33
+ }
34
+ }
35
+ return undefined;
36
+ }
37
+ function getCurrentRenderTree() {
38
+ const { formId, responses } = formStore.getState();
39
+ const definition = formStore.getState().hydrateDefinition();
40
+ if (!definition)
41
+ return [];
42
+ return renderer({ ...definition, id: formId }, responses);
43
+ }
44
+ const INPUT_TYPE_FORMAT = {
45
+ date: 'YYYY-MM-DD',
46
+ 'datetime-local': 'YYYY-MM-DDTHH:mm',
47
+ month: 'YYYY-MM',
48
+ time: 'HH:mm',
49
+ };
50
+ function toFieldSummary(id, def, required) {
51
+ const response = formStore.getState().responses[id];
52
+ const hasValue = response != null &&
53
+ Object.values(response).some((v) => v != null &&
54
+ v !== '' &&
55
+ !(Array.isArray(v) && v.length === 0) &&
56
+ !(typeof v === 'object' &&
57
+ !Array.isArray(v) &&
58
+ Object.keys(v).length === 0));
59
+ const summary = {
60
+ id,
61
+ fieldType: def.fieldType,
62
+ question: def.question,
63
+ required: required ?? def.required ?? false,
64
+ hasValue,
65
+ };
66
+ if (def.inputType)
67
+ summary.inputType = def.inputType;
68
+ if (def.inputType && INPUT_TYPE_FORMAT[def.inputType])
69
+ summary.valueFormat = INPUT_TYPE_FORMAT[def.inputType];
70
+ if (def.options && def.options.length > 0)
71
+ summary.options = def.options.map((o) => o.value);
72
+ if (def.rows && def.rows.length > 0)
73
+ summary.rows = def.rows.map((r) => r.value);
74
+ if (def.columns && def.columns.length > 0)
75
+ summary.columns = def.columns.map((c) => c.value);
76
+ return summary;
77
+ }
78
+ /**
79
+ * Normalize and validate a text value for a given inputType.
80
+ * Returns the normalized string on success, or null if the value cannot
81
+ * be coerced into a valid format (caller should reject the fill).
82
+ */
83
+ function normalizeTextValue(value, inputType) {
84
+ if (!value)
85
+ return value;
86
+ switch (inputType) {
87
+ case 'date': {
88
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value))
89
+ return value;
90
+ if (/^\d{4}-\d{2}$/.test(value))
91
+ return `${value}-01`;
92
+ if (/^\d{4}$/.test(value))
93
+ return `${value}-01-01`;
94
+ return null; // not a recognisable date
95
+ }
96
+ case 'month': {
97
+ if (/^\d{4}-\d{2}$/.test(value))
98
+ return value;
99
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value))
100
+ return value.slice(0, 7);
101
+ return null;
102
+ }
103
+ case 'datetime-local': {
104
+ const n = value.replace(' ', 'T');
105
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(n))
106
+ return n;
107
+ if (/^\d{4}-\d{2}-\d{2}$/.test(n))
108
+ return `${n}T00:00`;
109
+ return null;
110
+ }
111
+ case 'time': {
112
+ if (/^\d{2}:\d{2}$/.test(value))
113
+ return value;
114
+ const match = value.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)$/i);
115
+ if (match) {
116
+ let h = parseInt(match[1], 10);
117
+ const m = match[2];
118
+ const period = match[3].toUpperCase();
119
+ if (period === 'AM' && h === 12)
120
+ h = 0;
121
+ if (period === 'PM' && h !== 12)
122
+ h += 12;
123
+ return `${String(h).padStart(2, '0')}:${m}`;
124
+ }
125
+ return null;
126
+ }
127
+ default:
128
+ return value;
129
+ }
130
+ }
131
+ function applyFieldValue(fieldId, value, checkVisibility) {
132
+ const { normalized } = formStore.getState();
133
+ const node = normalized.byId[fieldId];
134
+ if (!node)
135
+ return false;
136
+ if (checkVisibility) {
137
+ const visibleIds = new Set(flattenVisible(getCurrentRenderTree()).map((n) => n.id));
138
+ if (!visibleIds.has(fieldId))
139
+ return false;
140
+ }
141
+ const def = node.definition;
142
+ let response;
143
+ if (def.fieldType === 'singlematrix' &&
144
+ value !== null &&
145
+ typeof value === 'object') {
146
+ const rows = def.rows ?? [];
147
+ const columns = def.columns ?? [];
148
+ const existing = (formStore.getState().responses[fieldId]?.selected ??
149
+ {});
150
+ const selected = {
151
+ ...existing,
152
+ };
153
+ const entries = Array.isArray(value)
154
+ ? value.map((v, i) => [rows[i]?.value ?? String(i), v])
155
+ : Object.entries(value);
156
+ for (const [rowKey, colVal] of entries) {
157
+ const row = rows.find((r) => r.value.toLowerCase() === rowKey.toLowerCase() || r.id === rowKey);
158
+ if (!row)
159
+ continue;
160
+ const colStr = String(colVal);
161
+ const col = columns.find((c) => c.value.toLowerCase() === colStr.toLowerCase() || c.id === colStr) ?? columns[parseInt(colStr, 10)];
162
+ if (col)
163
+ selected[row.id] = { id: col.id, value: col.value };
164
+ }
165
+ response = { selected };
166
+ }
167
+ else if (def.fieldType === 'multimatrix' &&
168
+ value !== null &&
169
+ typeof value === 'object') {
170
+ // multimatrix: { selected: Record<rowId, SelectedOption[]> }
171
+ const rows = def.rows ?? [];
172
+ const columns = def.columns ?? [];
173
+ const existing = (formStore.getState().responses[fieldId]?.selected ??
174
+ {});
175
+ const selected = {
176
+ ...existing,
177
+ };
178
+ const entries = Array.isArray(value)
179
+ ? value.map((v, i) => [rows[i]?.value ?? String(i), v])
180
+ : Object.entries(value);
181
+ for (const [rowKey, colVals] of entries) {
182
+ const row = rows.find((r) => r.value.toLowerCase() === rowKey.toLowerCase() || r.id === rowKey);
183
+ if (!row)
184
+ continue;
185
+ const colValArr = Array.isArray(colVals)
186
+ ? colVals
187
+ : [colVals];
188
+ const matched = colValArr
189
+ .map((v) => {
190
+ const s = String(v);
191
+ return columns.find((c) => c.value.toLowerCase() === s.toLowerCase() || c.id === s);
192
+ })
193
+ .filter((c) => c != null);
194
+ if (matched.length > 0)
195
+ selected[row.id] = matched;
196
+ }
197
+ response = { selected };
198
+ }
199
+ else if (['radio', 'dropdown', 'boolean', 'rating', 'slider'].includes(def.fieldType)) {
200
+ const opts = def.options ?? [];
201
+ const match = opts.find((o) => o.value.toLowerCase() === String(value).toLowerCase() ||
202
+ o.id === value);
203
+ // For slider/rating, also try matching by numeric index or option value as number
204
+ const numVal = Number(value);
205
+ const numMatch = !match && !isNaN(numVal) ? opts[numVal - 1] ?? opts[numVal] : undefined;
206
+ const resolved = match ?? numMatch;
207
+ response = resolved
208
+ ? { selected: { id: resolved.id, value: resolved.value } }
209
+ : { selected: undefined };
210
+ }
211
+ else if (def.fieldType === 'check' ||
212
+ def.fieldType === 'multiselectdropdown') {
213
+ const opts = def.options ?? [];
214
+ const vals = Array.isArray(value) ? value : [value];
215
+ const matches = opts.filter((o) => vals.some((v) => o.value.toLowerCase() === String(v).toLowerCase() || o.id === v));
216
+ response = {
217
+ selected: matches.map((o) => ({ id: o.id, value: o.value })),
218
+ };
219
+ }
220
+ else if (def.fieldType === 'ranking') {
221
+ // ranking: { selected: SelectedOption[] } in the user-specified order
222
+ const opts = def.options ?? [];
223
+ const vals = Array.isArray(value) ? value : [value];
224
+ const ordered = vals
225
+ .map((v) => opts.find((o) => o.value.toLowerCase() === String(v).toLowerCase() || o.id === v))
226
+ .filter((o) => o != null);
227
+ // append any options not mentioned by the user at the end
228
+ const mentioned = new Set(ordered.map((o) => o.id));
229
+ const remaining = opts.filter((o) => !mentioned.has(o.id));
230
+ response = { selected: [...ordered, ...remaining] };
231
+ }
232
+ else if (def.fieldType === 'multitext') {
233
+ // multitext: { multitextAnswers: Record<optionId, string> }
234
+ const opts = def.options ?? [];
235
+ const vals = Array.isArray(value)
236
+ ? value
237
+ : typeof value === 'object' && value !== null
238
+ ? Object.values(value)
239
+ : [value];
240
+ const multitextAnswers = {};
241
+ opts.forEach((opt, i) => {
242
+ if (vals[i] != null)
243
+ multitextAnswers[opt.id] = String(vals[i]);
244
+ });
245
+ response = { multitextAnswers };
246
+ }
247
+ else {
248
+ if (value == null) {
249
+ response = { answer: undefined };
250
+ }
251
+ else {
252
+ const normalized = normalizeTextValue(String(value), def.inputType);
253
+ if (normalized === null) {
254
+ const fmt = {
255
+ date: 'YYYY-MM-DD',
256
+ 'datetime-local': 'YYYY-MM-DDTHH:mm',
257
+ month: 'YYYY-MM',
258
+ time: 'HH:mm',
259
+ };
260
+ const expected = fmt[def.inputType ?? ''];
261
+ return expected
262
+ ? `Error: invalid value for inputType "${def.inputType}" — expected format ${expected} (e.g. ${new Date()
263
+ .toISOString()
264
+ .slice(0, expected.length)
265
+ .replace('T', 'T')
266
+ .replace(/[^\dT:-]/g, '0')})`
267
+ : `Error: invalid value for field`;
268
+ }
269
+ response = { answer: normalized };
270
+ }
271
+ }
272
+ formStore.getState().setResponse(fieldId, response);
273
+ return true;
274
+ }
275
+ return {
276
+ getForm: () => {
277
+ const { formId } = formStore.getState();
278
+ const visibleNodes = flattenVisible(getCurrentRenderTree());
279
+ const fields = visibleNodes.map((node) => toFieldSummary(node.id, node.definition, node.required));
280
+ return { formId, fieldCount: fields.length, fields };
281
+ },
282
+ getFormRaw: () => {
283
+ const { normalized, formId } = formStore.getState();
284
+ const fields = Object.keys(normalized.byId)
285
+ .filter((id) => !NON_INPUT.has(normalized.byId[id].definition.fieldType))
286
+ .map((id) => toFieldSummary(id, normalized.byId[id].definition));
287
+ return { formId, fieldCount: fields.length, fields };
288
+ },
289
+ getResponses: () => formStore.getState().responses,
290
+ getValidResponse: () => {
291
+ const state = formStore.getState();
292
+ const errors = validateForm(state.normalized, state.responses);
293
+ return {
294
+ response: errors.length === 0
295
+ ? state.responses
296
+ : null,
297
+ errors,
298
+ };
299
+ },
300
+ fillField: (fieldId, value) => applyFieldValue(fieldId, value, true),
301
+ clearResponses: () => formStore.getState().resetResponses(),
302
+ getFormTree: () => getCurrentRenderTree(),
303
+ resolveFieldId,
304
+ };
305
+ }
@@ -0,0 +1,28 @@
1
+ import { type FieldDefinition, type FormDefinition, type FormResponse } from '@esheet/core';
2
+ export interface RenderTreeOptions {
3
+ /** Include fields that resolve to invisible (default: false). */
4
+ includeHidden?: boolean;
5
+ }
6
+ export interface RenderFieldNode {
7
+ /** Field identifier. */
8
+ id: string;
9
+ /** Field definition without nested children. */
10
+ definition: Omit<FieldDefinition, 'fields'>;
11
+ /** Computed visible state. */
12
+ visible: boolean;
13
+ /** Computed enabled state. */
14
+ enabled: boolean;
15
+ /** Computed required state. */
16
+ required: boolean;
17
+ /** Renderable child nodes (sections only). */
18
+ children: RenderFieldNode[];
19
+ }
20
+ /**
21
+ * Build a render tree from form definition + responses.
22
+ *
23
+ * Sections are represented as nodes with nested `children`.
24
+ */
25
+ export declare function renderer(definition: FormDefinition, responses?: FormResponse, options?: RenderTreeOptions): RenderFieldNode[];
26
+ /** Alias for readability in consumers that prefer explicit naming. */
27
+ export declare const buildRenderTree: typeof renderer;
28
+ //# sourceMappingURL=renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/lib/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,iBAAiB;IAChC,iEAAiE;IACjE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,gDAAgD;IAChD,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAC;IAC5C,8BAA8B;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,8BAA8B;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,+BAA+B;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,8CAA8C;IAC9C,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CACtB,UAAU,EAAE,cAAc,EAC1B,SAAS,GAAE,YAAiB,EAC5B,OAAO,GAAE,iBAAsB,GAC9B,eAAe,EAAE,CA2CnB;AAED,sEAAsE;AACtE,eAAO,MAAM,eAAe,iBAAW,CAAC"}