@vertesia/fusion-ux 1.2.0 → 1.4.0-dev.20260614.160504Z

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 (126) hide show
  1. package/LICENSE +198 -10
  2. package/lib/fusion-fragment/ChartRenderer.d.ts.map +1 -0
  3. package/lib/{esm/fusion-fragment → fusion-fragment}/ChartRenderer.js +8 -6
  4. package/lib/fusion-fragment/ChartRenderer.js.map +1 -0
  5. package/lib/{types/fusion-fragment → fusion-fragment}/FieldRenderer.d.ts +1 -1
  6. package/lib/fusion-fragment/FieldRenderer.d.ts.map +1 -0
  7. package/lib/{esm/fusion-fragment → fusion-fragment}/FieldRenderer.js +14 -8
  8. package/lib/fusion-fragment/FieldRenderer.js.map +1 -0
  9. package/lib/{types/fusion-fragment → fusion-fragment}/FusionFragmentContext.d.ts +3 -3
  10. package/lib/fusion-fragment/FusionFragmentContext.d.ts.map +1 -0
  11. package/lib/{esm/fusion-fragment → fusion-fragment}/FusionFragmentContext.js +2 -2
  12. package/lib/fusion-fragment/FusionFragmentContext.js.map +1 -0
  13. package/lib/fusion-fragment/FusionFragmentHandler.d.ts.map +1 -0
  14. package/lib/{esm/fusion-fragment → fusion-fragment}/FusionFragmentHandler.js +7 -14
  15. package/lib/fusion-fragment/FusionFragmentHandler.js.map +1 -0
  16. package/lib/fusion-fragment/FusionFragmentRenderer.d.ts.map +1 -0
  17. package/lib/{esm/fusion-fragment → fusion-fragment}/FusionFragmentRenderer.js +2 -2
  18. package/lib/fusion-fragment/FusionFragmentRenderer.js.map +1 -0
  19. package/lib/{types/fusion-fragment → fusion-fragment}/SectionRenderer.d.ts +1 -1
  20. package/lib/fusion-fragment/SectionRenderer.d.ts.map +1 -0
  21. package/lib/{esm/fusion-fragment → fusion-fragment}/SectionRenderer.js +12 -5
  22. package/lib/fusion-fragment/SectionRenderer.js.map +1 -0
  23. package/lib/fusion-fragment/TableRenderer.d.ts.map +1 -0
  24. package/lib/{esm/fusion-fragment → fusion-fragment}/TableRenderer.js +9 -7
  25. package/lib/fusion-fragment/TableRenderer.js.map +1 -0
  26. package/lib/{types/fusion-fragment → fusion-fragment}/index.d.ts +4 -4
  27. package/lib/fusion-fragment/index.d.ts.map +1 -0
  28. package/lib/{esm/fusion-fragment → fusion-fragment}/index.js +4 -4
  29. package/lib/fusion-fragment/index.js.map +1 -0
  30. package/lib/{types/index.d.ts → index.d.ts} +4 -4
  31. package/lib/index.d.ts.map +1 -0
  32. package/lib/{esm/index.js → index.js} +4 -4
  33. package/lib/index.js.map +1 -0
  34. package/lib/{types/render → render}/index.d.ts +1 -1
  35. package/lib/render/index.d.ts.map +1 -0
  36. package/lib/{esm/render → render}/index.js +1 -1
  37. package/lib/render/index.js.map +1 -0
  38. package/lib/render/textPreview.d.ts.map +1 -0
  39. package/lib/{esm/render → render}/textPreview.js +12 -9
  40. package/lib/render/textPreview.js.map +1 -0
  41. package/lib/types.d.ts.map +1 -0
  42. package/lib/types.js.map +1 -0
  43. package/lib/validation/formatErrors.d.ts.map +1 -0
  44. package/lib/{esm/validation → validation}/formatErrors.js +1 -1
  45. package/lib/validation/formatErrors.js.map +1 -0
  46. package/lib/validation/fuzzyMatch.d.ts.map +1 -0
  47. package/lib/{esm/validation → validation}/fuzzyMatch.js +3 -4
  48. package/lib/validation/fuzzyMatch.js.map +1 -0
  49. package/lib/validation/index.d.ts +8 -0
  50. package/lib/validation/index.d.ts.map +1 -0
  51. package/lib/validation/index.js +8 -0
  52. package/lib/validation/index.js.map +1 -0
  53. package/lib/{types/validation → validation}/schemas.d.ts +1 -1
  54. package/lib/validation/schemas.d.ts.map +1 -0
  55. package/lib/{esm/validation → validation}/schemas.js +25 -25
  56. package/lib/validation/schemas.js.map +1 -0
  57. package/lib/validation/validateTemplate.d.ts.map +1 -0
  58. package/lib/{esm/validation → validation}/validateTemplate.js +23 -21
  59. package/lib/validation/validateTemplate.js.map +1 -0
  60. package/lib/vertesia-fusion-ux.js +1 -1
  61. package/lib/vertesia-fusion-ux.js.map +1 -1
  62. package/package.json +23 -35
  63. package/src/fusion-fragment/ChartRenderer.tsx +95 -96
  64. package/src/fusion-fragment/FieldRenderer.tsx +173 -174
  65. package/src/fusion-fragment/FusionFragmentContext.tsx +31 -37
  66. package/src/fusion-fragment/FusionFragmentHandler.tsx +214 -223
  67. package/src/fusion-fragment/FusionFragmentRenderer.tsx +102 -107
  68. package/src/fusion-fragment/SectionRenderer.tsx +174 -169
  69. package/src/fusion-fragment/TableRenderer.tsx +175 -171
  70. package/src/fusion-fragment/index.ts +11 -11
  71. package/src/index.ts +42 -45
  72. package/src/render/index.ts +3 -3
  73. package/src/render/textPreview.ts +183 -186
  74. package/src/types.ts +174 -174
  75. package/src/validation/formatErrors.ts +86 -86
  76. package/src/validation/fuzzyMatch.ts +69 -77
  77. package/src/validation/index.ts +3 -3
  78. package/src/validation/schemas.ts +120 -120
  79. package/src/validation/validateTemplate.ts +225 -226
  80. package/lib/esm/fusion-fragment/ChartRenderer.js.map +0 -1
  81. package/lib/esm/fusion-fragment/FieldRenderer.js.map +0 -1
  82. package/lib/esm/fusion-fragment/FusionFragmentContext.js.map +0 -1
  83. package/lib/esm/fusion-fragment/FusionFragmentHandler.js.map +0 -1
  84. package/lib/esm/fusion-fragment/FusionFragmentRenderer.js.map +0 -1
  85. package/lib/esm/fusion-fragment/SectionRenderer.js.map +0 -1
  86. package/lib/esm/fusion-fragment/TableRenderer.js.map +0 -1
  87. package/lib/esm/fusion-fragment/index.js.map +0 -1
  88. package/lib/esm/index.js.map +0 -1
  89. package/lib/esm/render/index.js.map +0 -1
  90. package/lib/esm/render/textPreview.js.map +0 -1
  91. package/lib/esm/types.js.map +0 -1
  92. package/lib/esm/validation/formatErrors.js.map +0 -1
  93. package/lib/esm/validation/fuzzyMatch.js.map +0 -1
  94. package/lib/esm/validation/index.js +0 -8
  95. package/lib/esm/validation/index.js.map +0 -1
  96. package/lib/esm/validation/schemas.js.map +0 -1
  97. package/lib/esm/validation/validateTemplate.js.map +0 -1
  98. package/lib/tsconfig.tsbuildinfo +0 -1
  99. package/lib/types/fusion-fragment/ChartRenderer.d.ts.map +0 -1
  100. package/lib/types/fusion-fragment/FieldRenderer.d.ts.map +0 -1
  101. package/lib/types/fusion-fragment/FusionFragmentContext.d.ts.map +0 -1
  102. package/lib/types/fusion-fragment/FusionFragmentHandler.d.ts.map +0 -1
  103. package/lib/types/fusion-fragment/FusionFragmentRenderer.d.ts.map +0 -1
  104. package/lib/types/fusion-fragment/SectionRenderer.d.ts.map +0 -1
  105. package/lib/types/fusion-fragment/TableRenderer.d.ts.map +0 -1
  106. package/lib/types/fusion-fragment/index.d.ts.map +0 -1
  107. package/lib/types/index.d.ts.map +0 -1
  108. package/lib/types/render/index.d.ts.map +0 -1
  109. package/lib/types/render/textPreview.d.ts.map +0 -1
  110. package/lib/types/types.d.ts.map +0 -1
  111. package/lib/types/validation/formatErrors.d.ts.map +0 -1
  112. package/lib/types/validation/fuzzyMatch.d.ts.map +0 -1
  113. package/lib/types/validation/index.d.ts +0 -8
  114. package/lib/types/validation/index.d.ts.map +0 -1
  115. package/lib/types/validation/schemas.d.ts.map +0 -1
  116. package/lib/types/validation/validateTemplate.d.ts.map +0 -1
  117. /package/lib/{types/fusion-fragment → fusion-fragment}/ChartRenderer.d.ts +0 -0
  118. /package/lib/{types/fusion-fragment → fusion-fragment}/FusionFragmentHandler.d.ts +0 -0
  119. /package/lib/{types/fusion-fragment → fusion-fragment}/FusionFragmentRenderer.d.ts +0 -0
  120. /package/lib/{types/fusion-fragment → fusion-fragment}/TableRenderer.d.ts +0 -0
  121. /package/lib/{types/render → render}/textPreview.d.ts +0 -0
  122. /package/lib/{types/types.d.ts → types.d.ts} +0 -0
  123. /package/lib/{esm/types.js → types.js} +0 -0
  124. /package/lib/{types/validation → validation}/formatErrors.d.ts +0 -0
  125. /package/lib/{types/validation → validation}/fuzzyMatch.d.ts +0 -0
  126. /package/lib/{types/validation → validation}/validateTemplate.d.ts +0 -0
