@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,24 +3,24 @@
3
3
  * Provides data and update handlers to nested components
4
4
  */
5
5
 
6
- import { createContext, useContext, useMemo, type ReactNode, type ReactElement } from 'react';
7
- import type { FusionFragmentContextValue, ChartComponentProps } from '../types.js';
6
+ import { createContext, type ReactElement, type ReactNode, useContext, useMemo } from 'react';
7
+ import type { ChartComponentProps, FusionFragmentContextValue } from '../types.js';
8
8
 
9
9
  const FusionFragmentContext = createContext<FusionFragmentContextValue | null>(null);
10
10
 
11
11
  export interface FusionFragmentProviderProps {
12
- /** Data to display in fragments */
13
- data: Record<string, unknown>;
14
- /** Callback when a field is updated (direct mode) */
15
- onUpdate?: (key: string, value: unknown) => Promise<void>;
16
- /** Send message to conversation (agent mode) */
17
- sendMessage?: (message: string) => void;
18
- /** Chart component to render Vega-Lite charts (injected to avoid circular deps) */
19
- ChartComponent?: React.ComponentType<ChartComponentProps>;
20
- /** Artifact run ID for resolving artifact references */
21
- artifactRunId?: string;
22
- /** Children components */
23
- children: ReactNode;
12
+ /** Data to display in fragments */
13
+ data: Record<string, unknown>;
14
+ /** Callback when a field is updated (direct mode) */
15
+ onUpdate?: (key: string, value: unknown) => Promise<void>;
16
+ /** Send message to conversation (agent mode) */
17
+ sendMessage?: (message: string) => void;
18
+ /** Chart component to render Vega-Lite charts (injected to avoid circular deps) */
19
+ ChartComponent?: React.ComponentType<ChartComponentProps>;
20
+ /** Artifact run ID for resolving artifact references */
21
+ artifactRunId?: string;
22
+ /** Children components */
23
+ children: ReactNode;
24
24
  }
25
25
 
26
26
  /**
@@ -40,23 +40,19 @@ export interface FusionFragmentProviderProps {
40
40
  * ```
41
41
  */
42
42
  export function FusionFragmentProvider({
43
- data,
44
- onUpdate,
45
- sendMessage,
46
- ChartComponent,
47
- artifactRunId,
48
- children
43
+ data,
44
+ onUpdate,
45
+ sendMessage,
46
+ ChartComponent,
47
+ artifactRunId,
48
+ children,
49
49
  }: FusionFragmentProviderProps): ReactElement {
50
- const value = useMemo<FusionFragmentContextValue>(
51
- () => ({ data, onUpdate, sendMessage, ChartComponent, artifactRunId }),
52
- [data, onUpdate, sendMessage, ChartComponent, artifactRunId]
53
- );
50
+ const value = useMemo<FusionFragmentContextValue>(
51
+ () => ({ data, onUpdate, sendMessage, ChartComponent, artifactRunId }),
52
+ [data, onUpdate, sendMessage, ChartComponent, artifactRunId],
53
+ );
54
54
 
55
- return (
56
- <FusionFragmentContext.Provider value={value}>
57
- {children}
58
- </FusionFragmentContext.Provider>
59
- );
55
+ return <FusionFragmentContext.Provider value={value}>{children}</FusionFragmentContext.Provider>;
60
56
  }
61
57
 
62
58
  /**
@@ -64,15 +60,13 @@ export function FusionFragmentProvider({
64
60
  * @throws Error if used outside of FusionFragmentProvider
65
61
  */
66
62
  export function useFusionFragmentContext(): FusionFragmentContextValue {
67
- const context = useContext(FusionFragmentContext);
63
+ const context = useContext(FusionFragmentContext);
68
64
 
69
- if (!context) {
70
- throw new Error(
71
- 'useFusionFragmentContext must be used within a FusionFragmentProvider'
72
- );
73
- }
65
+ if (!context) {
66
+ throw new Error('useFusionFragmentContext must be used within a FusionFragmentProvider');
67
+ }
74
68
 
75
- return context;
69
+ return context;
76
70
  }
