@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
@@ -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
- /** Chart template configuration */
14
- chart: ChartTemplate;
15
- /** Data context for dataKey resolution */
16
- data: Record<string, unknown>;
17
- /** CSS class name */
18
- className?: string;
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
- chart,
28
- data,
29
- className
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
- // Resolve data from context if dataKey is provided
36
- const resolvedSpec = { ...chart.spec };
31
+ // Resolve data from context if dataKey is provided
32
+ const resolvedSpec = { ...chart.spec };
37
33
 
38
- if (chart.dataKey && data[chart.dataKey]) {
39
- const chartData = data[chart.dataKey];
40
- if (Array.isArray(chartData)) {
41
- // Inject data into spec
42
- resolvedSpec.data = { values: chartData as Record<string, unknown>[] };
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
- // Build the VegaLiteChartSpec format expected by @vertesia/ui
47
- const vegaSpec = {
48
- library: 'vega-lite' as const,
49
- title: chart.title,
50
- description: chart.description,
51
- spec: resolvedSpec,
52
- options: {
53
- height: chart.height,
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
- // If a ChartComponent is provided via context, use it to render the actual chart
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
- style={{
81
- fontSize: '14px',
82
- fontWeight: 600,
83
- marginBottom: '8px',
84
- color: 'var(--gray-12, #111827)'
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
- {chart.title}
88
- </div>
89
- )}
90
- {chart.description && (
91
- <div
92
- style={{
93
- fontSize: '12px',
94
- color: 'var(--gray-11, #6b7280)',
95
- marginBottom: '12px'
96
- }}
97
- >
98
- {chart.description}
99
- </div>
100
- )}
101
- <div
102
- style={{
103
- display: 'flex',
104
- alignItems: 'center',
105
- justifyContent: 'center',
106
- height: (chart.height || 280) - 60,
107
- backgroundColor: 'var(--gray-3, #f3f4f6)',
108
- borderRadius: '4px',
109
- color: 'var(--gray-11, #6b7280)',
110
- fontSize: '12px'
111
- }}
112
- data-vega-spec={JSON.stringify(vegaSpec)}
113
- >
114
- <div style={{ textAlign: 'center' }}>
115
- <div style={{ marginBottom: '4px' }}>
116
- Chart: {resolvedSpec.mark ? String(typeof resolvedSpec.mark === 'string' ? resolvedSpec.mark : resolvedSpec.mark.type) : 'composite'}
117
- </div>
118
- {resolvedSpec.data?.values && (
119
- <div style={{ fontSize: '11px', opacity: 0.8 }}>
120
- {resolvedSpec.data.values.length} data points
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
- </div>
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 { useMemo, useState, useCallback, type ReactElement } from 'react';
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
- 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
- },
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
- if (value === null || value === undefined) {
63
- return '\u2014'; // em-dash for null/undefined
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
- case 'currency': {
79
- const num = typeof value === 'number' ? value : parseFloat(String(value));
80
- if (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);
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
- field,
132
- value,
133
- onUpdate,
134
- agentMode,
135
- }: FieldRendererProps): ReactElement {
136
- const [isHovered, setIsHovered] = useState(false);
137
-
138
- const formattedValue = useMemo(
139
- () => formatValue(value, field),
140
- [value, field]
141
- );
142
-
143
- const isNull = value === null || value === undefined;
144
- const isEditable = field.editable && (onUpdate || agentMode);
145
-
146
- const handleClick = useCallback(() => {
147
- if (!isEditable) return;
148
-
149
- if (agentMode?.enabled && agentMode.sendMessage) {
150
- // Send message to agent for editing
151
- agentMode.sendMessage(
152
- `Please help me update the "${field.label}" field (key: ${field.key}). Current value: ${formattedValue}`
153
- );
154
- }
155
- // Direct edit mode would go here (Phase 2)
156
- }, [isEditable, agentMode, field, formattedValue]);
157
-
158
- const valueStyle = useMemo(() => {
159
- const base = { ...styles.value };
160
-
161
- if (isNull) {
162
- return { ...base, ...styles.nullValue };
163
- }
164
-
165
- if (field.highlight) {
166
- return { ...base, ...styles.highlight[field.highlight] };
167
- }
168
-
169
- if (isEditable) {
170
- return {
171
- ...base,
172
- ...styles.editable,
173
- backgroundColor: isHovered ? 'var(--gray-3, #f3f4f6)' : 'transparent',
174
- };
175
- }
176
-
177
- return base;
178
- }, [isNull, field.highlight, isEditable, isHovered]);
179
-
180
- return (
181
- <div
182
- style={styles.container}
183
- title={field.tooltip}
184
- onMouseEnter={() => setIsHovered(true)}
185
- onMouseLeave={() => setIsHovered(false)}
186
- onClick={handleClick}
187
- role={isEditable ? 'button' : undefined}
188
- tabIndex={isEditable ? 0 : undefined}
189
- >
190
- <span style={styles.label}>{field.label}</span>
191
- <span style={valueStyle}>
192
- {formattedValue}
193
- {field.unit && !isNull && (
194
- <span style={styles.unit}>{field.unit}</span>
195
- )}
196
- </span>
197
- </div>
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
  }