@principal-ai/principal-view-react 0.14.13 → 0.14.15

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 (74) hide show
  1. package/dist/components/GraphRenderer.d.ts +15 -0
  2. package/dist/components/GraphRenderer.d.ts.map +1 -1
  3. package/dist/components/GraphRenderer.js +34 -4
  4. package/dist/components/GraphRenderer.js.map +1 -1
  5. package/dist/components/dashboard/DashboardRenderer.d.ts +9 -0
  6. package/dist/components/dashboard/DashboardRenderer.d.ts.map +1 -0
  7. package/dist/components/dashboard/DashboardRenderer.js +179 -0
  8. package/dist/components/dashboard/DashboardRenderer.js.map +1 -0
  9. package/dist/components/dashboard/MetricPanel.d.ts +9 -0
  10. package/dist/components/dashboard/MetricPanel.d.ts.map +1 -0
  11. package/dist/components/dashboard/MetricPanel.js +103 -0
  12. package/dist/components/dashboard/MetricPanel.js.map +1 -0
  13. package/dist/components/dashboard/MockDataProvider.d.ts +30 -0
  14. package/dist/components/dashboard/MockDataProvider.d.ts.map +1 -0
  15. package/dist/components/dashboard/MockDataProvider.js +270 -0
  16. package/dist/components/dashboard/MockDataProvider.js.map +1 -0
  17. package/dist/components/dashboard/components/BarChart.d.ts +9 -0
  18. package/dist/components/dashboard/components/BarChart.d.ts.map +1 -0
  19. package/dist/components/dashboard/components/BarChart.js +167 -0
  20. package/dist/components/dashboard/components/BarChart.js.map +1 -0
  21. package/dist/components/dashboard/components/LineChart.d.ts +9 -0
  22. package/dist/components/dashboard/components/LineChart.d.ts.map +1 -0
  23. package/dist/components/dashboard/components/LineChart.js +141 -0
  24. package/dist/components/dashboard/components/LineChart.js.map +1 -0
  25. package/dist/components/dashboard/components/MetricCard.d.ts +8 -0
  26. package/dist/components/dashboard/components/MetricCard.d.ts.map +1 -0
  27. package/dist/components/dashboard/components/MetricCard.js +163 -0
  28. package/dist/components/dashboard/components/MetricCard.js.map +1 -0
  29. package/dist/components/dashboard/components/SourceLink.d.ts +8 -0
  30. package/dist/components/dashboard/components/SourceLink.d.ts.map +1 -0
  31. package/dist/components/dashboard/components/SourceLink.js +39 -0
  32. package/dist/components/dashboard/components/SourceLink.js.map +1 -0
  33. package/dist/components/dashboard/components/TimeRangeSelector.d.ts +8 -0
  34. package/dist/components/dashboard/components/TimeRangeSelector.d.ts.map +1 -0
  35. package/dist/components/dashboard/components/TimeRangeSelector.js +167 -0
  36. package/dist/components/dashboard/components/TimeRangeSelector.js.map +1 -0
  37. package/dist/components/dashboard/components/index.d.ts +6 -0
  38. package/dist/components/dashboard/components/index.d.ts.map +1 -0
  39. package/dist/components/dashboard/components/index.js +6 -0
  40. package/dist/components/dashboard/components/index.js.map +1 -0
  41. package/dist/components/dashboard/index.d.ts +6 -0
  42. package/dist/components/dashboard/index.d.ts.map +1 -0
  43. package/dist/components/dashboard/index.js +8 -0
  44. package/dist/components/dashboard/index.js.map +1 -0
  45. package/dist/components/dashboard/types.d.ts +74 -0
  46. package/dist/components/dashboard/types.d.ts.map +1 -0
  47. package/dist/components/dashboard/types.js +8 -0
  48. package/dist/components/dashboard/types.js.map +1 -0
  49. package/dist/edges/CustomEdge.d.ts.map +1 -1
  50. package/dist/edges/CustomEdge.js +1 -0
  51. package/dist/edges/CustomEdge.js.map +1 -1
  52. package/dist/index.d.ts +2 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +2 -0
  55. package/dist/index.js.map +1 -1
  56. package/package.json +3 -3
  57. package/src/components/GraphRenderer.tsx +65 -1
  58. package/src/components/dashboard/DashboardRenderer.tsx +317 -0
  59. package/src/components/dashboard/MetricPanel.tsx +243 -0
  60. package/src/components/dashboard/MockDataProvider.ts +330 -0
  61. package/src/components/dashboard/components/BarChart.tsx +299 -0
  62. package/src/components/dashboard/components/LineChart.tsx +279 -0
  63. package/src/components/dashboard/components/MetricCard.tsx +270 -0
  64. package/src/components/dashboard/components/SourceLink.tsx +63 -0
  65. package/src/components/dashboard/components/TimeRangeSelector.tsx +280 -0
  66. package/src/components/dashboard/components/index.ts +5 -0
  67. package/src/components/dashboard/index.ts +47 -0
  68. package/src/components/dashboard/types.ts +126 -0
  69. package/src/edges/CustomEdge.tsx +1 -0
  70. package/src/index.ts +49 -0
  71. package/src/stories/CanvasEdgeTypes.stories.tsx +159 -2
  72. package/src/stories/dashboard/DashboardRenderer.stories.tsx +263 -0
  73. package/src/stories/dashboard/sample-dashboards/activity-feed-analytics.dashboard.json +300 -0
  74. package/src/stories/data/graph-converter-test-execution.json +49 -49