77
71
 
78
72
  /**
@@ -80,5 +74,5 @@ export function useFusionFragmentContext(): FusionFragmentContextValue {
80
74
  * Returns null if not within a provider (useful for optional context)
81
75
  */
82
76
  export function useFusionFragmentContextSafe(): FusionFragmentContextValue | null {
83
- return useContext(FusionFragmentContext);
77
+ return useContext(FusionFragmentContext);
84
78
  }
@@ -3,77 +3,77 @@
3
3
  * Parses fusion-fragment code blocks and renders them
4
4
  */
5
5
 
6
- import { useMemo, type ReactElement } from 'react';
6
+ import { type ReactElement, useMemo } from 'react';
7
7
  import type { FragmentTemplate } from '../types.js';
8
8
  import { parseAndValidateTemplate } from '../validation/validateTemplate.js';
9
- import { FusionFragmentRenderer } from './FusionFragmentRenderer.js';
10
9
  import { useFusionFragmentContextSafe } from './FusionFragmentContext.js';
10
+ import { FusionFragmentRenderer } from './FusionFragmentRenderer.js';
11
11
 
12
12
  const styles = {
13
- error: {
14
- backgroundColor: 'var(--red-2, #fef2f2)',
15
- border: '1px solid var(--red-6, #fca5a5)',
16
- borderRadius: '8px',
17
- padding: '16px',
18
- fontFamily: 'var(--font-mono, ui-monospace, monospace)',
19
- fontSize: '13px',
20
- },
21
- errorTitle: {
22
- fontWeight: 600,
23
- color: 'var(--red-11, #dc2626)',
24
- marginBottom: '8px',
25
- },
26
- errorMessage: {
27
- color: 'var(--red-10, #ef4444)',
28
- whiteSpace: 'pre-wrap' as const,
29
- },
30
- noContext: {
31
- backgroundColor: 'var(--yellow-2, #fefce8)',
32
- border: '1px solid var(--yellow-6, #fde047)',
33
- borderRadius: '8px',
34
- padding: '16px',
35
- },
36
- noContextTitle: {
37
- fontSize: '14px',
38
- fontWeight: 600,
39
- color: 'var(--yellow-11, #ca8a04)',
40
- marginBottom: '8px',
41
- },
42
- noContextMessage: {
43
- fontSize: '13px',
44
- color: 'var(--gray-11, #6b7280)',
45
- },
46
- loading: {
47
- display: 'flex',
48
- flexDirection: 'column' as const,
49
- alignItems: 'center',
50
- justifyContent: 'center',
51
- gap: '12px',
52
- minHeight: '150px',
53
- borderRadius: '8px',
54
- border: '1px solid var(--gray-6, #e5e7eb)',
55
- backgroundColor: 'var(--gray-2, #f9fafb)',
56
- },
57
- loadingIcon: {
58
- width: '32px',
59
- height: '32px',
60
- color: 'var(--gray-9, #9ca3af)',
61
- },
62
- loadingText: {
63
- fontSize: '14px',
64
- color: 'var(--gray-9, #9ca3af)',
65
- },
66
- loadingDots: {
67
- display: 'flex',
68
- gap: '4px',
69
- },
70
- loadingDot: {
71
- width: '8px',
72
- height: '8px',
73
- borderRadius: '50%',
74
- backgroundColor: 'var(--gray-6, #d1d5db)',
75
- animation: 'fusion-fragment-bounce 1s infinite',
76
- },
13
+ error: {
14
+ backgroundColor: 'var(--red-2, #fef2f2)',
15
+ border: '1px solid var(--red-6, #fca5a5)',
16
+ borderRadius: '8px',
17
+ padding: '16px',
18
+ fontFamily: 'var(--font-mono, ui-monospace, monospace)',
19
+ fontSize: '13px',
20
+ },
21
+ errorTitle: {
22
+ fontWeight: 600,
23
+ color: 'var(--red-11, #dc2626)',
24
+ marginBottom: '8px',
25
+ },
26
+ errorMessage: {
27
+ color: 'var(--red-10, #ef4444)',
28
+ whiteSpace: 'pre-wrap' as const,
29
+ },
30
+ noContext: {
31
+ backgroundColor: 'var(--yellow-2, #fefce8)',
32
+ border: '1px solid var(--yellow-6, #fde047)',
33
+ borderRadius: '8px',
34
+ padding: '16px',
35
+ },
36
+ noContextTitle: {
37
+ fontSize: '14px',
38
+ fontWeight: 600,
39
+ color: 'var(--yellow-11, #ca8a04)',
40
+ marginBottom: '8px',
41
+ },
42
+ noContextMessage: {
43
+ fontSize: '13px',
44
+ color: 'var(--gray-11, #6b7280)',
45
+ },
46
+ loading: {
47
+ display: 'flex',
48
+ flexDirection: 'column' as const,
49
+ alignItems: 'center',
50
+ justifyContent: 'center',
51
+ gap: '12px',
52
+ minHeight: '150px',
53
+ borderRadius: '8px',
54
+ border: '1px solid var(--gray-6, #e5e7eb)',
55
+ backgroundColor: 'var(--gray-2, #f9fafb)',
56
+ },
57
+ loadingIcon: {
58
+ width: '32px',
59
+ height: '32px',
60
+ color: 'var(--gray-9, #9ca3af)',
61
+ },
62
+ loadingText: {
63
+ fontSize: '14px',
64
+ color: 'var(--gray-9, #9ca3af)',
65
+ },
66
+ loadingDots: {
67
+ display: 'flex',
68
+ gap: '4px',
69
+ },
70
+ loadingDot: {
71
+ width: '8px',
72
+ height: '8px',
73
+ borderRadius: '50%',
74
+ backgroundColor: 'var(--gray-6, #d1d5db)',
75
+ animation: 'fusion-fragment-bounce 1s infinite',
76
+ },
77
77
  };