@@ -3,91 +3,90 @@
3
3
  * Renders a model-generated template with actual data values
4
4
  */
5
5
 
6
- import { useMemo, type ReactElement } from 'react';
6
+ import { type ReactElement, useMemo } from 'react';
7
7
  import type { FusionFragmentRendererProps, ValidationError } from '../types.js';
8
8
  import { validateTemplate } from '../validation/validateTemplate.js';
9
9
  import { SectionRenderer } from './SectionRenderer.js';
10
10
 
11
11
  const styles = {
12
- container: {
13
- backgroundColor: 'var(--gray-2, #f9fafb)',
14
- border: '1px solid var(--gray-5, #e5e7eb)',
15
- borderRadius: '8px',
16
- padding: '16px 20px',
17
- },
18
- title: {
19
- fontSize: '16px',
20
- fontWeight: 600,
21
- color: 'var(--gray-12, #1f2937)',
22
- marginBottom: '16px',
23
- paddingBottom: '12px',
24
- borderBottom: '1px solid var(--gray-5, #e5e7eb)',
25
- },
26
- footer: {
27
- fontSize: '12px',
28
- color: 'var(--gray-10, #9ca3af)',
29
- marginTop: '16px',
30
- paddingTop: '12px',
31
- borderTop: '1px solid var(--gray-5, #e5e7eb)',
32
- },
33
- error: {
34
- backgroundColor: 'var(--red-2, #fef2f2)',
35
- border: '1px solid var(--red-6, #fca5a5)',
36
- borderRadius: '8px',
37
- padding: '16px',
38
- },
39
- errorTitle: {
40
- fontSize: '14px',
41
- fontWeight: 600,
42
- color: 'var(--red-11, #dc2626)',
43
- marginBottom: '12px',
44
- },
45
- errorList: {
46
- listStyle: 'none',
47
- padding: 0,
48
- margin: 0,
49
- },
50
- errorItem: {
51
- fontSize: '13px',
52
- color: 'var(--red-11, #dc2626)',
53
- marginBottom: '8px',
54
- fontFamily: 'var(--font-mono, ui-monospace, monospace)',
55
- },
56
- errorPath: {
57
- fontSize: '11px',
58
- color: 'var(--red-9, #ef4444)',
59
- display: 'block',
60
- },
61
- errorSuggestion: {
62
- fontSize: '12px',
63
- color: 'var(--gray-11, #6b7280)',
64
- marginTop: '4px',
65
- fontStyle: 'italic',
66
- },
12
+ container: {
13
+ backgroundColor: 'var(--gray-2, #f9fafb)',
14
+ border: '1px solid var(--gray-5, #e5e7eb)',
15
+ borderRadius: '8px',
16
+ padding: '16px 20px',
17
+ },
18
+ title: {
19
+ fontSize: '16px',
20
+ fontWeight: 600,
21
+ color: 'var(--gray-12, #1f2937)',
22
+ marginBottom: '16px',
23
+ paddingBottom: '12px',
24
+ borderBottom: '1px solid var(--gray-5, #e5e7eb)',
25
+ },
26
+ footer: {
27
+ fontSize: '12px',
28
+ color: 'var(--gray-10, #9ca3af)',
29
+ marginTop: '16px',
30
+ paddingTop: '12px',
31
+ borderTop: '1px solid var(--gray-5, #e5e7eb)',
32
+ },
33
+ error: {
34
+ backgroundColor: 'var(--red-2, #fef2f2)',
35
+ border: '1px solid var(--red-6, #fca5a5)',
36
+ borderRadius: '8px',
37
+ padding: '16px',
38
+ },
39
+ errorTitle: {
40
+ fontSize: '14px',
41
+ fontWeight: 600,
42
+ color: 'var(--red-11, #dc2626)',
43
+ marginBottom: '12px',
44
+ },
45
+ errorList: {
46
+ listStyle: 'none',
47
+ padding: 0,
48
+ margin: 0,
49
+ },
50
+ errorItem: {
51
+ fontSize: '13px',
52
+ color: 'var(--red-11, #dc2626)',
53
+ marginBottom: '8px',
54
+ fontFamily: 'var(--font-mono, ui-monospace, monospace)',
55
+ },
56
+ errorPath: {
57
+ fontSize: '11px',
58
+ color: 'var(--red-9, #ef4444)',
59
+ display: 'block',
60
+ },
61
+ errorSuggestion: {
62
+ fontSize: '12px',
63
+ color: 'var(--gray-11, #6b7280)',
64
+ marginTop: '4px',
65
+ fontStyle: 'italic',
66
+ },
67
67
  };
