linecharts-demo 1.0.1

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/src/App.tsx ADDED
@@ -0,0 +1,965 @@
1
+ import React, { useState, useRef, useMemo } from 'react';
2
+ import {
3
+ LineChart,
4
+ MultiAxisLineChart,
5
+ SteppedLineChart,
6
+ SegmentedLineChart,
7
+ PointStyleLineChart,
8
+ } from 'unisys-barcharts';
9
+
10
+ // Unisys Design System Colors
11
+ const colors = {
12
+ primary: '#003134',
13
+ secondary: '#007173',
14
+ accent: '#00E28B',
15
+ success: '#73EFBF',
16
+ warning: '#FFE085',
17
+ error: '#FC7673',
18
+ info: '#B2FFFF',
19
+ bgApp: '#E5EAEB',
20
+ bgHeader: '#E8EDED',
21
+ bgContent: '#F3F6F6',
22
+ textPrimary: '#003134',
23
+ textSecondary: '#007173',
24
+ textSupporting: '#555555',
25
+ };
26
+
27
+ type LineChartType = 'basic' | 'multi-axis' | 'stepped' | 'segmented' | 'point-style';
28
+ type InterpolationMode = 'linear' | 'monotone' | 'step' | 'basis' | 'cardinal' | 'catmull-rom';
29
+ type ThemeType = 'default' | 'dark' | 'pastel' | 'vibrant';
30
+
31
+ interface DataPoint {
32
+ label: string;
33
+ value: number;
34
+ value2?: number;
35
+ }
36
+
37
+ const App: React.FC = () => {
38
+ const chartRef = useRef<HTMLDivElement>(null);
39
+ const [activeTab, setActiveTab] = useState<'preview' | 'code'>('preview');
40
+ const [copied, setCopied] = useState(false);
41
+
42
+ // Chart configuration state
43
+ const [lineChartType, setLineChartType] = useState<LineChartType>('basic');
44
+ const [chartTitle, setChartTitle] = useState('My Custom Chart');
45
+ const [theme, setTheme] = useState<ThemeType>('default');
46
+ const [interpolation, setInterpolation] = useState<InterpolationMode>('monotone');
47
+ const [tension, setTension] = useState(0.4);
48
+ const [showPoints, setShowPoints] = useState(true);
49
+ const [fillArea, setFillArea] = useState(true);
50
+ const [showLegend, setShowLegend] = useState(true);
51
+ const [showSecondDataset, setShowSecondDataset] = useState(false);
52
+ const [steppedMode, setSteppedMode] = useState<'before' | 'after' | 'middle'>('after');
53
+
54
+ // Data points
55
+ const [dataPoints, setDataPoints] = useState<DataPoint[]>([
56
+ { label: 'Jan', value: 400, value2: 300 },
57
+ { label: 'Feb', value: 300, value2: 400 },
58
+ { label: 'Mar', value: 600, value2: 350 },
59
+ { label: 'Apr', value: 800, value2: 500 },
60
+ { label: 'May', value: 500, value2: 600 },
61
+ { label: 'Jun', value: 700, value2: 450 },
62
+ ]);
63
+
64
+ const addDataPoint = () => {
65
+ const newLabel = `Point ${dataPoints.length + 1}`;
66
+ setDataPoints([...dataPoints, { label: newLabel, value: Math.floor(Math.random() * 500) + 100, value2: Math.floor(Math.random() * 500) + 100 }]);
67
+ };
68
+
69
+ const removeDataPoint = (index: number) => {
70
+ if (dataPoints.length > 2) {
71
+ setDataPoints(dataPoints.filter((_, i) => i !== index));
72
+ }
73
+ };
74
+
75
+ const updateDataPoint = (index: number, field: 'label' | 'value' | 'value2', value: string | number) => {
76
+ const newData = [...dataPoints];
77
+ if (field === 'label') {
78
+ newData[index].label = value as string;
79
+ } else if (field === 'value') {
80
+ newData[index].value = Number(value) || 0;
81
+ } else if (field === 'value2') {
82
+ newData[index].value2 = Number(value) || 0;
83
+ }
84
+ setDataPoints(newData);
85
+ };
86
+
87
+ // Generate dynamic code based on current configuration
88
+ const generatedCode = useMemo(() => {
89
+ const chartComponentMap: Record<LineChartType, string> = {
90
+ 'basic': 'LineChart',
91
+ 'multi-axis': 'MultiAxisLineChart',
92
+ 'stepped': 'SteppedLineChart',
93
+ 'segmented': 'SegmentedLineChart',
94
+ 'point-style': 'PointStyleLineChart',
95
+ };
96
+
97
+ const componentName = chartComponentMap[lineChartType];
98
+ const labels = dataPoints.map(d => `'${d.label}'`).join(', ');
99
+ const values = dataPoints.map(d => d.value).join(', ');
100
+ const values2 = dataPoints.map(d => d.value2 || 0).join(', ');
101
+
102
+ let datasetsCode = '';
103
+ let extraProps = '';
104
+
105
+ // Base dataset
106
+ const fillConfig = fillArea
107
+ ? `{ enabled: true, opacity: 0.3 }`
108
+ : `{ enabled: false }`;
109
+
110
+ if (lineChartType === 'multi-axis') {
111
+ datasetsCode = `[
112
+ {
113
+ label: 'Dataset 1',
114
+ data: [${values}],
115
+ borderColor: '#007173',
116
+ fill: ${fillConfig},
117
+ yAxisID: 'y1',
118
+ },${showSecondDataset ? `
119
+ {
120
+ label: 'Dataset 2',
121
+ data: [${values2}],
122
+ borderColor: '#00E28B',
123
+ fill: { enabled: ${fillArea}, opacity: 0.2 },
124
+ yAxisID: 'y2',
125
+ },` : ''}
126
+ ]`;
127
+ extraProps = `
128
+ yAxes={[
129
+ { id: 'y1', position: 'left', title: { text: 'Dataset 1' } },${showSecondDataset ? `
130
+ { id: 'y2', position: 'right', title: { text: 'Dataset 2' } },` : ''}
131
+ ]}`;
132
+ } else if (lineChartType === 'stepped') {
133
+ datasetsCode = `[
134
+ {
135
+ label: 'Dataset 1',
136
+ data: [${values}],
137
+ borderColor: '#007173',
138
+ fill: ${fillConfig},
139
+ },${showSecondDataset ? `
140
+ {
141
+ label: 'Dataset 2',
142
+ data: [${values2}],
143
+ borderColor: '#00E28B',
144
+ fill: { enabled: ${fillArea}, opacity: 0.2 },
145
+ },` : ''}
146
+ ]`;
147
+ extraProps = `
148
+ stepped="${steppedMode}"`;
149
+ } else if (lineChartType === 'segmented') {
150
+ datasetsCode = `[
151
+ {
152
+ label: 'Dataset 1',
153
+ data: [${values}],
154
+ borderColor: '#007173',
155
+ fill: ${fillConfig},
156
+ segmentStyle: (ctx) => ({
157
+ borderColor: ctx.p1.value > ctx.p0.value ? '#73EFBF' : '#FC7673',
158
+ }),
159
+ },${showSecondDataset ? `
160
+ {
161
+ label: 'Dataset 2',
162
+ data: [${values2}],
163
+ borderColor: '#00E28B',
164
+ fill: { enabled: ${fillArea}, opacity: 0.2 },
165
+ segmentStyle: (ctx) => ({
166
+ borderColor: ctx.p1.value > ctx.p0.value ? '#73EFBF' : '#FC7673',
167
+ }),
168
+ },` : ''}
169
+ ]`;
170
+ } else if (lineChartType === 'point-style') {
171
+ datasetsCode = `[
172
+ {
173
+ label: 'Dataset 1',
174
+ data: [${values}],
175
+ borderColor: '#007173',
176
+ fill: ${fillConfig},
177
+ point: { pointStyle: 'circle', radius: 8, hoverRadius: 12 },
178
+ },${showSecondDataset ? `
179
+ {
180
+ label: 'Dataset 2',
181
+ data: [${values2}],
182
+ borderColor: '#00E28B',
183
+ fill: { enabled: ${fillArea}, opacity: 0.2 },
184
+ point: { pointStyle: 'triangle', radius: 8, hoverRadius: 12 },
185
+ },` : ''}
186
+ ]`;
187
+ } else {
188
+ datasetsCode = `[
189
+ {
190
+ label: 'Dataset 1',
191
+ data: [${values}],
192
+ borderColor: '#007173',
193
+ fill: ${fillConfig},
194
+ },${showSecondDataset ? `
195
+ {
196
+ label: 'Dataset 2',
197
+ data: [${values2}],
198
+ borderColor: '#00E28B',
199
+ fill: { enabled: ${fillArea}, opacity: 0.2 },
200
+ },` : ''}
201
+ ]`;
202
+ }
203
+
204
+ const pointConfig = showPoints
205
+ ? `{ radius: 5, hoverRadius: 7 }`
206
+ : `{ radius: 0, hoverRadius: 0 }`;
207
+
208
+ return `// App.tsx - Complete CodeSandbox Example
209
+ // Install: npm install unisys-barcharts react react-dom
210
+
211
+ import React from 'react';
212
+ import { ${componentName} } from 'unisys-barcharts';
213
+
214
+ // Chart data
215
+ const labels = [${labels}];
216
+
217
+ const datasets = ${datasetsCode};
218
+
219
+ // Chart configuration
220
+ const chartConfig = {
221
+ title: "${chartTitle}",
222
+ theme: "${theme}",
223
+ interpolation: "${interpolation}",
224
+ tension: ${tension},
225
+ point: ${pointConfig},
226
+ legend: { display: ${showLegend}, position: 'bottom' as const },
227
+ tooltip: { enabled: true },
228
+ height: 400,
229
+ };
230
+
231
+ const App: React.FC = () => {
232
+ return (
233
+ <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
234
+ <h1 style={{ marginBottom: '20px' }}>${chartTitle}</h1>
235
+ <${componentName}
236
+ title={chartConfig.title}
237
+ labels={labels}
238
+ datasets={datasets}
239
+ theme={chartConfig.theme}
240
+ interpolation={chartConfig.interpolation}
241
+ tension={chartConfig.tension}
242
+ point={chartConfig.point}
243
+ legend={chartConfig.legend}
244
+ tooltip={chartConfig.tooltip}
245
+ height={chartConfig.height}${extraProps}
246
+ />
247
+ </div>
248
+ );
249
+ };
250
+
251
+ export default App;
252
+
253
+ /*
254
+ index.tsx - Entry point:
255
+
256
+ import React from 'react';
257
+ import ReactDOM from 'react-dom/client';
258
+ import App from './App';
259
+
260
+ const root = ReactDOM.createRoot(document.getElementById('root')!);
261
+ root.render(<App />);
262
+ */`;
263
+ }, [lineChartType, chartTitle, dataPoints, theme, interpolation, tension, showPoints, fillArea, showLegend, showSecondDataset, steppedMode]);
264
+
265
+ // Copy code to clipboard
266
+ const copyToClipboard = async () => {
267
+ try {
268
+ await navigator.clipboard.writeText(generatedCode);
269
+ setCopied(true);
270
+ setTimeout(() => setCopied(false), 2000);
271
+ } catch (err) {
272
+ console.error('Failed to copy:', err);
273
+ }
274
+ };
275
+
276
+ // Download functions
277
+ const downloadSVG = () => {
278
+ if (!chartRef.current) return;
279
+ const svg = chartRef.current.querySelector('svg');
280
+ if (!svg) return;
281
+
282
+ const svgData = new XMLSerializer().serializeToString(svg);
283
+ const blob = new Blob([svgData], { type: 'image/svg+xml' });
284
+ const url = URL.createObjectURL(blob);
285
+ const link = document.createElement('a');
286
+ link.href = url;
287
+ link.download = `${chartTitle.replace(/\s+/g, '_')}.svg`;
288
+ link.click();
289
+ URL.revokeObjectURL(url);
290
+ };
291
+
292
+ const downloadPNG = () => {
293
+ if (!chartRef.current) return;
294
+ const svg = chartRef.current.querySelector('svg');
295
+ if (!svg) return;
296
+
297
+ const canvas = document.createElement('canvas');
298
+ const ctx = canvas.getContext('2d');
299
+ const svgData = new XMLSerializer().serializeToString(svg);
300
+ const img = new Image();
301
+
302
+ canvas.width = svg.clientWidth * 2;
303
+ canvas.height = svg.clientHeight * 2;
304
+
305
+ img.onload = () => {
306
+ ctx?.drawImage(img, 0, 0, canvas.width, canvas.height);
307
+ const link = document.createElement('a');
308
+ link.href = canvas.toDataURL('image/png');
309
+ link.download = `${chartTitle.replace(/\s+/g, '_')}.png`;
310
+ link.click();
311
+ };
312
+
313
+ img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
314
+ };
315
+
316
+ // Render the appropriate chart
317
+ const renderChart = () => {
318
+ const labels = dataPoints.map(d => d.label);
319
+ const values = dataPoints.map(d => d.value);
320
+ const values2 = dataPoints.map(d => d.value2 || 0);
321
+
322
+ const commonProps = {
323
+ title: chartTitle,
324
+ height: 400,
325
+ tooltip: { enabled: true },
326
+ legend: { display: showLegend, position: 'bottom' as const },
327
+ theme: theme,
328
+ };
329
+
330
+ const lineProps = {
331
+ ...commonProps,
332
+ labels,
333
+ interpolation: interpolation,
334
+ tension: tension,
335
+ point: showPoints ? { radius: 5, hoverRadius: 7 } : { radius: 0, hoverRadius: 0 },
336
+ };
337
+
338
+ const lineDatasets = [
339
+ {
340
+ label: 'Dataset 1',
341
+ data: values,
342
+ borderColor: colors.secondary,
343
+ fill: fillArea ? { enabled: true, opacity: 0.3 } : { enabled: false },
344
+ },
345
+ ...(showSecondDataset ? [{
346
+ label: 'Dataset 2',
347
+ data: values2,
348
+ borderColor: colors.accent,
349
+ fill: fillArea ? { enabled: true, opacity: 0.2 } : { enabled: false },
350
+ }] : []),
351
+ ];
352
+
353
+ switch (lineChartType) {
354
+ case 'multi-axis':
355
+ return (
356
+ <MultiAxisLineChart
357
+ {...lineProps}
358
+ datasets={lineDatasets.map((d, i) => ({ ...d, yAxisID: `y${i + 1}` }))}
359
+ yAxes={[
360
+ { id: 'y1', position: 'left', title: { text: 'Dataset 1' } },
361
+ ...(showSecondDataset ? [{ id: 'y2', position: 'right' as const, title: { text: 'Dataset 2' } }] : []),
362
+ ]}
363
+ />
364
+ );
365
+ case 'stepped':
366
+ return (
367
+ <SteppedLineChart
368
+ {...lineProps}
369
+ datasets={lineDatasets}
370
+ stepped={steppedMode}
371
+ />
372
+ );
373
+ case 'segmented':
374
+ return (
375
+ <SegmentedLineChart
376
+ {...lineProps}
377
+ datasets={lineDatasets.map(d => ({
378
+ ...d,
379
+ segmentStyle: (ctx: any) => {
380
+ if (ctx.p1.value > ctx.p0.value) {
381
+ return { borderColor: colors.success };
382
+ }
383
+ return { borderColor: colors.error };
384
+ },
385
+ }))}
386
+ />
387
+ );
388
+ case 'point-style':
389
+ return (
390
+ <PointStyleLineChart
391
+ {...lineProps}
392
+ datasets={lineDatasets.map((d, i) => ({
393
+ ...d,
394
+ point: {
395
+ pointStyle: i === 0 ? 'circle' : 'triangle',
396
+ radius: 8,
397
+ hoverRadius: 12,
398
+ },
399
+ }))}
400
+ />
401
+ );
402
+ default:
403
+ return (
404
+ <LineChart
405
+ {...lineProps}
406
+ datasets={lineDatasets}
407
+ />
408
+ );
409
+ }
410
+ };
411
+
412
+ return (
413
+ <div style={styles.container}>
414
+ {/* Left Panel - Chart Preview & Code */}
415
+ <div style={styles.previewPanel}>
416
+ {/* Tab Header */}
417
+ <div style={styles.tabHeader}>
418
+ <div style={styles.tabButtons}>
419
+ <button
420
+ onClick={() => setActiveTab('preview')}
421
+ style={activeTab === 'preview' ? styles.tabBtnActive : styles.tabBtn}
422
+ >
423
+ 📊 Preview
424
+ </button>
425
+ <button
426
+ onClick={() => setActiveTab('code')}
427
+ style={activeTab === 'code' ? styles.tabBtnActive : styles.tabBtn}
428
+ >
429
+ {'</>'} Code
430
+ </button>
431
+ </div>
432
+ <div style={styles.headerActions}>
433
+ {activeTab === 'preview' ? (
434
+ <>
435
+ <button onClick={downloadSVG} style={styles.actionBtn}>
436
+ ⬇ SVG
437
+ </button>
438
+ <button onClick={downloadPNG} style={styles.actionBtnSecondary}>
439
+ ⬇ PNG
440
+ </button>
441
+ </>
442
+ ) : (
443
+ <button onClick={copyToClipboard} style={styles.actionBtn}>
444
+ {copied ? '✓ Copied!' : '📋 Copy Code'}
445
+ </button>
446
+ )}
447
+ </div>
448
+ </div>
449
+
450
+ {/* Content Area */}
451
+ <div style={styles.contentArea}>
452
+ {activeTab === 'preview' ? (
453
+ <div style={styles.chartContainer} ref={chartRef}>
454
+ {renderChart()}
455
+ </div>
456
+ ) : (
457
+ <div style={styles.codeContainer}>
458
+ <pre style={styles.codeBlock}>
459
+ <code>{generatedCode}</code>
460
+ </pre>
461
+ </div>
462
+ )}
463
+ </div>
464
+ </div>
465
+
466
+ {/* Right Panel - Controls */}
467
+ <div style={styles.controlPanel}>
468
+ <h2 style={styles.controlTitle}>Chart Configuration</h2>
469
+
470
+ {/* Chart Type */}
471
+ <div style={styles.section}>
472
+ <label style={styles.sectionLabel}>Chart Type</label>
473
+ <div style={styles.typeGrid}>
474
+ {(['basic', 'multi-axis', 'stepped', 'segmented', 'point-style'] as LineChartType[]).map(type => (
475
+ <button
476
+ key={type}
477
+ onClick={() => setLineChartType(type)}
478
+ style={lineChartType === type ? styles.typeBtnActive : styles.typeBtn}
479
+ >
480
+ {type.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')}
481
+ </button>
482
+ ))}
483
+ </div>
484
+ </div>
485
+
486
+ {/* Chart Title */}
487
+ <div style={styles.section}>
488
+ <label style={styles.sectionLabel}>Chart Title</label>
489
+ <input
490
+ type="text"
491
+ value={chartTitle}
492
+ onChange={(e) => setChartTitle(e.target.value)}
493
+ style={styles.input}
494
+ placeholder="Enter chart title"
495
+ />
496
+ </div>
497
+
498
+ {/* Theme */}
499
+ <div style={styles.section}>
500
+ <label style={styles.sectionLabel}>Theme</label>
501
+ <select
502
+ value={theme}
503
+ onChange={(e) => setTheme(e.target.value as ThemeType)}
504
+ style={styles.select}
505
+ >
506
+ <option value="default">Default</option>
507
+ <option value="dark">Dark</option>
508
+ <option value="pastel">Pastel</option>
509
+ <option value="vibrant">Vibrant</option>
510
+ </select>
511
+ </div>
512
+
513
+ {/* Interpolation */}
514
+ <div style={styles.section}>
515
+ <label style={styles.sectionLabel}>Curve Type</label>
516
+ <select
517
+ value={interpolation}
518
+ onChange={(e) => setInterpolation(e.target.value as InterpolationMode)}
519
+ style={styles.select}
520
+ >
521
+ <option value="linear">Linear</option>
522
+ <option value="monotone">Smooth (Monotone)</option>
523
+ <option value="step">Step</option>
524
+ <option value="basis">Basis</option>
525
+ <option value="cardinal">Cardinal</option>
526
+ <option value="catmull-rom">Catmull-Rom</option>
527
+ </select>
528
+ </div>
529
+
530
+ {/* Curve Tension */}
531
+ <div style={styles.section}>
532
+ <label style={styles.sectionLabel}>
533
+ Curve Tension: <span style={styles.tensionValue}>{tension.toFixed(2)}</span>
534
+ </label>
535
+ <input
536
+ type="range"
537
+ min="0"
538
+ max="1"
539
+ step="0.05"
540
+ value={tension}
541
+ onChange={(e) => setTension(parseFloat(e.target.value))}
542
+ style={styles.slider}
543
+ />
544
+ </div>
545
+
546
+ {/* Stepped Mode (only for stepped chart) */}
547
+ {lineChartType === 'stepped' && (
548
+ <div style={styles.section}>
549
+ <label style={styles.sectionLabel}>Step Mode</label>
550
+ <select
551
+ value={steppedMode}
552
+ onChange={(e) => setSteppedMode(e.target.value as 'before' | 'after' | 'middle')}
553
+ style={styles.select}
554
+ >
555
+ <option value="before">Before</option>
556
+ <option value="after">After</option>
557
+ <option value="middle">Middle</option>
558
+ </select>
559
+ </div>
560
+ )}
561
+
562
+ {/* Toggle Options */}
563
+ <div style={styles.section}>
564
+ <label style={styles.sectionLabel}>Display Options</label>
565
+ <div style={styles.toggleGrid}>
566
+ <label style={styles.toggleItem}>
567
+ <input
568
+ type="checkbox"
569
+ checked={showPoints}
570
+ onChange={(e) => setShowPoints(e.target.checked)}
571
+ style={styles.checkboxInput}
572
+ />
573
+ <span style={styles.toggleLabel}>Show Points</span>
574
+ </label>
575
+ <label style={styles.toggleItem}>
576
+ <input
577
+ type="checkbox"
578
+ checked={fillArea}
579
+ onChange={(e) => setFillArea(e.target.checked)}
580
+ style={styles.checkboxInput}
581
+ />
582
+ <span style={styles.toggleLabel}>Fill Area</span>
583
+ </label>
584
+ <label style={styles.toggleItem}>
585
+ <input
586
+ type="checkbox"
587
+ checked={showLegend}
588
+ onChange={(e) => setShowLegend(e.target.checked)}
589
+ style={styles.checkboxInput}
590
+ />
591
+ <span style={styles.toggleLabel}>Show Legend</span>
592
+ </label>
593
+ <label style={styles.toggleItem}>
594
+ <input
595
+ type="checkbox"
596
+ checked={showSecondDataset}
597
+ onChange={(e) => setShowSecondDataset(e.target.checked)}
598
+ style={styles.checkboxInput}
599
+ />
600
+ <span style={styles.toggleLabel}>Second Dataset</span>
601
+ </label>
602
+ </div>
603
+ </div>
604
+
605
+ {/* Data Points */}
606
+ <div style={styles.section}>
607
+ <div style={styles.dataHeader}>
608
+ <label style={styles.sectionLabel}>Data Points ({dataPoints.length})</label>
609
+ <button onClick={addDataPoint} style={styles.addBtn}>+ Add</button>
610
+ </div>
611
+ <div style={styles.dataList}>
612
+ {dataPoints.map((point, index) => (
613
+ <div key={index} style={styles.dataItem}>
614
+ <div style={styles.dataItemHeader}>
615
+ <span style={styles.dataItemTitle}>Point {index + 1}</span>
616
+ {dataPoints.length > 2 && (
617
+ <button
618
+ onClick={() => removeDataPoint(index)}
619
+ style={styles.removeBtn}
620
+ title="Remove point"
621
+ >
622
+ ×
623
+ </button>
624
+ )}
625
+ </div>
626
+ <div style={styles.dataInputRow}>
627
+ <div style={styles.inputGroup}>
628
+ <label style={styles.inputLabel}>Label</label>
629
+ <input
630
+ type="text"
631
+ value={point.label}
632
+ onChange={(e) => updateDataPoint(index, 'label', e.target.value)}
633
+ style={styles.dataInput}
634
+ />
635
+ </div>
636
+ <div style={styles.inputGroup}>
637
+ <label style={styles.inputLabel}>Value</label>
638
+ <input
639
+ type="number"
640
+ value={point.value}
641
+ onChange={(e) => updateDataPoint(index, 'value', e.target.value)}
642
+ style={styles.dataInputNumber}
643
+ />
644
+ </div>
645
+ </div>
646
+ </div>
647
+ ))}
648
+ </div>
649
+ </div>
650
+ </div>
651
+ </div>
652
+ );
653
+ };
654
+
655
+ // Styles following Unisys Design System with enhanced UX
656
+ const styles: { [key: string]: React.CSSProperties } = {
657
+ container: {
658
+ display: 'flex',
659
+ minHeight: '100vh',
660
+ backgroundColor: colors.bgApp,
661
+ fontFamily: '"Unisys", "Tomato Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
662
+ },
663
+ previewPanel: {
664
+ flex: 1,
665
+ padding: '20px',
666
+ display: 'flex',
667
+ flexDirection: 'column',
668
+ gap: '0',
669
+ },
670
+ tabHeader: {
671
+ display: 'flex',
672
+ justifyContent: 'space-between',
673
+ alignItems: 'center',
674
+ backgroundColor: colors.primary,
675
+ padding: '12px 20px',
676
+ borderRadius: '12px 12px 0 0',
677
+ },
678
+ tabButtons: {
679
+ display: 'flex',
680
+ gap: '4px',
681
+ backgroundColor: 'rgba(0,0,0,0.2)',
682
+ padding: '4px',
683
+ borderRadius: '8px',
684
+ },
685
+ tabBtn: {
686
+ padding: '10px 20px',
687
+ backgroundColor: 'transparent',
688
+ color: 'rgba(255,255,255,0.7)',
689
+ border: 'none',
690
+ borderRadius: '6px',
691
+ cursor: 'pointer',
692
+ fontSize: '14px',
693
+ fontWeight: 500,
694
+ transition: 'all 0.2s',
695
+ },
696
+ tabBtnActive: {
697
+ padding: '10px 20px',
698
+ backgroundColor: colors.secondary,
699
+ color: '#fff',
700
+ border: 'none',
701
+ borderRadius: '6px',
702
+ cursor: 'pointer',
703
+ fontSize: '14px',
704
+ fontWeight: 500,
705
+ },
706
+ headerActions: {
707
+ display: 'flex',
708
+ gap: '10px',
709
+ },
710
+ actionBtn: {
711
+ padding: '10px 16px',
712
+ backgroundColor: colors.accent,
713
+ color: colors.primary,
714
+ border: 'none',
715
+ borderRadius: '6px',
716
+ cursor: 'pointer',
717
+ fontSize: '13px',
718
+ fontWeight: 600,
719
+ transition: 'all 0.2s',
720
+ },
721
+ actionBtnSecondary: {
722
+ padding: '10px 16px',
723
+ backgroundColor: 'transparent',
724
+ color: '#fff',
725
+ border: '1px solid rgba(255,255,255,0.5)',
726
+ borderRadius: '6px',
727
+ cursor: 'pointer',
728
+ fontSize: '13px',
729
+ fontWeight: 500,
730
+ transition: 'all 0.2s',
731
+ },
732
+ contentArea: {
733
+ flex: 1,
734
+ backgroundColor: '#fff',
735
+ borderRadius: '0 0 12px 12px',
736
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
737
+ overflow: 'hidden',
738
+ },
739
+ chartContainer: {
740
+ padding: '24px',
741
+ height: '100%',
742
+ },
743
+ codeContainer: {
744
+ height: '100%',
745
+ overflow: 'auto',
746
+ backgroundColor: '#1e1e1e',
747
+ },
748
+ codeBlock: {
749
+ margin: 0,
750
+ padding: '24px',
751
+ fontFamily: '"Fira Code", "Consolas", "Monaco", monospace',
752
+ fontSize: '13px',
753
+ lineHeight: 1.6,
754
+ color: '#d4d4d4',
755
+ whiteSpace: 'pre',
756
+ overflow: 'auto',
757
+ },
758
+ controlPanel: {
759
+ width: '380px',
760
+ backgroundColor: colors.primary,
761
+ padding: '24px',
762
+ overflowY: 'auto',
763
+ maxHeight: '100vh',
764
+ },
765
+ controlTitle: {
766
+ color: '#fff',
767
+ fontSize: '18px',
768
+ fontWeight: 600,
769
+ marginBottom: '24px',
770
+ paddingBottom: '16px',
771
+ borderBottom: '1px solid rgba(255,255,255,0.1)',
772
+ },
773
+ section: {
774
+ marginBottom: '20px',
775
+ },
776
+ sectionLabel: {
777
+ display: 'block',
778
+ color: 'rgba(255,255,255,0.9)',
779
+ fontSize: '12px',
780
+ fontWeight: 600,
781
+ textTransform: 'uppercase',
782
+ letterSpacing: '0.5px',
783
+ marginBottom: '10px',
784
+ },
785
+ typeGrid: {
786
+ display: 'grid',
787
+ gridTemplateColumns: 'repeat(2, 1fr)',
788
+ gap: '8px',
789
+ },
790
+ typeBtn: {
791
+ padding: '12px 10px',
792
+ backgroundColor: 'rgba(255, 255, 255, 0.08)',
793
+ color: 'rgba(255,255,255,0.8)',
794
+ border: '1px solid rgba(255, 255, 255, 0.15)',
795
+ borderRadius: '8px',
796
+ cursor: 'pointer',
797
+ fontSize: '12px',
798
+ fontWeight: 500,
799
+ transition: 'all 0.2s',
800
+ },
801
+ typeBtnActive: {
802
+ padding: '12px 10px',
803
+ backgroundColor: colors.secondary,
804
+ color: '#fff',
805
+ border: '1px solid ' + colors.secondary,
806
+ borderRadius: '8px',
807
+ cursor: 'pointer',
808
+ fontSize: '12px',
809
+ fontWeight: 600,
810
+ },
811
+ input: {
812
+ width: '100%',
813
+ padding: '12px 14px',
814
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
815
+ border: 'none',
816
+ borderRadius: '8px',
817
+ fontSize: '14px',
818
+ color: colors.textPrimary,
819
+ outline: 'none',
820
+ boxSizing: 'border-box',
821
+ },
822
+ select: {
823
+ width: '100%',
824
+ padding: '12px 14px',
825
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
826
+ border: 'none',
827
+ borderRadius: '8px',
828
+ fontSize: '14px',
829
+ color: colors.textPrimary,
830
+ cursor: 'pointer',
831
+ outline: 'none',
832
+ boxSizing: 'border-box',
833
+ },
834
+ tensionValue: {
835
+ color: colors.accent,
836
+ fontWeight: 600,
837
+ },
838
+ slider: {
839
+ width: '100%',
840
+ height: '6px',
841
+ borderRadius: '3px',
842
+ background: 'rgba(255, 255, 255, 0.2)',
843
+ outline: 'none',
844
+ cursor: 'pointer',
845
+ accentColor: colors.accent,
846
+ },
847
+ toggleGrid: {
848
+ display: 'grid',
849
+ gridTemplateColumns: 'repeat(2, 1fr)',
850
+ gap: '10px',
851
+ },
852
+ toggleItem: {
853
+ display: 'flex',
854
+ alignItems: 'center',
855
+ gap: '8px',
856
+ cursor: 'pointer',
857
+ padding: '8px 10px',
858
+ backgroundColor: 'rgba(255,255,255,0.05)',
859
+ borderRadius: '6px',
860
+ transition: 'background-color 0.2s',
861
+ },
862
+ checkboxInput: {
863
+ width: '16px',
864
+ height: '16px',
865
+ accentColor: colors.accent,
866
+ cursor: 'pointer',
867
+ },
868
+ toggleLabel: {
869
+ color: 'rgba(255,255,255,0.9)',
870
+ fontSize: '12px',
871
+ fontWeight: 500,
872
+ },
873
+ dataHeader: {
874
+ display: 'flex',
875
+ justifyContent: 'space-between',
876
+ alignItems: 'center',
877
+ marginBottom: '12px',
878
+ },
879
+ addBtn: {
880
+ backgroundColor: colors.accent,
881
+ color: colors.primary,
882
+ border: 'none',
883
+ padding: '8px 14px',
884
+ borderRadius: '6px',
885
+ cursor: 'pointer',
886
+ fontSize: '12px',
887
+ fontWeight: 600,
888
+ transition: 'transform 0.1s',
889
+ },
890
+ dataList: {
891
+ maxHeight: '280px',
892
+ overflowY: 'auto',
893
+ paddingRight: '4px',
894
+ },
895
+ dataItem: {
896
+ backgroundColor: 'rgba(255, 255, 255, 0.08)',
897
+ borderRadius: '8px',
898
+ padding: '12px',
899
+ marginBottom: '8px',
900
+ },
901
+ dataItemHeader: {
902
+ display: 'flex',
903
+ justifyContent: 'space-between',
904
+ alignItems: 'center',
905
+ marginBottom: '10px',
906
+ },
907
+ dataItemTitle: {
908
+ color: 'rgba(255,255,255,0.7)',
909
+ fontSize: '11px',
910
+ fontWeight: 600,
911
+ textTransform: 'uppercase',
912
+ letterSpacing: '0.5px',
913
+ },
914
+ removeBtn: {
915
+ backgroundColor: colors.error,
916
+ color: '#fff',
917
+ border: 'none',
918
+ fontSize: '14px',
919
+ cursor: 'pointer',
920
+ padding: '2px 8px',
921
+ borderRadius: '4px',
922
+ lineHeight: 1,
923
+ transition: 'opacity 0.2s',
924
+ },
925
+ dataInputRow: {
926
+ display: 'flex',
927
+ gap: '10px',
928
+ },
929
+ inputGroup: {
930
+ flex: 1,
931
+ },
932
+ inputLabel: {
933
+ display: 'block',
934
+ color: 'rgba(255,255,255,0.5)',
935
+ fontSize: '10px',
936
+ fontWeight: 500,
937
+ textTransform: 'uppercase',
938
+ marginBottom: '4px',
939
+ },
940
+ dataInput: {
941
+ width: '100%',
942
+ padding: '10px 12px',
943
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
944
+ border: 'none',
945
+ borderRadius: '6px',
946
+ fontSize: '13px',
947
+ color: colors.textPrimary,
948
+ outline: 'none',
949
+ boxSizing: 'border-box',
950
+ },
951
+ dataInputNumber: {
952
+ width: '100%',
953
+ padding: '10px 12px',
954
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
955
+ border: 'none',
956
+ borderRadius: '6px',
957
+ fontSize: '13px',
958
+ color: colors.textPrimary,
959
+ outline: 'none',
960
+ textAlign: 'right' as const,
961
+ boxSizing: 'border-box',
962
+ },
963
+ };
964
+
965
+ export default App;