78
78
 
79
79
  /**
@@ -81,146 +81,141 @@ const styles = {
81
81
  * vs actually invalid JSON structure
82
82
  */
83
83
  function isIncompleteJson(code: string): boolean {
84
- const trimmed = code.trim();
84
+ const trimmed = code.trim();
85
85
 
86
- // Empty or very short content is likely incomplete
87
- if (trimmed.length < 2) return true;
86
+ // Empty or very short content is likely incomplete
87
+ if (trimmed.length < 2) return true;
88
88
 
89
- // Must start with { for a valid JSON object
90
- if (!trimmed.startsWith('{')) return false;
89
+ // Must start with { for a valid JSON object
90
+ if (!trimmed.startsWith('{')) return false;
91
91
 
92
- // Try to parse - if it succeeds, it's not incomplete
93
- try {
94
- JSON.parse(trimmed);
95
- return false; // Valid JSON
96
- } catch (e) {
97
- const message = e instanceof Error ? e.message : '';
92
+ // Try to parse - if it succeeds, it's not incomplete
93
+ try {
94
+ JSON.parse(trimmed);
95
+ return false; // Valid JSON
96
+ } catch (e) {
97
+ const message = e instanceof Error ? e.message : '';
98
98
 
99
- // Common indicators of incomplete JSON during streaming
100
- const incompleteIndicators = [
101
- 'unexpected end',
102
- 'unterminated string',
103
- 'expected',
104
- 'unexpected token',
105
- ];
99
+ // Common indicators of incomplete JSON during streaming
100
+ const incompleteIndicators = ['unexpected end', 'unterminated string', 'expected', 'unexpected token'];
106
101
 
107
- const lowerMessage = message.toLowerCase();
108
- if (incompleteIndicators.some(ind => lowerMessage.includes(ind))) {
109
- // Additional check: count brackets to see if they're unbalanced
110
- let braceCount = 0;
111
- let bracketCount = 0;
112
- let inString = false;
113
- let escaped = false;
102
+ const lowerMessage = message.toLowerCase();
103
+ if (incompleteIndicators.some((ind) => lowerMessage.includes(ind))) {
104
+ // Additional check: count brackets to see if they're unbalanced
105
+ let braceCount = 0;
106
+ let bracketCount = 0;
107
+ let inString = false;
108
+ let escaped = false;
114
109
 
115
- for (const char of trimmed) {
116
- if (escaped) {
117
- escaped = false;
118
- continue;
119
- }
120
- if (char === '\\') {
121
- escaped = true;
122
- continue;
123
- }
124
- if (char === '"') {
125
- inString = !inString;
126
- continue;
127
- }
128
- if (!inString) {
129
- if (char === '{') braceCount++;
130
- else if (char === '}') braceCount--;
131
- else if (char === '[') bracketCount++;
132
- else if (char === ']') bracketCount--;
110
+ for (const char of trimmed) {
111
+ if (escaped) {
112
+ escaped = false;
113
+ continue;
114
+ }
115
+ if (char === '\\') {
116
+ escaped = true;
117
+ continue;
118
+ }
119
+ if (char === '"') {
120
+ inString = !inString;
121
+ continue;
122
+ }
123
+ if (!inString) {
124
+ if (char === '{') braceCount++;
125
+ else if (char === '}') braceCount--;
126
+ else if (char === '[') bracketCount++;
127
+ else if (char === ']') bracketCount--;
128
+ }
129
+ }
130
+
131
+ // If brackets are unbalanced or we're in an unclosed string, it's incomplete
132
+ return braceCount > 0 || bracketCount > 0 || inString;
133
133
  }
134
- }
135
134
 
136
- // If brackets are unbalanced or we're in an unclosed string, it's incomplete
137
- return braceCount > 0 || bracketCount > 0 || inString;
135
+ // For other parse errors, consider it invalid rather than incomplete
136
+ return false;
138
137
  }
139
-
140
- // For other parse errors, consider it invalid rather than incomplete
141
- return false;
142
- }
143
138
  }
144
139
 
145
140
  export interface FusionFragmentHandlerProps {
146
- /** The JSON code from the fusion-fragment code block */
147
- code: string;
148
- /** Optional data to use instead of context */
149
- data?: Record<string, unknown>;
150
- /** Optional update handler */
151
- onUpdate?: (key: string, value: unknown) => Promise<void>;
141
+ /** The JSON code from the fusion-fragment code block */
142
+ code: string;
143
+ /** Optional data to use instead of context */
144
+ data?: Record<string, unknown>;
145
+ /** Optional update handler */
146
+ onUpdate?: (key: string, value: unknown) => Promise<void>;
152
147
  }
153
148
 
154
149
  /**
155
150
  * Loading placeholder shown while streaming incomplete JSON
156
151
  */
157
152
  function LoadingPlaceholder(): ReactElement {
158
- return (
159
- <div style={styles.loading}>
160
- <style>{`
153
+ return (
154
+ <div style={styles.loading}>
155
+ <style>{`
161
156
  @keyframes fusion-fragment-bounce {
162
157
  0%, 80%, 100% { transform: translateY(0); }
163
158
  40% { transform: translateY(-6px); }
164
159
  }
165
160
  `}</style>
166
- <svg
167
- style={styles.loadingIcon}
168
- viewBox="0 0 24 24"
169
- fill="none"
170
- stroke="currentColor"
171
- strokeWidth="2"
172
- strokeLinecap="round"
173
- strokeLinejoin="round"
174
- >
175
- <polyline points="16 18 22 12 16 6" />
176
- <polyline points="8 6 2 12 8 18" />
177
- </svg>
178
- <span style={styles.loadingText}>Loading fragment...</span>
179
- <div style={styles.loadingDots}>
180
- {[0, 1, 2].map(i => (
181
- <div
182
- key={i}
183
- style={{
184
- ...styles.loadingDot,
185
- animationDelay: `${i * 150}ms`,
186
- }}
187
- />
188
- ))}
189
- </div>
190
- </div>
191
- );
161
+ <svg
162
+ style={styles.loadingIcon}
163
+ viewBox="0 0 24 24"
164
+ fill="none"
165
+ stroke="currentColor"
166
+ strokeWidth="2"
167
+ strokeLinecap="round"
168
+ strokeLinejoin="round"
169
+ role="img"
170
+ >
171
+ <title>Loading</title>
172
+ <polyline points="16 18 22 12 16 6" />
173
+ <polyline points="8 6 2 12 8 18" />
174
+ </svg>
175
+ <span style={styles.loadingText}>Loading fragment...</span>
176
+ <div style={styles.loadingDots}>
177
+ {[0, 1, 2].map((i) => (
178
+ <div
179
+ key={i}
180
+ style={{
181
+ ...styles.loadingDot,
182
+ animationDelay: `${i * 150}ms`,
183
+ }}
184
+ />
185
+ ))}
186
+ </div>
187
+ </div>
188
+ );
192
189
  }
193
190
 
194
191
  /**
195
192
  * Error display component
196
193
  */
197
194
  function ParseError({ message }: { message: string }): ReactElement {
198
- return (
199
- <div style={styles.error}>
200
- <div style={styles.errorTitle}>Failed to parse fusion-fragment</div>
201
- <div style={styles.errorMessage}>{message}</div>
202
- </div>
203
- );
195
+ return (
196
+ <div style={styles.error}>
197
+ <div style={styles.errorTitle}>Failed to parse fusion-fragment</div>
198
+ <div style={styles.errorMessage}>{message}</div>
199
+ </div>
200
+ );
204
201
  }
205
202
 
206
203
  /**
207
204
  * Warning when no context is available
208
205
  */
209
206
  function NoContextWarning({ template }: { template: FragmentTemplate }): ReactElement {
210
- const requiredKeys = template.sections
211
- .flatMap(s => s.fields || [])
212
- .map(f => f.key);
207
+ const requiredKeys = template.sections.flatMap((s) => s.fields || []).map((f) => f.key);
213
208
 
214
- return (
215
- <div style={styles.noContext}>
216
- <div style={styles.noContextTitle}>No data context available</div>
217
- <div style={styles.noContextMessage}>
218
- This fusion-fragment requires data with keys: {requiredKeys.join(', ')}.
219
- <br />
220
- Wrap this component in a FusionFragmentProvider with the required data.
221
- </div>
222
- </div>
223
- );
209
+ return (
210
+ <div style={styles.noContext}>
211
+ <div style={styles.noContextTitle}>No data context available</div>
212
+ <div style={styles.noContextMessage}>
213
+ This fusion-fragment requires data with keys: {requiredKeys.join(', ')}.
214
+ <br />
215
+ Wrap this component in a FusionFragmentProvider with the required data.
216
+ </div>
217
+ </div>
218
+ );
224
219
  }
225
220
 
226
221
  /**
@@ -239,66 +234,62 @@ function NoContextWarning({ template }: { template: FragmentTemplate }): ReactEl
239
234
  * ```
240
235
  */
241
236
  export function FusionFragmentHandler({
242
- code,
243
- data: propData,
244
- onUpdate: propOnUpdate,
237
+ code,
238
+ data: propData,
239
+ onUpdate: propOnUpdate,
245
240
  }: FusionFragmentHandlerProps): ReactElement {
246
- // Try to get context (may be null if no provider)
247
- const context = useFusionFragmentContextSafe();
241
+ // Try to get context (may be null if no provider)
242
+ const context = useFusionFragmentContextSafe();
248
243
 
249
- // Use prop data or context data
250
- const data = propData ?? context?.data;
251
- const onUpdate = propOnUpdate ?? context?.onUpdate;
252
- const sendMessage = context?.sendMessage;
244
+ // Use prop data or context data
245
+ const data = propData ?? context?.data;
246
+ const onUpdate = propOnUpdate ?? context?.onUpdate;
247
+ const sendMessage = context?.sendMessage;
253
248
 
254
- // Check if JSON is incomplete (streaming) - show loading placeholder
255
- const incomplete = useMemo(() => isIncompleteJson(code), [code]);
249
+ // Check if JSON is incomplete (streaming) - show loading placeholder
250
+ const incomplete = useMemo(() => isIncompleteJson(code), [code]);
256
251
 
257
- // Parse and validate the template (only if not obviously incomplete)
258
- const result = useMemo(() => {
252
+ // Parse and validate the template (only if not obviously incomplete)
253
+ const result = useMemo(() => {
254
+ if (incomplete) {
255
+ return { valid: false, errors: [], template: undefined };
256
+ }
257
+ const dataKeys = data ? Object.keys(data) : [];
258
+ return parseAndValidateTemplate(code, dataKeys);
259
+ }, [code, data, incomplete]);
260
+
261
+ // Show loading placeholder for incomplete JSON (streaming in progress)
259
262
  if (incomplete) {
260
- return { valid: false, errors: [], template: undefined };
263
+ return <LoadingPlaceholder />;
261
264
  }
262
- const dataKeys = data ? Object.keys(data) : [];
263
- return parseAndValidateTemplate(code, dataKeys);
264
- }, [code, data, incomplete]);
265
-
266
- // Show loading placeholder for incomplete JSON (streaming in progress)
267
- if (incomplete) {
268
- return <LoadingPlaceholder />;
269
- }
270
265
 
271
- // Parse error
272
- if (!result.valid && !result.template) {
273
- const parseError = result.errors.find(e => e.path === 'root' && e.message.includes('JSON'));
274
- if (parseError) {
275
- return <ParseError message={parseError.message} />;
266
+ // Parse error
267
+ if (!result.valid && !result.template) {
268
+ const parseError = result.errors.find((e) => e.path === 'root' && e.message.includes('JSON'));
269
+ if (parseError) {
270
+ return <ParseError message={parseError.message} />;
271
+ }
276
272
  }
277
- }
278
273
 
279
- // Template parsed but no data available
280
- if (result.template && !data) {
281
- return <NoContextWarning template={result.template} />;
282
- }
274
+ // Template parsed but no data available
275
+ if (result.template && !data) {
276
+ return <NoContextWarning template={result.template} />;
277
+ }
283
278
 
284
- // Template parsed but validation errors (show errors in renderer)
285
- if (result.template && data) {
286
- return (
287
- <FusionFragmentRenderer
288
- template={result.template}
289
- data={data}
290
- onUpdate={onUpdate}
291
- agentMode={sendMessage ? { enabled: true, sendMessage } : undefined}
292
- />
293
- );
294
- }
279
+ // Template parsed but validation errors (show errors in renderer)
280
+ if (result.template && data) {
281
+ return (
282
+ <FusionFragmentRenderer
283
+ template={result.template}
284
+ data={data}
285
+ onUpdate={onUpdate}
286
+ agentMode={sendMessage ? { enabled: true, sendMessage } : undefined}
287
+ />
288
+ );
289
+ }
295
290
 
296
- // Fallback for any other error case
297
- return (
298
- <ParseError
299
- message={result.errors.map(e => `${e.path}: ${e.message}`).join('\n')}
300
- />
301
- );
291
+ // Fallback for any other error case
292
+ return <ParseError message={result.errors.map((e) => `${e.path}: ${e.message}`).join('\n')} />;
302
293
  }
303
294
 
304
295
  /**
@@ -306,8 +297,8 @@ export function FusionFragmentHandler({
306
297
  * for use with markdown renderers that support custom code blocks
307
298
  */
308
299
  export function createFusionFragmentCodeBlockRenderer() {
309
- return {
310
- language: 'fusion-fragment',
311
- component: FusionFragmentHandler,
312
- };
300
+ return {
301
+ language: 'fusion-fragment',
302
+ component: FusionFragmentHandler,
303
+ };
313
304
  }