68
68
 
69
69
  /**
70
70
  * Component to display validation errors
71
71
  */
72
72
  function ValidationErrors({ errors }: { errors: ValidationError[] }): ReactElement {
73
- return (
74
- <div style={styles.error}>
75
- <div style={styles.errorTitle}>
76
- Template Validation Failed ({errors.length} error{errors.length > 1 ? 's' : ''})
77
- </div>
78
- <ul style={styles.errorList}>
79
- {errors.map((error, index) => (
80
- <li key={index} style={styles.errorItem}>
81
- <span>{error.message}</span>
82
- <span style={styles.errorPath}>at {error.path}</span>
83
- {error.suggestion && (
84
- <span style={styles.errorSuggestion}>\u2192 {error.suggestion}</span>
85
- )}
86
- </li>
87
- ))}
88
- </ul>
89
- </div>
90
- );
73
+ return (
74
+ <div style={styles.error}>
75
+ <div style={styles.errorTitle}>
76
+ Template Validation Failed ({errors.length} error{errors.length > 1 ? 's' : ''})
77
+ </div>
78
+ <ul style={styles.errorList}>
79
+ {errors.map((error, index) => (
80
+ // biome-ignore lint/suspicious/noArrayIndexKey: list order is stable for this render
81
+ <li key={index} style={styles.errorItem}>
82
+ <span>{error.message}</span>
83
+ <span style={styles.errorPath}>at {error.path}</span>
84
+ {error.suggestion && <span style={styles.errorSuggestion}>\u2192 {error.suggestion}</span>}
85
+ </li>
86
+ ))}
87
+ </ul>
88
+ </div>
89
+ );
91
90
  }