@@ -0,0 +1,279 @@
1
+ /**
2
+ * LineChart
3
+ *
4
+ * Simple SVG-based line chart for time series data.
5
+ * For prototyping - can be replaced with a more robust charting library later.
6
+ */
7
+
8
+ import { useMemo } from 'react';
9
+ import { useTheme } from '@principal-ade/industry-theme';
10
+ import type { LineChartProps } from '../types';
11
+
12
+ const CHART_COLORS = [
13
+ '#3b82f6', // blue
14
+ '#22c55e', // green
15
+ '#f59e0b', // amber
16
+ '#ef4444', // red
17
+ '#8b5cf6', // violet
18
+ '#ec4899', // pink
19
+ ];
20
+
21
+ export function LineChart({
22
+ title,
23
+ data,
24
+ xKey = 'date',
25
+ yKey = 'value',
26
+ series,
27
+ unit,
28
+ height = 200,
29
+ onClick,
30
+ }: LineChartProps) {
31
+ const { theme } = useTheme();
32
+
33
+ const padding = { top: 20, right: 20, bottom: 40, left: 60 };
34
+ const chartWidth = 400;
35
+ const chartHeight = height;
36
+
37
+ const { paths, yAxisLabels, xAxisLabels } = useMemo(() => {
38
+ if (!data || data.length === 0) {
39
+ return { paths: [], yAxisLabels: [], xAxisLabels: [], maxValue: 0 };
40
+ }
41
+
42
+ // Determine which keys to plot
43
+ const keys = series || [yKey];
44
+
45
+ // Find max value across all series
46
+ let max = 0;
47
+ for (const point of data) {
48
+ for (const key of keys) {
49
+ const val = point[key];
50
+ if (typeof val === 'number' && val > max) {
51
+ max = val;
52
+ }
53
+ }
54
+ }
55
+
56
+ // Add 10% padding to max
57
+ max = max * 1.1;
58
+
59
+ const innerWidth = chartWidth - padding.left - padding.right;
60
+ const innerHeight = chartHeight - padding.top - padding.bottom;
61
+
62
+ // Generate paths for each series
63
+ const pathsResult: Array<{ path: string; color: string; label: string }> = [];
64
+
65
+ keys.forEach((key, keyIndex) => {
66
+ const points: Array<{ x: number; y: number }> = [];
67
+
68
+ data.forEach((point, i) => {
69
+ const val = point[key];
70
+ if (typeof val === 'number') {
71
+ const x = padding.left + (i / (data.length - 1 || 1)) * innerWidth;
72
+ const y = padding.top + innerHeight - (val / max) * innerHeight;
73
+ points.push({ x, y });
74
+ }
75
+ });
76
+
77
+ if (points.length > 0) {
78
+ const pathD = points
79
+ .map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`)
80
+ .join(' ');
81
+
82
+ pathsResult.push({
83
+ path: pathD,
84
+ color: CHART_COLORS[keyIndex % CHART_COLORS.length],
85
+ label: key,
86
+ });
87
+ }
88
+ });
89
+
90
+ // Y-axis labels (5 ticks)
91
+ const yLabels: Array<{ value: string; y: number }> = [];
92
+ for (let i = 0; i <= 4; i++) {
93
+ const value = (max / 4) * (4 - i);
94
+ const y = padding.top + (i / 4) * innerHeight;
95
+ yLabels.push({
96
+ value: formatNumber(value),
97
+ y,
98
+ });
99
+ }
100
+
101
+ // X-axis labels (show first, middle, last)
102
+ const xLabels: Array<{ value: string; x: number }> = [];
103
+ const indices = [0, Math.floor(data.length / 2), data.length - 1];
104
+ for (const idx of indices) {
105
+ if (data[idx]) {
106
+ const x = padding.left + (idx / (data.length - 1 || 1)) * innerWidth;
107
+ xLabels.push({
108
+ value: String(data[idx][xKey] || ''),
109
+ x,
110
+ });
111
+ }
112
+ }
113
+
114
+ return {
115
+ paths: pathsResult,
116
+ yAxisLabels: yLabels,
117
+ xAxisLabels: xLabels,
118
+ maxValue: max,
119
+ };
120
+ }, [data, series, yKey, xKey, chartWidth, chartHeight]);
121
+
122
+ return (
123
+ <div
124
+ onClick={onClick}
125
+ style={{
126
+ backgroundColor: theme.colors.surface || theme.colors.background,
127
+ border: `1px solid ${theme.colors.border}`,
128
+ borderRadius: theme.radii?.[2] || 8,
129
+ padding: theme.space?.[3] || 16,
130
+ cursor: onClick ? 'pointer' : 'default',
131
+ fontFamily: theme.fonts.body,
132
+ }}
133
+ >
134
+ {/* Title */}
135
+ <div
136
+ style={{
137
+ fontSize: theme.fontSizes[1],
138
+ fontWeight: theme.fontWeights.medium,
139
+ color: theme.colors.text,
140
+ marginBottom: theme.space?.[3] || 16,
141
+ }}
142
+ >
143
+ {title}
144
+ {unit && (
145
+ <span style={{ color: theme.colors.textSecondary, fontWeight: theme.fontWeights.body }}>
146
+ {' '}
147
+ ({unit})
148
+ </span>
149
+ )}
150
+ </div>
151
+
152
+ {/* Chart */}
153
+ <svg
154
+ width="100%"
155
+ height={chartHeight}
156
+ viewBox={`0 0 ${chartWidth} ${chartHeight}`}
157
+ preserveAspectRatio="xMidYMid meet"
158
+ >
159
+ {/* Grid lines */}
160
+ {yAxisLabels.map((label, i) => (
161
+ <line
162
+ key={i}
163
+ x1={padding.left}
164
+ y1={label.y}
165
+ x2={chartWidth - padding.right}
166
+ y2={label.y}
167
+ stroke={theme.colors.border}
168
+ strokeDasharray="4 4"
169
+ opacity={0.5}
170
+ />
171
+ ))}
172
+
173
+ {/* Y-axis labels */}
174
+ {yAxisLabels.map((label, i) => (
175
+ <text
176
+ key={i}
177
+ x={padding.left - 8}
178
+ y={label.y}
179
+ textAnchor="end"
180
+ dominantBaseline="middle"
181
+ fontSize={theme.fontSizes[0]}
182
+ fontFamily={theme.fonts.monospace}
183
+ fill={theme.colors.textSecondary}
184
+ >
185
+ {label.value}
186
+ </text>
187
+ ))}
188
+
189
+ {/* X-axis labels */}
190
+ {xAxisLabels.map((label, i) => (
191
+ <text
192
+ key={i}
193
+ x={label.x}
194
+ y={chartHeight - padding.bottom + 20}
195
+ textAnchor="middle"
196
+ fontSize={theme.fontSizes[0]}
197
+ fontFamily={theme.fonts.monospace}
198
+ fill={theme.colors.textSecondary}
199
+ >
200
+ {label.value}
201
+ </text>
202
+ ))}
203
+
204
+ {/* Lines */}
205
+ {paths.map((p, i) => (
206
+ <path
207
+ key={i}
208
+ d={p.path}
209
+ fill="none"
210
+ stroke={p.color}
211
+ strokeWidth={2}
212
+ strokeLinecap="round"
213
+ strokeLinejoin="round"
214
+ />
215
+ ))}
216
+
217
+ {/* Data points */}
218
+ {paths.map((p, pathIndex) => {
219
+ const points = p.path.split(/[ML]/).filter(Boolean);
220
+ return points.map((point, i) => {
221
+ const [x, y] = point.trim().split(' ').map(Number);
222
+ return (
223
+ <circle
224
+ key={`${pathIndex}-${i}`}
225
+ cx={x}
226
+ cy={y}
227
+ r={3}
228
+ fill={p.color}
229
+ />
230
+ );
231
+ });
232
+ })}
233
+ </svg>
234
+
235
+ {/* Legend */}
236
+ {series && series.length > 1 && (
237
+ <div
238
+ style={{
239
+ display: 'flex',
240
+ gap: theme.space?.[3] || 16,
241
+ marginTop: theme.space?.[3] || 12,
242
+ justifyContent: 'center',
243
+ }}
244
+ >
245
+ {series.map((s, i) => (
246
+ <div
247
+ key={s}
248
+ style={{
249
+ display: 'flex',
250
+ alignItems: 'center',
251
+ gap: 6,
252
+ fontSize: theme.fontSizes[0],
253
+ color: theme.colors.textSecondary,
254
+ fontFamily: theme.fonts.body,
255
+ }}
256
+ >
257
+ <div
258
+ style={{
259
+ width: 12,
260
+ height: 3,
261
+ backgroundColor: CHART_COLORS[i % CHART_COLORS.length],
262
+ borderRadius: 2,
263
+ }}
264
+ />
265
+ {s}
266
+ </div>
267
+ ))}
268
+ </div>
269
+ )}
270
+ </div>
271
+ );
272
+ }
273
+
274
+ function formatNumber(value: number): string {
275
+ if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
276
+ if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;
277
+ if (value % 1 !== 0) return value.toFixed(1);
278
+ return Math.round(value).toString();
279
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * MetricCard
3
+ *
4
+ * Displays a single metric value with optional trend indicator and sparkline.
5
+ */
6
+
7
+ import { useTheme } from '@principal-ade/industry-theme';
8
+ import type { MetricCardProps } from '../types';
9
+
10
+ export function MetricCard({
11
+ title,
12
+ value,
13
+ unit,
14
+ description,
15
+ trend,
16
+ changePercent,
17
+ showTrend = true,
18
+ showSparkline = false,
19
+ sparklineData,
20
+ thresholds,
21
+ size = 'medium',
22
+ onClick,
23
+ }: MetricCardProps) {
24
+ const { theme } = useTheme();
25
+
26
+ // Font size indices into theme.fontSizes array
27
+ // Typical array: [12, 14, 16, 20, 24, 32, 48]
28
+ const sizeConfig = {
29
+ small: {
30
+ padding: '12px 16px',
31
+ titleIndex: 0, // ~12px
32
+ valueIndex: 4, // ~24px
33
+ unitIndex: 0, // ~12px
34
+ trendIndex: 0, // ~12px
35
+ },
36
+ medium: {
37
+ padding: '16px 20px',
38
+ titleIndex: 1, // ~14px
39
+ valueIndex: 5, // ~32px
40
+ unitIndex: 1, // ~14px
41
+ trendIndex: 0, // ~12px
42
+ },
43
+ large: {
44
+ padding: '20px 24px',
45
+ titleIndex: 2, // ~16px
46
+ valueIndex: 6, // ~48px
47
+ unitIndex: 2, // ~16px
48
+ trendIndex: 1, // ~14px
49
+ },
50
+ };
51
+
52
+ const config = sizeConfig[size];
53
+
54
+ const getValueColor = () => {
55
+ if (!thresholds || typeof value !== 'number') {
56
+ return theme.colors.text;
57
+ }
58
+ if (thresholds.critical !== undefined && value >= thresholds.critical) {
59
+ return theme.colors.error;
60
+ }
61
+ if (thresholds.warning !== undefined && value >= thresholds.warning) {
62
+ return theme.colors.warning;
63
+ }
64
+ return theme.colors.success;
65
+ };
66
+
67
+ const getTrendIcon = () => {
68
+ if (!trend || !showTrend) return null;
69
+
70
+ const iconSize = size === 'small' ? 12 : 16;
71
+ const iconStyle: React.CSSProperties = {
72
+ width: iconSize,
73
+ height: iconSize,
74
+ marginRight: 4,
75
+ };
76
+
77
+ if (trend === 'up') {
78
+ return (
79
+ <svg style={iconStyle} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
80
+ <path d="M7 17l5-5 5 5M7 7l5 5 5-5" />
81
+ </svg>
82
+ );
83
+ }
84
+ if (trend === 'down') {
85
+ return (
86
+ <svg style={iconStyle} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
87
+ <path d="M7 7l5 5 5-5M7 17l5-5 5 5" />
88
+ </svg>
89
+ );
90
+ }
91
+ return (
92
+ <svg style={iconStyle} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
93
+ <path d="M5 12h14" />
94
+ </svg>
95
+ );
96
+ };
97
+
98
+ const getTrendColor = () => {
99
+ if (!trend) return theme.colors.textSecondary;
100
+ if (trend === 'up') return theme.colors.success;
101
+ if (trend === 'down') return theme.colors.error;
102
+ return theme.colors.textSecondary;
103
+ };
104
+
105
+ const formatValue = (val: number | string): string => {
106
+ if (typeof val === 'string') return val;
107
+ if (val >= 1000000) return `${(val / 1000000).toFixed(1)}M`;
108
+ if (val >= 1000) return `${(val / 1000).toFixed(1)}K`;
109
+ if (val % 1 !== 0) return val.toFixed(1);
110
+ return val.toString();
111
+ };
112
+
113
+ const renderSparkline = () => {
114
+ if (!showSparkline || !sparklineData || sparklineData.length < 2) return null;
115
+
116
+ const values = sparklineData
117
+ .map((point) => point.value)
118
+ .filter((v): v is number => typeof v === 'number');
119
+
120
+ if (values.length < 2) return null;
121
+
122
+ const minVal = Math.min(...values);
123
+ const maxVal = Math.max(...values);
124
+ const range = maxVal - minVal || 1;
125
+
126
+ const width = size === 'small' ? 100 : size === 'medium' ? 140 : 180;
127
+ const height = size === 'small' ? 24 : size === 'medium' ? 32 : 40;
128
+ const padding = 2;
129
+
130
+ const points = values.map((val, i) => {
131
+ const x = padding + (i / (values.length - 1)) * (width - padding * 2);
132
+ const y = height - padding - ((val - minVal) / range) * (height - padding * 2);
133
+ return `${x},${y}`;
134
+ });
135
+
136
+ const pathD = `M ${points.join(' L ')}`;
137
+
138
+ // Create gradient fill path
139
+ const fillPoints = [
140
+ `${padding},${height - padding}`,
141
+ ...points,
142
+ `${width - padding},${height - padding}`,
143
+ ];
144
+ const fillD = `M ${fillPoints.join(' L ')} Z`;
145
+
146
+ const lineColor = trend === 'up' ? theme.colors.success : trend === 'down' ? theme.colors.error : theme.colors.primary || theme.colors.accent;
147
+
148
+ return (
149
+ <div style={{ marginTop: theme.space?.[2] || 8 }}>
150
+ <svg width={width} height={height} style={{ display: 'block' }}>
151
+ <defs>
152
+ <linearGradient id={`sparkline-gradient-${title}`} x1="0%" y1="0%" x2="0%" y2="100%">
153
+ <stop offset="0%" stopColor={lineColor} stopOpacity={0.3} />
154
+ <stop offset="100%" stopColor={lineColor} stopOpacity={0.05} />
155
+ </linearGradient>
156
+ </defs>
157
+ <path d={fillD} fill={`url(#sparkline-gradient-${title})`} />
158
+ <path d={pathD} fill="none" stroke={lineColor} strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" />
159
+ </svg>
160
+ </div>
161
+ );
162
+ };
163
+
164
+ return (
165
+ <div
166
+ onClick={onClick}
167
+ style={{
168
+ backgroundColor: theme.colors.surface || theme.colors.background,
169
+ border: `1px solid ${theme.colors.border}`,
170
+ borderRadius: theme.radii?.[2] || 8,
171
+ padding: config.padding,
172
+ cursor: onClick ? 'pointer' : 'default',
173
+ transition: 'border-color 0.2s, box-shadow 0.2s',
174
+ minWidth: size === 'small' ? 140 : size === 'medium' ? 180 : 220,
175
+ fontFamily: theme.fonts.body,
176
+ }}
177
+ onMouseEnter={(e) => {
178
+ if (onClick) {
179
+ e.currentTarget.style.borderColor = theme.colors.primary || theme.colors.accent;
180
+ }
181
+ }}
182
+ onMouseLeave={(e) => {
183
+ e.currentTarget.style.borderColor = theme.colors.border;
184
+ }}
185
+ >
186
+ {/* Title */}
187
+ <div
188
+ style={{
189
+ fontSize: theme.fontSizes[config.titleIndex],
190
+ color: theme.colors.textSecondary,
191
+ marginBottom: theme.space?.[2] || 8,
192
+ fontWeight: theme.fontWeights.medium,
193
+ }}
194
+ >
195
+ {title}
196
+ </div>
197
+
198
+ {/* Value */}
199
+ <div
200
+ style={{
201
+ display: 'flex',
202
+ alignItems: 'baseline',
203
+ gap: 4,
204
+ }}
205
+ >
206
+ <span
207
+ style={{
208
+ fontSize: theme.fontSizes[config.valueIndex],
209
+ fontWeight: theme.fontWeights.semibold,
210
+ color: getValueColor(),
211
+ fontVariantNumeric: 'tabular-nums',
212
+ fontFamily: theme.fonts.monospace,
213
+ }}
214
+ >
215
+ {formatValue(value)}
216
+ </span>
217
+ {unit && (
218
+ <span
219
+ style={{
220
+ fontSize: theme.fontSizes[config.unitIndex],
221
+ color: theme.colors.textSecondary,
222
+ }}
223
+ >
224
+ {unit}
225
+ </span>
226
+ )}
227
+ </div>
228
+
229
+ {/* Trend */}
230
+ {showTrend && (trend || changePercent !== undefined) && (
231
+ <div
232
+ style={{
233
+ display: 'flex',
234
+ alignItems: 'center',
235
+ marginTop: theme.space?.[2] || 8,
236
+ fontSize: theme.fontSizes[config.trendIndex],
237
+ color: getTrendColor(),
238
+ fontFamily: theme.fonts.monospace,
239
+ }}
240
+ >
241
+ {getTrendIcon()}
242
+ {changePercent !== undefined && (
243
+ <span>
244
+ {changePercent > 0 ? '+' : ''}
245
+ {changePercent.toFixed(1)}%
246
+ </span>
247
+ )}
248
+ </div>
249
+ )}
250
+
251
+ {/* Description */}
252
+ {description && (
253
+ <div
254
+ style={{
255
+ fontSize: theme.fontSizes[config.trendIndex],
256
+ color: theme.colors.textSecondary,
257
+ marginTop: theme.space?.[2] || 8,
258
+ lineHeight: theme.lineHeights.body,
259
+ opacity: 0.8,
260
+ }}
261
+ >
262
+ {description}
263
+ </div>
264
+ )}
265
+
266
+ {/* Sparkline */}
267
+ {renderSparkline()}
268
+ </div>
269
+ );
270
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * SourceLink
3
+ *
4
+ * Displays a link to the workflow/canvas that sources a metric.
5
+ */
6
+
7
+ import { useTheme } from '@principal-ade/industry-theme';
8
+ import type { SourceLinkProps } from '../types';
9
+
10
+ export function SourceLink({ source, onClick }: SourceLinkProps) {
11
+ const { theme } = useTheme();
12
+
13
+ const formatSource = () => {
14
+ const parts = [source.storyboard, source.workflow];
15
+ if (source.nodes && source.nodes.length > 0) {
16
+ parts.push(source.nodes.join(', '));
17
+ }
18
+ if (source.event) {
19
+ parts.push(source.event);
20
+ }
21
+ return parts.join(' → ');
22
+ };
23
+
24
+ return (
25
+ <button
26
+ onClick={() => onClick?.(source)}
27
+ style={{
28
+ display: 'inline-flex',
29
+ alignItems: 'center',
30
+ gap: 6,
31
+ padding: '4px 8px',
32
+ fontSize: theme.fontSizes[0],
33
+ fontFamily: theme.fonts.body,
34
+ color: theme.colors.primary || theme.colors.accent,
35
+ backgroundColor: 'transparent',
36
+ border: `1px solid ${theme.colors.border}`,
37
+ borderRadius: theme.radii?.[1] || 4,
38
+ cursor: 'pointer',
39
+ transition: 'background-color 0.2s',
40
+ }}
41
+ onMouseEnter={(e) => {
42
+ e.currentTarget.style.backgroundColor = theme.colors.surface || theme.colors.background;
43
+ }}
44
+ onMouseLeave={(e) => {
45
+ e.currentTarget.style.backgroundColor = 'transparent';
46
+ }}
47
+ >
48
+ {/* Link icon */}
49
+ <svg
50
+ width={12}
51
+ height={12}
52
+ viewBox="0 0 24 24"
53
+ fill="none"
54
+ stroke="currentColor"
55
+ strokeWidth={2}
56
+ >
57
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
58
+ <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
59
+ </svg>
60
+ <span style={{ fontFamily: theme.fonts.monospace }}>{formatSource()}</span>
61
+ </button>
62
+ );
63
+ }