@esheet/renderer 0.0.4-1 → 0.0.4-2

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 (33) hide show
  1. package/dist/index.d.ts +2 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +5240 -4197
  4. package/dist/lib/EsheetRenderer.d.ts +17 -4
  5. package/dist/lib/EsheetRenderer.d.ts.map +1 -1
  6. package/dist/lib/components/FieldNode.d.ts +2 -2
  7. package/dist/lib/components/FieldNode.d.ts.map +1 -1
  8. package/dist/lib/components/RendererBody.d.ts +1 -1
  9. package/dist/lib/hooks/useRendererInit.d.ts +2 -2
  10. package/dist/lib/hooks/useRendererInit.d.ts.map +1 -1
  11. package/dist/lib/mcp/tool-definitions.d.ts.map +1 -1
  12. package/dist/lib/mcp/tool-executor.d.ts +1 -1
  13. package/dist/lib/mcp/tool-executor.d.ts.map +1 -1
  14. package/dist/lib/mcp/useRendererToolBridge.d.ts +1 -1
  15. package/dist/lib/register-defaults.d.ts.map +1 -1
  16. package/dist/lib/renderer-tools.d.ts +2 -2
  17. package/dist/lib/renderer.d.ts +22 -1
  18. package/dist/lib/renderer.d.ts.map +1 -1
  19. package/package.json +5 -5
  20. package/dist/lib/EsheetRenderer.js +0 -115
  21. package/dist/lib/components/FieldNode.js +0 -96
  22. package/dist/lib/components/RendererBody.js +0 -45
  23. package/dist/lib/components/index.js +0 -2
  24. package/dist/lib/hooks/index.js +0 -1
  25. package/dist/lib/hooks/useRendererInit.js +0 -87
  26. package/dist/lib/mcp/index.js +0 -4
  27. package/dist/lib/mcp/system-prompt.js +0 -24
  28. package/dist/lib/mcp/tool-definitions.js +0 -66
  29. package/dist/lib/mcp/tool-executor.js +0 -55
  30. package/dist/lib/mcp/useRendererToolBridge.js +0 -53
  31. package/dist/lib/register-defaults.js +0 -36
  32. package/dist/lib/renderer-tools.js +0 -305
  33. package/dist/lib/renderer.js +0 -33
@@ -1,66 +0,0 @@
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
- ];
@@ -1,55 +0,0 @@
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
- }
@@ -1,53 +0,0 @@
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
- }
@@ -1,36 +0,0 @@
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
- }
@@ -1,305 +0,0 @@
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
- }
@@ -1,33 +0,0 @@
1
- import { normalizeDefinition, resolveEffect, } from '@esheet/core';
2
- /**
3
- * Build a render tree from form definition + responses.
4
- *
5
- * Sections are represented as nodes with nested `children`.
6
- */
7
- export function renderer(definition, responses = {}, options = {}) {
8
- const normalized = normalizeDefinition(definition.fields);
9
- const includeHidden = options.includeHidden === true;
10
- function build(ids) {
11
- const result = [];
12
- for (const id of ids) {
13
- const node = normalized.byId[id];
14
- if (!node)
15
- continue;
16
- const visible = resolveEffect('visible', node.definition, normalized, responses);
17
- if (!visible && !includeHidden)
18
- continue;
19
- result.push({
20
- id,
21
- definition: node.definition,
22
- visible,
23
- enabled: resolveEffect('enable', node.definition, normalized, responses),
24
- required: resolveEffect('required', node.definition, normalized, responses),
25
- children: build(node.childIds),
26
- });
27
- }
28
- return result;
29
- }
30
- return build(normalized.rootIds);
31
- }
32
- /** Alias for readability in consumers that prefer explicit naming. */
33
- export const buildRenderTree = renderer;