92
91
 
93
92
  /**
@@ -115,42 +114,38 @@ function ValidationErrors({ errors }: { errors: ValidationError[] }): ReactEleme
115
114
  * ```
116
115
  */
117
116
  export function FusionFragmentRenderer({
118
- template,
119
- data,
120
- onUpdate,
121
- agentMode,
122
- className,
117
+ template,
118
+ data,
119
+ onUpdate,
120
+ agentMode,
121
+ className,
123
122
  }: FusionFragmentRendererProps): ReactElement {
124
- // Validate template against data keys
125
- const validation = useMemo(() => {
126
- const dataKeys = Object.keys(data);
127
- return validateTemplate(template, dataKeys);
128
- }, [template, data]);
123
+ // Validate template against data keys
124
+ const validation = useMemo(() => {
125
+ const dataKeys = Object.keys(data);
126
+ return validateTemplate(template, dataKeys);
127
+ }, [template, data]);
129
128
 
130
- // Show validation errors if any
131
- if (!validation.valid) {
132
- return <ValidationErrors errors={validation.errors} />;
133
- }
129
+ // Show validation errors if any
130
+ if (!validation.valid) {
131
+ return <ValidationErrors errors={validation.errors} />;
132
+ }
134
133
 
135
- return (
136
- <div style={styles.container} className={className}>
137
- {template.title && (
138
- <div style={styles.title}>{template.title}</div>
139
- )}
134
+ return (
135
+ <div style={styles.container} className={className}>
136
+ {template.title && <div style={styles.title}>{template.title}</div>}
140
137
 
141
- {template.sections.map((section, index) => (
142
- <SectionRenderer
143
- key={section.title || index}
144
- section={section}
145
- data={data}
146
- onUpdate={onUpdate}
147
- agentMode={agentMode}
148
- />
149
- ))}
138
+ {template.sections.map((section, index) => (
139
+ <SectionRenderer
140
+ key={section.title || index}
141
+ section={section}
142
+ data={data}
143
+ onUpdate={onUpdate}
144
+ agentMode={agentMode}
145
+ />
146
+ ))}
150
147
 
151
- {template.footer && (
152
- <div style={styles.footer}>{template.footer}</div>
153
- )}
154
- </div>
155
- );
148
+ {template.footer && <div style={styles.footer}>{template.footer}</div>}
149
+ </div>
150
+ );
156
151
  }
