@vertesia/fusion-ux 1.3.0 → 1.4.0-dev.20260615.042033Z
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.
- package/LICENSE +198 -10
- package/lib/fusion-fragment/ChartRenderer.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/ChartRenderer.js +8 -6
- package/lib/fusion-fragment/ChartRenderer.js.map +1 -0
- package/lib/{types/fusion-fragment → fusion-fragment}/FieldRenderer.d.ts +1 -1
- package/lib/fusion-fragment/FieldRenderer.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/FieldRenderer.js +14 -8
- package/lib/fusion-fragment/FieldRenderer.js.map +1 -0
- package/lib/{types/fusion-fragment → fusion-fragment}/FusionFragmentContext.d.ts +3 -3
- package/lib/fusion-fragment/FusionFragmentContext.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/FusionFragmentContext.js +2 -2
- package/lib/fusion-fragment/FusionFragmentContext.js.map +1 -0
- package/lib/fusion-fragment/FusionFragmentHandler.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/FusionFragmentHandler.js +7 -14
- package/lib/fusion-fragment/FusionFragmentHandler.js.map +1 -0
- package/lib/fusion-fragment/FusionFragmentRenderer.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/FusionFragmentRenderer.js +2 -2
- package/lib/fusion-fragment/FusionFragmentRenderer.js.map +1 -0
- package/lib/{types/fusion-fragment → fusion-fragment}/SectionRenderer.d.ts +1 -1
- package/lib/fusion-fragment/SectionRenderer.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/SectionRenderer.js +12 -5
- package/lib/fusion-fragment/SectionRenderer.js.map +1 -0
- package/lib/fusion-fragment/TableRenderer.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/TableRenderer.js +9 -7
- package/lib/fusion-fragment/TableRenderer.js.map +1 -0
- package/lib/{types/fusion-fragment → fusion-fragment}/index.d.ts +4 -4
- package/lib/fusion-fragment/index.d.ts.map +1 -0
- package/lib/{esm/fusion-fragment → fusion-fragment}/index.js +4 -4
- package/lib/fusion-fragment/index.js.map +1 -0
- package/lib/{types/index.d.ts → index.d.ts} +4 -4
- package/lib/index.d.ts.map +1 -0
- package/lib/{esm/index.js → index.js} +4 -4
- package/lib/index.js.map +1 -0
- package/lib/{types/render → render}/index.d.ts +1 -1
- package/lib/render/index.d.ts.map +1 -0
- package/lib/{esm/render → render}/index.js +1 -1
- package/lib/render/index.js.map +1 -0
- package/lib/render/textPreview.d.ts.map +1 -0
- package/lib/{esm/render → render}/textPreview.js +12 -9
- package/lib/render/textPreview.js.map +1 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js.map +1 -0
- package/lib/validation/formatErrors.d.ts.map +1 -0
- package/lib/{esm/validation → validation}/formatErrors.js +1 -1
- package/lib/validation/formatErrors.js.map +1 -0
- package/lib/validation/fuzzyMatch.d.ts.map +1 -0
- package/lib/{esm/validation → validation}/fuzzyMatch.js +3 -4
- package/lib/validation/fuzzyMatch.js.map +1 -0
- package/lib/validation/index.d.ts +8 -0
- package/lib/validation/index.d.ts.map +1 -0
- package/lib/validation/index.js +8 -0
- package/lib/validation/index.js.map +1 -0
- package/lib/{types/validation → validation}/schemas.d.ts +1 -1
- package/lib/validation/schemas.d.ts.map +1 -0
- package/lib/{esm/validation → validation}/schemas.js +25 -25
- package/lib/validation/schemas.js.map +1 -0
- package/lib/validation/validateTemplate.d.ts.map +1 -0
- package/lib/{esm/validation → validation}/validateTemplate.js +23 -21
- package/lib/validation/validateTemplate.js.map +1 -0
- package/lib/vertesia-fusion-ux.js +1 -1
- package/lib/vertesia-fusion-ux.js.map +1 -1
- package/package.json +23 -35
- package/src/fusion-fragment/ChartRenderer.tsx +95 -96
- package/src/fusion-fragment/FieldRenderer.tsx +173 -174
- package/src/fusion-fragment/FusionFragmentContext.tsx +31 -37
- package/src/fusion-fragment/FusionFragmentHandler.tsx +214 -223
- package/src/fusion-fragment/FusionFragmentRenderer.tsx +102 -107
- package/src/fusion-fragment/SectionRenderer.tsx +174 -169
- package/src/fusion-fragment/TableRenderer.tsx +175 -171
- package/src/fusion-fragment/index.ts +11 -11
- package/src/index.ts +42 -45
- package/src/render/index.ts +3 -3
- package/src/render/textPreview.ts +183 -186
- package/src/types.ts +174 -174
- package/src/validation/formatErrors.ts +86 -86
- package/src/validation/fuzzyMatch.ts +69 -77
- package/src/validation/index.ts +3 -3
- package/src/validation/schemas.ts +120 -120
- package/src/validation/validateTemplate.ts +225 -226
- package/lib/esm/fusion-fragment/ChartRenderer.js.map +0 -1
- package/lib/esm/fusion-fragment/FieldRenderer.js.map +0 -1
- package/lib/esm/fusion-fragment/FusionFragmentContext.js.map +0 -1
- package/lib/esm/fusion-fragment/FusionFragmentHandler.js.map +0 -1
- package/lib/esm/fusion-fragment/FusionFragmentRenderer.js.map +0 -1
- package/lib/esm/fusion-fragment/SectionRenderer.js.map +0 -1
- package/lib/esm/fusion-fragment/TableRenderer.js.map +0 -1
- package/lib/esm/fusion-fragment/index.js.map +0 -1
- package/lib/esm/index.js.map +0 -1
- package/lib/esm/render/index.js.map +0 -1
- package/lib/esm/render/textPreview.js.map +0 -1
- package/lib/esm/types.js.map +0 -1
- package/lib/esm/validation/formatErrors.js.map +0 -1
- package/lib/esm/validation/fuzzyMatch.js.map +0 -1
- package/lib/esm/validation/index.js +0 -8
- package/lib/esm/validation/index.js.map +0 -1
- package/lib/esm/validation/schemas.js.map +0 -1
- package/lib/esm/validation/validateTemplate.js.map +0 -1
- package/lib/tsconfig.tsbuildinfo +0 -1
- package/lib/types/fusion-fragment/ChartRenderer.d.ts.map +0 -1
- package/lib/types/fusion-fragment/FieldRenderer.d.ts.map +0 -1
- package/lib/types/fusion-fragment/FusionFragmentContext.d.ts.map +0 -1
- package/lib/types/fusion-fragment/FusionFragmentHandler.d.ts.map +0 -1
- package/lib/types/fusion-fragment/FusionFragmentRenderer.d.ts.map +0 -1
- package/lib/types/fusion-fragment/SectionRenderer.d.ts.map +0 -1
- package/lib/types/fusion-fragment/TableRenderer.d.ts.map +0 -1
- package/lib/types/fusion-fragment/index.d.ts.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/render/index.d.ts.map +0 -1
- package/lib/types/render/textPreview.d.ts.map +0 -1
- package/lib/types/types.d.ts.map +0 -1
- package/lib/types/validation/formatErrors.d.ts.map +0 -1
- package/lib/types/validation/fuzzyMatch.d.ts.map +0 -1
- package/lib/types/validation/index.d.ts +0 -8
- package/lib/types/validation/index.d.ts.map +0 -1
- package/lib/types/validation/schemas.d.ts.map +0 -1
- package/lib/types/validation/validateTemplate.d.ts.map +0 -1
- /package/lib/{types/fusion-fragment → fusion-fragment}/ChartRenderer.d.ts +0 -0
- /package/lib/{types/fusion-fragment → fusion-fragment}/FusionFragmentHandler.d.ts +0 -0
- /package/lib/{types/fusion-fragment → fusion-fragment}/FusionFragmentRenderer.d.ts +0 -0
- /package/lib/{types/fusion-fragment → fusion-fragment}/TableRenderer.d.ts +0 -0
- /package/lib/{types/render → render}/textPreview.d.ts +0 -0
- /package/lib/{types/types.d.ts → types.d.ts} +0 -0
- /package/lib/{esm/types.js → types.js} +0 -0
- /package/lib/{types/validation → validation}/formatErrors.d.ts +0 -0
- /package/lib/{types/validation → validation}/fuzzyMatch.d.ts +0 -0
- /package/lib/{types/validation → validation}/validateTemplate.d.ts +0 -0
|
@@ -10,12 +10,12 @@ import type { ChartTemplate } from '../types.js';
|
|
|
10
10
|
import { useFusionFragmentContextSafe } from './FusionFragmentContext.js';
|
|
11
11
|
|
|
12
12
|
export interface ChartRendererProps {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
/** Chart template configuration */
|
|
14
|
+
chart: ChartTemplate;
|
|
15
|
+
/** Data context for dataKey resolution */
|
|
16
|
+
data: Record<string, unknown>;
|
|
17
|
+
/** CSS class name */
|
|
18
|
+
className?: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -23,105 +23,104 @@ export interface ChartRendererProps {
|
|
|
23
23
|
* If dataKey is provided, it merges data from context into the spec.
|
|
24
24
|
* Uses the ChartComponent from FusionFragmentContext if available.
|
|
25
25
|
*/
|
|
26
|
-
export function ChartRenderer({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}: ChartRendererProps): ReactElement {
|
|
31
|
-
const context = useFusionFragmentContextSafe();
|
|
32
|
-
const ChartComponent = context?.ChartComponent;
|
|
33
|
-
const artifactRunId = context?.artifactRunId;
|
|
26
|
+
export function ChartRenderer({ chart, data, className }: ChartRendererProps): ReactElement {
|
|
27
|
+
const context = useFusionFragmentContextSafe();
|
|
28
|
+
const ChartComponent = context?.ChartComponent;
|
|
29
|
+
const artifactRunId = context?.artifactRunId;
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
// Resolve data from context if dataKey is provided
|
|
32
|
+
const resolvedSpec = { ...chart.spec };
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
if (chart.dataKey && data[chart.dataKey]) {
|
|
35
|
+
const chartData = data[chart.dataKey];
|
|
36
|
+
if (Array.isArray(chartData)) {
|
|
37
|
+
// Inject data into spec
|
|
38
|
+
resolvedSpec.data = { values: chartData as Record<string, unknown>[] };
|
|
39
|
+
}
|
|
43
40
|
}
|
|
44
|
-
}
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
42
|
+
// Build the VegaLiteChartSpec format expected by @vertesia/ui
|
|
43
|
+
const vegaSpec = {
|
|
44
|
+
library: 'vega-lite' as const,
|
|
45
|
+
title: chart.title,
|
|
46
|
+
description: chart.description,
|
|
47
|
+
spec: resolvedSpec,
|
|
48
|
+
options: {
|
|
49
|
+
height: chart.height,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// If a ChartComponent is provided via context, use it to render the actual chart
|
|
54
|
+
if (ChartComponent) {
|
|
55
|
+
return (
|
|
56
|
+
<div className={className}>
|
|
57
|
+
<ChartComponent spec={vegaSpec} artifactRunId={artifactRunId} />
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
54
60
|
}
|
|
55
|
-
};
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
if (ChartComponent) {
|
|
62
|
+
// Fallback: render a placeholder with spec info when no ChartComponent is injected
|
|
59
63
|
return (
|
|
60
|
-
<div className={className}>
|
|
61
|
-
<ChartComponent spec={vegaSpec} artifactRunId={artifactRunId} />
|
|
62
|
-
</div>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Fallback: render a placeholder with spec info when no ChartComponent is injected
|
|
67
|
-
return (
|
|
68
|
-
<div
|
|
69
|
-
className={className}
|
|
70
|
-
style={{
|
|
71
|
-
padding: '16px',
|
|
72
|
-
backgroundColor: 'var(--gray-2, #f9fafb)',
|
|
73
|
-
border: '1px solid var(--gray-5, #e5e7eb)',
|
|
74
|
-
borderRadius: '8px',
|
|
75
|
-
minHeight: chart.height || 280
|
|
76
|
-
}}
|
|
77
|
-
>
|
|
78
|
-
{chart.title && (
|
|
79
64
|
<div
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
65
|
+
className={className}
|
|
66
|
+
style={{
|
|
67
|
+
padding: '16px',
|
|
68
|
+
backgroundColor: 'var(--gray-2, #f9fafb)',
|
|
69
|
+
border: '1px solid var(--gray-5, #e5e7eb)',
|
|
70
|
+
borderRadius: '8px',
|
|
71
|
+
minHeight: chart.height || 280,
|
|
72
|
+
}}
|
|
86
73
|
>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
74
|
+
{chart.title && (
|
|
75
|
+
<div
|
|
76
|
+
style={{
|
|
77
|
+
fontSize: '14px',
|
|
78
|
+
fontWeight: 600,
|
|
79
|
+
marginBottom: '8px',
|
|
80
|
+
color: 'var(--gray-12, #111827)',
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{chart.title}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
{chart.description && (
|
|
87
|
+
<div
|
|
88
|
+
style={{
|
|
89
|
+
fontSize: '12px',
|
|
90
|
+
color: 'var(--gray-11, #6b7280)',
|
|
91
|
+
marginBottom: '12px',
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
{chart.description}
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
<div
|
|
98
|
+
style={{
|
|
99
|
+
display: 'flex',
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
justifyContent: 'center',
|
|
102
|
+
height: (chart.height || 280) - 60,
|
|
103
|
+
backgroundColor: 'var(--gray-3, #f3f4f6)',
|
|
104
|
+
borderRadius: '4px',
|
|
105
|
+
color: 'var(--gray-11, #6b7280)',
|
|
106
|
+
fontSize: '12px',
|
|
107
|
+
}}
|
|
108
|
+
data-vega-spec={JSON.stringify(vegaSpec)}
|
|
109
|
+
>
|
|
110
|
+
<div style={{ textAlign: 'center' }}>
|
|
111
|
+
<div style={{ marginBottom: '4px' }}>
|
|
112
|
+
Chart:{' '}
|
|
113
|
+
{resolvedSpec.mark
|
|
114
|
+
? String(typeof resolvedSpec.mark === 'string' ? resolvedSpec.mark : resolvedSpec.mark.type)
|
|
115
|
+
: 'composite'}
|
|
116
|
+
</div>
|
|
117
|
+
{resolvedSpec.data?.values && (
|
|
118
|
+
<div style={{ fontSize: '11px', opacity: 0.8 }}>
|
|
119
|
+
{resolvedSpec.data.values.length} data points
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
121
123
|
</div>
|
|
122
|
-
)}
|
|
123
124
|
</div>
|
|
124
|
-
|
|
125
|
-
</div>
|
|
126
|
-
);
|
|
125
|
+
);
|
|
127
126
|
}
|
|
@@ -3,197 +3,196 @@
|
|
|
3
3
|
* Renders a single field with formatting and optional edit capability
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { type ReactElement, useCallback, useMemo, useState } from 'react';
|
|
7
7
|
import type { FieldRendererProps, FieldTemplate } from '../types.js';
|
|
8
8
|
|
|
9
9
|
// Styles as constants
|
|
10
10
|
const styles = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
11
|
+
container: {
|
|
12
|
+
display: 'flex',
|
|
13
|
+
flexDirection: 'column' as const,
|
|
14
|
+
gap: '2px',
|
|
15
|
+
minWidth: 0, // Allow text truncation
|
|
16
|
+
},
|
|
17
|
+
label: {
|
|
18
|
+
fontSize: '11px',
|
|
19
|
+
fontWeight: 500,
|
|
20
|
+
color: 'var(--gray-11, #6b7280)',
|
|
21
|
+
textTransform: 'uppercase' as const,
|
|
22
|
+
letterSpacing: '0.5px',
|
|
23
|
+
},
|
|
24
|
+
value: {
|
|
25
|
+
fontSize: '14px',
|
|
26
|
+
fontWeight: 500,
|
|
27
|
+
color: 'var(--gray-12, #1f2937)',
|
|
28
|
+
fontFamily: 'var(--font-mono, ui-monospace, monospace)',
|
|
29
|
+
fontVariantNumeric: 'tabular-nums' as const,
|
|
30
|
+
letterSpacing: '-0.02em',
|
|
31
|
+
},
|
|
32
|
+
unit: {
|
|
33
|
+
fontSize: '12px',
|
|
34
|
+
color: 'var(--gray-10, #9ca3af)',
|
|
35
|
+
marginLeft: '4px',
|
|
36
|
+
},
|
|
37
|
+
highlight: {
|
|
38
|
+
success: { color: 'var(--green-11, #15803d)' },
|
|
39
|
+
warning: { color: 'var(--yellow-11, #ca8a04)' },
|
|
40
|
+
error: { color: 'var(--red-11, #dc2626)' },
|
|
41
|
+
info: { color: 'var(--blue-11, #2563eb)' },
|
|
42
|
+
},
|
|
43
|
+
editable: {
|
|
44
|
+
cursor: 'pointer',
|
|
45
|
+
padding: '2px 4px',
|
|
46
|
+
borderRadius: '4px',
|
|
47
|
+
transition: 'background-color 0.15s',
|
|
48
|
+
},
|
|
49
|
+
tooltip: {
|
|
50
|
+
position: 'relative' as const,
|
|
51
|
+
},
|
|
52
|
+
nullValue: {
|
|
53
|
+
color: 'var(--gray-9, #9ca3af)',
|
|
54
|
+
fontStyle: 'italic' as const,
|
|
55
|
+
},
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Format a value according to the field's format specification
|
|
60
60
|
*/
|
|
61
61
|
function formatValue(value: unknown, field: FieldTemplate): string {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
switch (field.format) {
|
|
67
|
-
case 'number': {
|
|
68
|
-
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
|
69
|
-
if (isNaN(num)) return String(value);
|
|
70
|
-
|
|
71
|
-
const options: Intl.NumberFormatOptions = {
|
|
72
|
-
minimumFractionDigits: field.decimals ?? 0,
|
|
73
|
-
maximumFractionDigits: field.decimals ?? 2,
|
|
74
|
-
};
|
|
75
|
-
return new Intl.NumberFormat('en-US', options).format(num);
|
|
62
|
+
if (value === null || value === undefined) {
|
|
63
|
+
return '\u2014'; // em-dash for null/undefined
|
|
76
64
|
}
|
|
77
65
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
66
|
+
switch (field.format) {
|
|
67
|
+
case 'number': {
|
|
68
|
+
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
|
69
|
+
if (Number.isNaN(num)) return String(value);
|
|
70
|
+
|
|
71
|
+
const options: Intl.NumberFormatOptions = {
|
|
72
|
+
minimumFractionDigits: field.decimals ?? 0,
|
|
73
|
+
maximumFractionDigits: field.decimals ?? 2,
|
|
74
|
+
};
|
|
75
|
+
return new Intl.NumberFormat('en-US', options).format(num);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case 'currency': {
|
|
79
|
+
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
|
80
|
+
if (Number.isNaN(num)) return String(value);
|
|
81
|
+
|
|
82
|
+
const options: Intl.NumberFormatOptions = {
|
|
83
|
+
style: 'currency',
|
|
84
|
+
currency: field.currency || 'USD',
|
|
85
|
+
minimumFractionDigits: field.decimals ?? 0,
|
|
86
|
+
maximumFractionDigits: field.decimals ?? 0,
|
|
87
|
+
};
|
|
88
|
+
return new Intl.NumberFormat('en-US', options).format(num);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case 'percent': {
|
|
92
|
+
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
|
93
|
+
if (Number.isNaN(num)) return String(value);
|
|
94
|
+
|
|
95
|
+
// Assume value is already a percentage (e.g., 25 for 25%)
|
|
96
|
+
// If it's a decimal (e.g., 0.25), multiply by 100
|
|
97
|
+
const pct = num < 1 && num > -1 && num !== 0 ? num * 100 : num;
|
|
98
|
+
const decimals = field.decimals ?? 1;
|
|
99
|
+
return `${pct.toFixed(decimals)}%`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case 'date': {
|
|
103
|
+
if (!value) return '\u2014';
|
|
104
|
+
|
|
105
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
106
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
107
|
+
|
|
108
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
109
|
+
year: 'numeric',
|
|
110
|
+
month: 'short',
|
|
111
|
+
day: 'numeric',
|
|
112
|
+
}).format(date);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case 'boolean': {
|
|
116
|
+
const bool = typeof value === 'boolean' ? value : value === 'true' || value === '1';
|
|
117
|
+
return bool ? 'Yes' : 'No';
|
|
118
|
+
}
|
|
119
|
+
default:
|
|
120
|
+
return String(value);
|
|
89
121
|
}
|
|
90
|
-
|
|
91
|
-
case 'percent': {
|
|
92
|
-
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
|
93
|
-
if (isNaN(num)) return String(value);
|
|
94
|
-
|
|
95
|
-
// Assume value is already a percentage (e.g., 25 for 25%)
|
|
96
|
-
// If it's a decimal (e.g., 0.25), multiply by 100
|
|
97
|
-
const pct = num < 1 && num > -1 && num !== 0 ? num * 100 : num;
|
|
98
|
-
const decimals = field.decimals ?? 1;
|
|
99
|
-
return `${pct.toFixed(decimals)}%`;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
case 'date': {
|
|
103
|
-
if (!value) return '\u2014';
|
|
104
|
-
|
|
105
|
-
const date = value instanceof Date ? value : new Date(String(value));
|
|
106
|
-
if (isNaN(date.getTime())) return String(value);
|
|
107
|
-
|
|
108
|
-
return new Intl.DateTimeFormat('en-US', {
|
|
109
|
-
year: 'numeric',
|
|
110
|
-
month: 'short',
|
|
111
|
-
day: 'numeric',
|
|
112
|
-
}).format(date);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
case 'boolean': {
|
|
116
|
-
const bool = typeof value === 'boolean' ? value : value === 'true' || value === '1';
|
|
117
|
-
return bool ? 'Yes' : 'No';
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
case 'text':
|
|
121
|
-
default:
|
|
122
|
-
return String(value);
|
|
123
|
-
}
|
|
124
122
|
}
|
|
125
123
|
|
|
126
124
|
/**
|
|
127
125
|
* FieldRenderer component
|
|
128
126
|
* Displays a field with label, formatted value, and optional highlighting
|
|
129
127
|
*/
|
|
130
|
-
export function FieldRenderer({
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
128
|
+
export function FieldRenderer({ field, value, onUpdate, agentMode }: FieldRendererProps): ReactElement {
|
|
129
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
130
|
+
|
|
131
|
+
const formattedValue = useMemo(() => formatValue(value, field), [value, field]);
|
|
132
|
+
|
|
133
|
+
const isNull = value === null || value === undefined;
|
|
134
|
+
const isEditable = field.editable && (onUpdate || agentMode);
|
|
135
|
+
|
|
136
|
+
const handleClick = useCallback(() => {
|
|
137
|
+
if (!isEditable) return;
|
|
138
|
+
|
|
139
|
+
if (agentMode?.enabled && agentMode.sendMessage) {
|
|
140
|
+
// Send message to agent for editing
|
|
141
|
+
agentMode.sendMessage(
|
|
142
|
+
`Please help me update the "${field.label}" field (key: ${field.key}). Current value: ${formattedValue}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
// Direct edit mode would go here (Phase 2)
|
|
146
|
+
}, [isEditable, agentMode, field, formattedValue]);
|
|
147
|
+
|
|
148
|
+
const valueStyle = useMemo(() => {
|
|
149
|
+
const base = { ...styles.value };
|
|
150
|
+
|
|
151
|
+
if (isNull) {
|
|
152
|
+
return { ...base, ...styles.nullValue };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (field.highlight) {
|
|
156
|
+
return { ...base, ...styles.highlight[field.highlight] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (isEditable) {
|
|
160
|
+
return {
|
|
161
|
+
...base,
|
|
162
|
+
...styles.editable,
|
|
163
|
+
backgroundColor: isHovered ? 'var(--gray-3, #f3f4f6)' : 'transparent',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return base;
|
|
168
|
+
}, [isNull, field.highlight, isEditable, isHovered]);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: hover handlers used for visual feedback only; interactive click+keyboard only added when isEditable is true.
|
|
172
|
+
<div
|
|
173
|
+
style={styles.container}
|
|
174
|
+
title={field.tooltip}
|
|
175
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
176
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
177
|
+
onClick={handleClick}
|
|
178
|
+
onKeyDown={
|
|
179
|
+
isEditable
|
|
180
|
+
? (e) => {
|
|
181
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
handleClick();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
: undefined
|
|
187
|
+
}
|
|
188
|
+
role={isEditable ? 'button' : undefined}
|
|
189
|
+
tabIndex={isEditable ? 0 : undefined}
|
|
190
|
+
>
|
|
191
|
+
<span style={styles.label}>{field.label}</span>
|
|
192
|
+
<span style={valueStyle}>
|
|
193
|
+
{formattedValue}
|
|
194
|
+
{field.unit && !isNull && <span style={styles.unit}>{field.unit}</span>}
|
|
195
|
+
</span>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
199
198
|
}
|