@@ -3,193 +3,198 @@
3
3
  * Renders a section with grid layout and collapsible behavior
4
4
  */
5
5
 
6
- import { useState, useMemo, type ReactElement } from 'react';
6
+ import { type ReactElement, useMemo, useState } from 'react';
7
7
  import type { SectionRendererProps } from '../types.js';
8
+ import { ChartRenderer } from './ChartRenderer.js';
8
9
  import { FieldRenderer } from './FieldRenderer.js';
9
10
  import { TableRenderer } from './TableRenderer.js';
10
- import { ChartRenderer } from './ChartRenderer.js';
11
11
 
12
12
  // Layout grid configurations
13
13
  const gridLayouts = {
14
- 'grid-2': {
15
- display: 'grid',
16
- gridTemplateColumns: 'repeat(2, 1fr)',
17
- gap: '16px',
18
- },
19
- 'grid-3': {
20
- display: 'grid',
21
- gridTemplateColumns: 'repeat(3, 1fr)',
22
- gap: '16px',
23
- },
24
- 'grid-4': {
25
- display: 'grid',
26
- gridTemplateColumns: 'repeat(4, 1fr)',
27
- gap: '16px',
28
- },
29
- list: {
30
- display: 'flex',
31
- flexDirection: 'column' as const,
32
- gap: '12px',
33
- },
14
+ 'grid-2': {
15
+ display: 'grid',
16
+ gridTemplateColumns: 'repeat(2, 1fr)',
17
+ gap: '16px',
18
+ },
19
+ 'grid-3': {
20
+ display: 'grid',
21
+ gridTemplateColumns: 'repeat(3, 1fr)',
22
+ gap: '16px',
23
+ },
24
+ 'grid-4': {
25
+ display: 'grid',
26
+ gridTemplateColumns: 'repeat(4, 1fr)',
27
+ gap: '16px',
28
+ },
29
+ list: {
30
+ display: 'flex',
31
+ flexDirection: 'column' as const,
32
+ gap: '12px',
33
+ },
34
34
  };
35
35
 
36
36
  const styles = {
37
- section: {
38
- marginBottom: '20px',
39
- },
40
- header: {
41
- display: 'flex',
42
- alignItems: 'center',
43
- gap: '8px',
44
- marginBottom: '12px',
45
- cursor: 'default',
46
- },
47
- headerCollapsible: {
48
- cursor: 'pointer',
49
- userSelect: 'none' as const,
50
- },
51
- title: {
52
- fontSize: '11px',
53
- fontWeight: 600,
54
- color: 'var(--gray-11, #6b7280)',
55
- textTransform: 'uppercase' as const,
56
- letterSpacing: '0.5px',
57
- },
58
- chevron: {
59
- width: '14px',
60
- height: '14px',
61
- color: 'var(--gray-10, #9ca3af)',
62
- transition: 'transform 0.2s',
63
- },
64
- chevronCollapsed: {
65
- transform: 'rotate(-90deg)',
66
- },
67
- content: {
68
- overflow: 'hidden',
69
- transition: 'max-height 0.2s ease-out',
70
- },
71
- contentCollapsed: {
72
- maxHeight: '0',
73
- opacity: 0,
74
- },
75
- contentExpanded: {
76
- maxHeight: '2000px', // Large enough for most content
77
- opacity: 1,
78
- },
79
- divider: {
80
- height: '1px',
81
- backgroundColor: 'var(--gray-5, #e5e7eb)',
82
- marginTop: '16px',
83
- },
37
+ section: {
38
+ marginBottom: '20px',
39
+ },
40
+ header: {
41
+ display: 'flex',
42
+ alignItems: 'center',
43
+ gap: '8px',
44
+ marginBottom: '12px',
45
+ cursor: 'default',
46
+ },
47
+ headerCollapsible: {
48
+ cursor: 'pointer',
49
+ userSelect: 'none' as const,
50
+ },
51
+ title: {
52
+ fontSize: '11px',
53
+ fontWeight: 600,
54
+ color: 'var(--gray-11, #6b7280)',
55
+ textTransform: 'uppercase' as const,
56
+ letterSpacing: '0.5px',
57
+ },
58
+ chevron: {
59
+ width: '14px',
60
+ height: '14px',
61
+ color: 'var(--gray-10, #9ca3af)',
62
+ transition: 'transform 0.2s',
63
+ },
64
+ chevronCollapsed: {
65
+ transform: 'rotate(-90deg)',
66
+ },
67
+ content: {
68
+ overflow: 'hidden',
69
+ transition: 'max-height 0.2s ease-out',
70
+ },
71
+ contentCollapsed: {
72
+ maxHeight: '0',
73
+ opacity: 0,
74
+ },
75
+ contentExpanded: {
76
+ maxHeight: '2000px', // Large enough for most content
77
+ opacity: 1,
78
+ },
79
+ divider: {
80
+ height: '1px',
81
+ backgroundColor: 'var(--gray-5, #e5e7eb)',
82
+ marginTop: '16px',
83
+ },
84
84
  };
85
85
 
86
86
  // Simple chevron SVG component
87
87
  function ChevronIcon({ collapsed }: { collapsed: boolean }): ReactElement {
88
- return (
89
- <svg
90
- style={{
91
- ...styles.chevron,
92
- ...(collapsed ? styles.chevronCollapsed : {}),
93
- }}
94
- viewBox="0 0 24 24"
95
- fill="none"
96
- stroke="currentColor"
97
- strokeWidth="2"
98
- strokeLinecap="round"
99
- strokeLinejoin="round"
100
- >
101
- <polyline points="6 9 12 15 18 9" />
102
- </svg>
103
- );
88
+ return (
89
+ <svg
90
+ style={{
91
+ ...styles.chevron,
92
+ ...(collapsed ? styles.chevronCollapsed : {}),
93
+ }}
94
+ viewBox="0 0 24 24"
95
+ fill="none"
96
+ stroke="currentColor"
97
+ strokeWidth="2"
98
+ strokeLinecap="round"
99
+ strokeLinejoin="round"
100
+ aria-hidden="true"
101
+ >
102
+ <polyline points="6 9 12 15 18 9" />
103
+ </svg>
104
+ );
104
105
  }
105
106
 
106
107
  /**
107
108
  * SectionRenderer component
108
109
  * Displays a section with title, optional collapse, and grid of fields
109
110
  */
110
- export function SectionRenderer({
111
- section,
112
- data,
113
- onUpdate,
114
- agentMode,
115
- }: SectionRendererProps): ReactElement {
116
- const [isCollapsed, setIsCollapsed] = useState(section.collapsed ?? false);
117
-
118
- const layout = section.layout || 'grid-3';
119
- const isTable = layout === 'table';
120
- const isChart = layout === 'chart';
121
-
122
- const isCollapsible = section.collapsed !== undefined;
123
-
124
- // Get table rows from data
125
- const tableRows = useMemo(() => {
126
- if (!isTable || !section.dataKey) return [];
127
- const rows = data[section.dataKey];
128
- return Array.isArray(rows) ? rows as Record<string, unknown>[] : [];
129
- }, [isTable, section.dataKey, data]);
130
-
131
- const headerStyle = useMemo(
132
- () => ({
133
- ...styles.header,
134
- ...(isCollapsible ? styles.headerCollapsible : {}),
135
- }),
136
- [isCollapsible]
137
- );
138
-
139
- const contentStyle = useMemo(
140
- () => ({
141
- ...styles.content,
142
- ...(isTable || isChart ? {} : gridLayouts[layout as keyof typeof gridLayouts]),
143
- ...(isCollapsed ? styles.contentCollapsed : styles.contentExpanded),
144
- }),
145
- [isCollapsed, isTable, isChart, layout]
146
- );
147
-
148
- const handleHeaderClick = () => {
149
- if (isCollapsible) {
150
- setIsCollapsed(!isCollapsed);
151
- }
152
- };
153
-
154
- // Render content based on layout type
155
- const renderContent = () => {
156
- if (isTable && section.columns) {
157
- return <TableRenderer columns={section.columns} rows={tableRows} />;
158
- }
159
-
160
- if (isChart && section.chart) {
161
- return <ChartRenderer chart={section.chart} data={data} />;
162
- }
163
-
164
- // Default: render fields
165
- return section.fields?.map((field, index) => (
166
- <FieldRenderer
167
- key={field.key || index}
168
- field={field}
169
- value={data[field.key]}
170
- onUpdate={
171
- onUpdate ? (value) => onUpdate(field.key, value) : undefined
111
+ export function SectionRenderer({ section, data, onUpdate, agentMode }: SectionRendererProps): ReactElement {
112
+ const [isCollapsed, setIsCollapsed] = useState(section.collapsed ?? false);
113
+
114
+ const layout = section.layout || 'grid-3';
115
+ const isTable = layout === 'table';
116
+ const isChart = layout === 'chart';
117
+
118
+ const isCollapsible = section.collapsed !== undefined;
119
+
120
+ // Get table rows from data
121
+ const tableRows = useMemo(() => {
122
+ if (!isTable || !section.dataKey) return [];
123
+ const rows = data[section.dataKey];
124
+ return Array.isArray(rows) ? (rows as Record<string, unknown>[]) : [];
125
+ }, [isTable, section.dataKey, data]);
126
+
127
+ const headerStyle = useMemo(
128
+ () => ({
129
+ ...styles.header,
130
+ ...(isCollapsible ? styles.headerCollapsible : {}),
131
+ }),
132
+ [isCollapsible],
133
+ );
134
+
135
+ const contentStyle = useMemo(
136
+ () => ({
137
+ ...styles.content,
138
+ ...(isTable || isChart ? {} : gridLayouts[layout as keyof typeof gridLayouts]),
139
+ ...(isCollapsed ? styles.contentCollapsed : styles.contentExpanded),
140
+ }),
141
+ [isCollapsed, isTable, isChart, layout],
142
+ );
143
+
144
+ const handleHeaderClick = () => {
145
+ if (isCollapsible) {
146
+ setIsCollapsed(!isCollapsed);
147
+ }
148
+ };
149
+
150
+ // Render content based on layout type
151
+ const renderContent = () => {
152
+ if (isTable && section.columns) {
153
+ return <TableRenderer columns={section.columns} rows={tableRows} />;
172
154
  }
173
- agentMode={agentMode}
174
- />
175
- ));
176
- };
177
-
178
- return (
179
- <div style={styles.section}>
180
- <div
181
- style={headerStyle}
182
- onClick={handleHeaderClick}
183
- role={isCollapsible ? 'button' : undefined}
184
- aria-expanded={isCollapsible ? !isCollapsed : undefined}
185
- >
186
- {isCollapsible && <ChevronIcon collapsed={isCollapsed} />}
187
- <span style={styles.title}>{section.title}</span>
188
- </div>
189
-
190
- <div style={contentStyle}>
191
- {renderContent()}
192
- </div>
193
- </div>
194
- );
155
+
156
+ if (isChart && section.chart) {
157
+ return <ChartRenderer chart={section.chart} data={data} />;
158
+ }
159
+
160
+ // Default: render fields
161
+ return section.fields?.map((field, index) => (
162
+ <FieldRenderer
163
+ key={field.key || index}
164
+ field={field}
165
+ value={data[field.key]}
166
+ onUpdate={onUpdate ? (value) => onUpdate(field.key, value) : undefined}
167
+ agentMode={agentMode}
168
+ />
169
+ ));
170
+ };
171
+
172
+ return (
173
+ <div style={styles.section}>
174
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: onClick is no-op when not collapsible; interactive role/keyboard only added when isCollapsible is true. */}
175
+ {/* biome-ignore lint/a11y/useAriaPropsSupportedByRole: aria-expanded only emitted when role='button' is also set (both gated on isCollapsible). */}
176
+ <div
177
+ style={headerStyle}
178
+ onClick={handleHeaderClick}
179
+ onKeyDown={
180
+ isCollapsible
181
+ ? (e) => {
182
+ if (e.key === 'Enter' || e.key === ' ') {
183
+ e.preventDefault();
184
+ handleHeaderClick();
185
+ }
186
+ }
187
+ : undefined
188
+ }
189
+ role={isCollapsible ? 'button' : undefined}
190
+ tabIndex={isCollapsible ? 0 : undefined}
191
+ aria-expanded={isCollapsible ? !isCollapsed : undefined}
192
+ >
193
+ {isCollapsible && <ChevronIcon collapsed={isCollapsed} />}
194
+ <span style={styles.title}>{section.title}</span>
195
+ </div>
196
+
197
+ <div style={contentStyle}>{renderContent()}</div>
198
+ </div>
199
+ );
195
200
  }