lupine.components 1.1.41 → 1.1.42

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 (32) hide show
  1. package/package.json +1 -1
  2. package/src/component-pool/charts/area-chart-demo.tsx +0 -4
  3. package/src/component-pool/charts/area-chart.tsx +5 -2
  4. package/src/component-pool/charts/bar-chart-demo.tsx +1 -4
  5. package/src/component-pool/charts/bar-chart.tsx +5 -2
  6. package/src/component-pool/charts/chart-utils.ts +1 -1
  7. package/src/component-pool/charts/column-chart-demo.tsx +1 -4
  8. package/src/component-pool/charts/column-chart.tsx +5 -2
  9. package/src/component-pool/charts/donut-chart-demo.tsx +2 -5
  10. package/src/component-pool/charts/donut-chart.tsx +1 -23
  11. package/src/component-pool/charts/gauge-chart-demo.tsx +0 -4
  12. package/src/component-pool/charts/gauge-chart.tsx +5 -2
  13. package/src/component-pool/charts/line-chart-demo.tsx +0 -4
  14. package/src/component-pool/charts/line-chart.tsx +5 -2
  15. package/src/component-pool/charts/pie-chart-demo.tsx +4 -10
  16. package/src/component-pool/charts/pie-chart.tsx +78 -61
  17. package/src/component-pool/charts/radar-chart-demo.tsx +0 -3
  18. package/src/component-pool/charts/radar-chart.tsx +40 -5
  19. package/src/component-pool/charts/scatter-chart-demo.tsx +0 -3
  20. package/src/component-pool/charts/scatter-chart.tsx +5 -2
  21. package/src/component-pool/index.ts +1 -0
  22. package/src/component-pool/svg-graph/index.ts +1 -0
  23. package/src/component-pool/svg-graph/svg-graph-demo.tsx +59 -0
  24. package/src/component-pool/svg-graph/svg-graph.ts +166 -0
  25. package/src/component-pool/youtube-player/youtube-player-demo.tsx +4 -4
  26. package/src/component-pool/youtube-player/youtube-player.tsx +89 -13
  27. package/src/components/menu-bar.tsx +15 -4
  28. package/src/components/menu-sidebar.tsx +3 -1
  29. package/src/components/tabs.tsx +9 -6
  30. package/src/demo/demo-about.tsx +1 -1
  31. package/src/demo/demo-frame-helper.tsx +8 -1
  32. package/src/demo/demo-registry.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lupine.components",
3
- "version": "1.1.41",
3
+ "version": "1.1.42",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -16,14 +16,12 @@ export const areaChartDemo: DemoStory<any> = {
16
16
  args: {
17
17
  title: 'Company Revenues',
18
18
  width: '100%',
19
- height: '350px',
20
19
  showLegend: true,
21
20
  curved: true,
22
21
  },
23
22
  argTypes: {
24
23
  title: { control: 'text' },
25
24
  width: { control: 'text' },
26
- height: { control: 'text' },
27
25
  showLegend: { control: 'boolean' },
28
26
  curved: { control: 'boolean' },
29
27
  },
@@ -70,7 +68,6 @@ export const areaChartDemo: DemoStory<any> = {
70
68
  data={multiSeriesData}
71
69
  title={args.title}
72
70
  width={args.width}
73
- height={args.height}
74
71
  showLegend={args.showLegend}
75
72
  curved={args.curved}
76
73
  yAxisFormatter={(val) => '$' + val + 'M'}
@@ -84,7 +81,6 @@ export const areaChartDemo: DemoStory<any> = {
84
81
  <AreaChart
85
82
  data={multiSeriesData}
86
83
  title='Straight Area Comparison'
87
- height='300px'
88
84
  showLegend={true}
89
85
  curved={false}
90
86
  yAxisFormatter={(val) => '$' + val + 'M'}
@@ -228,13 +228,16 @@ export const AreaChart = (props: AreaChartProps) => {
228
228
  },
229
229
  };
230
230
 
231
- const styleStr = `width: ${props.width || '100%'}; height: ${props.height || '100%'};`;
231
+ const ratio = props.aspectRatio ?? 16 / 9;
232
+ const paddingTop = `${(1 / ratio) * 100}%`;
233
+
234
+ const styleStr = `width: ${props.width || '100%'};`;
232
235
 
233
236
  return (
234
237
  <div ref={ref} class='&-container' style={styleStr}>
235
238
  {props.title && <div class='chart-title'>{props.title}</div>}
236
239
 
237
- <div style={{ flex: 1, position: 'relative', minHeight: 0 }}>{chartVar.node}</div>
240
+ <div style={{ flex: 1, position: 'relative', minHeight: 0, paddingTop }}>{chartVar.node}</div>
238
241
 
239
242
  {showLegend && (
240
243
  <div class='chart-legend'>
@@ -26,13 +26,11 @@ export const barChartDemo: DemoStory<any> = {
26
26
  args: {
27
27
  title: 'Top Economies',
28
28
  width: '100%',
29
- height: '350px',
30
29
  showLegend: true,
31
30
  },
32
31
  argTypes: {
33
32
  title: { control: 'text' },
34
33
  width: { control: 'text' },
35
- height: { control: 'text' },
36
34
  showLegend: { control: 'boolean' },
37
35
  },
38
36
  render: (args: any) => {
@@ -78,7 +76,6 @@ export const barChartDemo: DemoStory<any> = {
78
76
  data={singleSeriesData}
79
77
  title={args.title}
80
78
  width={args.width}
81
- height={args.height}
82
79
  showLegend={args.showLegend}
83
80
  xAxisFormatter={(val) => '$' + val + 'T'}
84
81
  />
@@ -88,7 +85,7 @@ export const barChartDemo: DemoStory<any> = {
88
85
  <section class='demo-section'>
89
86
  <div class='section-title'>Multi-Series (Grouped Horizontal)</div>
90
87
  <div class='chart-box'>
91
- <BarChart data={multiSeriesData} title='Quarterly Comparison' height='300px' showLegend={true} />
88
+ <BarChart data={multiSeriesData} title='Quarterly Comparison' showLegend={true} />
92
89
  </div>
93
90
  </section>
94
91
  </div>
@@ -197,13 +197,16 @@ export const BarChart = (props: BarChartProps) => {
197
197
  },
198
198
  };
199
199
 
200
- const styleStr = `width: ${props.width || '100%'}; height: ${props.height || '100%'};`;
200
+ const ratio = props.aspectRatio ?? 16 / 9;
201
+ const paddingTop = `${(1 / ratio) * 100}%`;
202
+
203
+ const styleStr = `width: ${props.width || '100%'};`;
201
204
 
202
205
  return (
203
206
  <div ref={ref} class='&-container' style={styleStr}>
204
207
  {props.title && <div class='chart-title'>{props.title}</div>}
205
208
 
206
- <div style={{ flex: 1, position: 'relative', minHeight: 0 }}>{chartVar.node}</div>
209
+ <div style={{ flex: 1, position: 'relative', minHeight: 0, paddingTop }}>{chartVar.node}</div>
207
210
 
208
211
  {showLegend && (
209
212
  <div class='chart-legend'>
@@ -113,7 +113,7 @@ export type ChartData = {
113
113
  export type BasicChartProps = {
114
114
  title?: string;
115
115
  data: ChartData;
116
- height?: string | number;
116
+ aspectRatio?: number;
117
117
  width?: string | number;
118
118
  showLegend?: boolean;
119
119
  };
@@ -27,13 +27,11 @@ export const columnChartDemo: DemoStory<any> = {
27
27
  args: {
28
28
  title: 'Monthly Revenue',
29
29
  width: '100%',
30
- height: '350px',
31
30
  showLegend: true,
32
31
  },
33
32
  argTypes: {
34
33
  title: { control: 'text' },
35
34
  width: { control: 'text' },
36
- height: { control: 'text' },
37
35
  showLegend: { control: 'boolean' },
38
36
  },
39
37
  render: (args: any) => {
@@ -79,7 +77,6 @@ export const columnChartDemo: DemoStory<any> = {
79
77
  data={singleSeriesData}
80
78
  title={args.title}
81
79
  width={args.width}
82
- height={args.height}
83
80
  showLegend={args.showLegend}
84
81
  yAxisFormatter={(val) => '$' + val}
85
82
  />
@@ -89,7 +86,7 @@ export const columnChartDemo: DemoStory<any> = {
89
86
  <section class='demo-section'>
90
87
  <div class='section-title'>Multi-Series (Grouped)</div>
91
88
  <div class='chart-box'>
92
- <ColumnChart data={multiSeriesData} title='Quarterly Product Sales' height='300px' showLegend={true} />
89
+ <ColumnChart data={multiSeriesData} title='Quarterly Product Sales' showLegend={true} />
93
90
  </div>
94
91
  </section>
95
92
  </div>
@@ -204,13 +204,16 @@ export const ColumnChart = (props: ColumnChartProps) => {
204
204
  },
205
205
  };
206
206
 
207
- const styleStr = `width: ${props.width || '100%'}; height: ${props.height || '100%'};`;
207
+ const ratio = props.aspectRatio ?? 16 / 9;
208
+ const paddingTop = `${(1 / ratio) * 100}%`;
209
+
210
+ const styleStr = `width: ${props.width || '100%'};`;
208
211
 
209
212
  return (
210
213
  <div ref={ref} class='&-container' style={styleStr}>
211
214
  {props.title && <div class='chart-title'>{props.title}</div>}
212
215
 
213
- <div style={{ flex: 1, position: 'relative', minHeight: 0 }}>{chartVar.node}</div>
216
+ <div style={{ flex: 1, position: 'relative', minHeight: 0, paddingTop }}>{chartVar.node}</div>
214
217
 
215
218
  {showLegend && (
216
219
  <div class='chart-legend'>
@@ -18,14 +18,12 @@ export const donutChartDemo: DemoStory<any> = {
18
18
  args: {
19
19
  title: 'Traffic Sources',
20
20
  width: '100%',
21
- height: '300px',
22
21
  showLegend: true,
23
22
  innerRadiusRatio: 0.6,
24
23
  },
25
24
  argTypes: {
26
25
  title: { control: 'text' },
27
26
  width: { control: 'text' },
28
- height: { control: 'text' },
29
27
  showLegend: { control: 'boolean' },
30
28
  innerRadiusRatio: { control: 'number' },
31
29
  },
@@ -78,7 +76,6 @@ export const donutChartDemo: DemoStory<any> = {
78
76
  data={sampleData}
79
77
  title={args.title}
80
78
  width={args.width}
81
- height={args.height}
82
79
  showLegend={args.showLegend}
83
80
  innerRadiusRatio={args.innerRadiusRatio}
84
81
  />
@@ -89,10 +86,10 @@ export const donutChartDemo: DemoStory<any> = {
89
86
  <div class='section-title'>Variations</div>
90
87
  <div class='grid'>
91
88
  <div class='chart-box'>
92
- <DonutChart data={sampleData} title='Thin Ring' innerRadiusRatio={0.8} height='200px' />
89
+ <DonutChart data={sampleData} title='Thin Ring' innerRadiusRatio={0.8} />
93
90
  </div>
94
91
  <div class='chart-box'>
95
- <DonutChart data={sampleData} title='Thick Ring' innerRadiusRatio={0.3} height='200px' />
92
+ <DonutChart data={sampleData} title='Thick Ring' innerRadiusRatio={0.3} />
96
93
  </div>
97
94
  </div>
98
95
  </section>
@@ -2,27 +2,5 @@ import { PieChartProps, PieChart } from './pie-chart';
2
2
 
3
3
  // Donut Chart is just a Pie Chart with an inner radius wrapper
4
4
  export const DonutChart = (props: Omit<PieChartProps, 'innerRadiusRatio'> & { innerRadiusRatio?: number }) => {
5
- const viewBoxSize = 100; // Assuming a default viewBox size for the wrapper
6
-
7
- return (
8
- <div
9
- style={{
10
- flex: 1,
11
- position: 'relative',
12
- display: 'flex',
13
- justifyContent: 'center',
14
- alignItems: 'center',
15
- minHeight: '0',
16
- }}
17
- >
18
- <svg
19
- className='chart-svg' // Changed 'class' to 'className' for React
20
- style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', overflow: 'visible' }}
21
- viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
22
- preserveAspectRatio='xMidYMid meet'
23
- >
24
- <PieChart {...props} innerRadiusRatio={props.innerRadiusRatio ?? 0.6} />
25
- </svg>
26
- </div>
27
- );
5
+ return <PieChart {...props} innerRadiusRatio={props.innerRadiusRatio ?? 0.6} />;
28
6
  };
@@ -8,13 +8,11 @@ export const gaugeChartDemo: DemoStory<any> = {
8
8
  args: {
9
9
  title: 'Server Load',
10
10
  width: '100%',
11
- height: '350px',
12
11
  value: 65,
13
12
  },
14
13
  argTypes: {
15
14
  title: { control: 'text' },
16
15
  width: { control: 'text' },
17
- height: { control: 'text' },
18
16
  value: { control: 'number' },
19
17
  },
20
18
  render: (args: any) => {
@@ -60,7 +58,6 @@ export const gaugeChartDemo: DemoStory<any> = {
60
58
  liveValue.value = (
61
59
  <GaugeChart
62
60
  title='CPU Usage'
63
- height='300px'
64
61
  value={Math.round(currentValue)}
65
62
  color='#e74c3c'
66
63
  valueFormatter={(v) => v + '%'}
@@ -79,7 +76,6 @@ export const gaugeChartDemo: DemoStory<any> = {
79
76
  <GaugeChart
80
77
  title={args.title}
81
78
  width={args.width}
82
- height={args.height}
83
79
  value={args.value}
84
80
  valueFormatter={(v) => v + '%'}
85
81
  />
@@ -63,13 +63,16 @@ export const GaugeChart = (props: GaugeChartProps) => {
63
63
  globalCssId,
64
64
  };
65
65
 
66
- const styleStr = `width: ${props.width || '100%'}; height: ${props.height || '100%'};`;
66
+ const ratio = props.aspectRatio ?? 16 / 9;
67
+ const paddingTop = `${(1 / ratio) * 100}%`;
68
+
69
+ const styleStr = `width: ${props.width || '100%'};`;
67
70
 
68
71
  return (
69
72
  <div ref={ref} class='&-container' style={styleStr}>
70
73
  {props.title && <div class='chart-title'>{props.title}</div>}
71
74
 
72
- <div style={{ flex: 1, position: 'relative', display: 'flex', justifyContent: 'center', minHeight: 0 }}>
75
+ <div style={{ flex: 1, position: 'relative', display: 'flex', justifyContent: 'center', minHeight: 0, paddingTop }}>
73
76
  <svg
74
77
  class='chart-svg'
75
78
  style={{
@@ -21,14 +21,12 @@ export const lineChartDemo: DemoStory<any> = {
21
21
  args: {
22
22
  title: 'Exchange Rates over Week',
23
23
  width: '100%',
24
- height: '350px',
25
24
  showLegend: true,
26
25
  curved: false,
27
26
  },
28
27
  argTypes: {
29
28
  title: { control: 'text' },
30
29
  width: { control: 'text' },
31
- height: { control: 'text' },
32
30
  showLegend: { control: 'boolean' },
33
31
  curved: { control: 'boolean' },
34
32
  },
@@ -75,7 +73,6 @@ export const lineChartDemo: DemoStory<any> = {
75
73
  data={multiSeriesData}
76
74
  title={args.title}
77
75
  width={args.width}
78
- height={args.height}
79
76
  showLegend={args.showLegend}
80
77
  curved={args.curved}
81
78
  />
@@ -88,7 +85,6 @@ export const lineChartDemo: DemoStory<any> = {
88
85
  <LineChart
89
86
  data={exchangeRatesData}
90
87
  title='Crypto Value 2024'
91
- height='300px'
92
88
  showLegend={true}
93
89
  curved={true}
94
90
  />
@@ -207,13 +207,16 @@ export const LineChart = (props: LineChartProps) => {
207
207
  },
208
208
  };
209
209
 
210
- const styleStr = `width: ${props.width || '100%'}; height: ${props.height || '100%'};`;
210
+ const ratio = props.aspectRatio ?? 16 / 9;
211
+ const paddingTop = `${(1 / ratio) * 100}%`;
212
+
213
+ const styleStr = `width: ${props.width || '100%'};`;
211
214
 
212
215
  return (
213
216
  <div ref={ref} class='&-container' style={styleStr}>
214
217
  {props.title && <div class='chart-title'>{props.title}</div>}
215
218
 
216
- <div style={{ flex: 1, position: 'relative', minHeight: 0 }}>{chartVar.node}</div>
219
+ <div style={{ flex: 1, position: 'relative', minHeight: 0, paddingTop }}>{chartVar.node}</div>
217
220
 
218
221
  {showLegend && (
219
222
  <div class='chart-legend'>
@@ -23,13 +23,11 @@ export const pieChartDemo: DemoStory<any> = {
23
23
  args: {
24
24
  title: 'Fruit Sales',
25
25
  width: '100%',
26
- height: '300px',
27
26
  showLegend: true,
28
27
  },
29
28
  argTypes: {
30
29
  title: { control: 'text' },
31
30
  width: { control: 'text' },
32
- height: { control: 'text' },
33
31
  showLegend: { control: 'boolean' },
34
32
  },
35
33
  render: (args: any) => {
@@ -81,7 +79,6 @@ export const pieChartDemo: DemoStory<any> = {
81
79
  data={sampleData}
82
80
  title={args.title}
83
81
  width={args.width}
84
- height={args.height}
85
82
  showLegend={args.showLegend}
86
83
  />
87
84
  </div>
@@ -91,10 +88,10 @@ export const pieChartDemo: DemoStory<any> = {
91
88
  <div class='section-title'>Variations</div>
92
89
  <div class='grid'>
93
90
  <div class='chart-box'>
94
- <PieChart data={sampleData} title='No Legend' showLegend={false} height='200px' />
91
+ <PieChart data={sampleData} title='No Legend' showLegend={false} />
95
92
  </div>
96
93
  <div class='chart-box'>
97
- <PieChart data={emptyData} title='Empty Data' height='200px' />
94
+ <PieChart data={emptyData} title='Empty Data' />
98
95
  </div>
99
96
  <div class='chart-box'>
100
97
  <PieChart
@@ -103,8 +100,7 @@ export const pieChartDemo: DemoStory<any> = {
103
100
  series: [{ name: 'Lone', data: [100] }],
104
101
  }}
105
102
  title='100% Value'
106
- height='200px'
107
- />
103
+ />
108
104
  </div>
109
105
  <div class='chart-box'>
110
106
  <PieChart
@@ -113,8 +109,7 @@ export const pieChartDemo: DemoStory<any> = {
113
109
  series: [{ name: 'Half', data: [50, 50] }],
114
110
  }}
115
111
  title='50/50 Split'
116
- height='200px'
117
- />
112
+ />
118
113
  </div>
119
114
  </div>
120
115
  </section>
@@ -132,7 +127,6 @@ const data = {
132
127
  data={data}
133
128
  title="Fruit Sales"
134
129
  width="100%"
135
- height="300px"
136
130
  showLegend={true}
137
131
  />
138
132
  `,
@@ -13,78 +13,94 @@ export const PieChart = (props: PieChartProps) => {
13
13
  const showLegend = props.showLegend !== false;
14
14
  const innerRadiusRatio = props.innerRadiusRatio ?? 0;
15
15
 
16
- // Pie chart relies on the first series only
17
- const series = props.data.series[0];
18
- if (!series || !series.data || series.data.length === 0) {
16
+ const seriesList = props.data.series;
17
+ if (!seriesList || seriesList.length === 0) {
19
18
  return <div class='&-container'>No data</div>;
20
19
  }
21
20
 
22
- const values = series.data;
23
- const total = values.reduce((sum, val) => sum + val, 0);
24
-
25
21
  // SVG viewBox settings
26
22
  const viewBoxSize = 200;
27
23
  const center = viewBoxSize / 2;
28
- const radius = (viewBoxSize / 2) * 0.9; // 90% of container to leave margin for stroke/anti-aliasing
29
- const innerRadius = radius * innerRadiusRatio;
30
-
31
- let currentAngle = 0;
32
-
33
- const slices = values.map((val, index) => {
34
- // Prevent 0 area slices. Also handle case where 1 val is 100%
35
- const ratio = total > 0 ? val / total : 0;
36
- const angleDelta = ratio * 360;
37
-
38
- // Fix for 100% slice (SVG arcs don't draw well at exactly 360)
39
- const isFullCircle = angleDelta === 360;
40
- const adjustedAngleDelta = isFullCircle ? 359.99 : angleDelta;
41
-
42
- const startAngle = currentAngle;
43
- const endAngle = currentAngle + adjustedAngleDelta;
44
- currentAngle += adjustedAngleDelta;
45
-
46
- const pathD = describeArc(center, center, radius, startAngle, endAngle, innerRadius);
47
- const color = getChartColor(index);
48
- const label = props.data.labels[index] || `Item ${index + 1}`;
49
-
50
- const handleMouseEnter = (e: any) => {
51
- const percentage = (ratio * 100).toFixed(1) + '%';
52
- Tooltip.show(
53
- e,
54
- <div>
55
- <div style={{ fontWeight: 'bold' }}>{label}</div>
56
- <div>Value: {val}</div>
57
- <div>Share: {percentage}</div>
58
- </div>,
59
- { position: 'auto' }
24
+ const maxRadius = (viewBoxSize / 2) * 0.9; // 90% of container to leave margin for stroke/anti-aliasing
25
+
26
+ const globalInnerRadius = maxRadius * innerRadiusRatio;
27
+ const numSeries = seriesList.length;
28
+ // If only 1 series and innerRadiusRatio is 0, ringThickness is maxRadius.
29
+ // If multiple series, we divide the available space (maxRadius - globalInnerRadius) among them.
30
+ const ringThickness = (maxRadius - globalInnerRadius) / numSeries;
31
+
32
+ const allSlices: any[] = [];
33
+
34
+ seriesList.forEach((series, seriesIndex) => {
35
+ const values = series.data;
36
+ if (!values || values.length === 0) return;
37
+
38
+ const total = values.reduce((sum, val) => sum + val, 0);
39
+ let currentAngle = 0;
40
+
41
+ // Outermost ring is series 0
42
+ const outerR = maxRadius - seriesIndex * ringThickness;
43
+ const innerR = outerR - ringThickness;
44
+
45
+ values.forEach((val, index) => {
46
+ // Prevent 0 area slices. Also handle case where 1 val is 100%
47
+ const ratio = total > 0 ? val / total : 0;
48
+ const angleDelta = ratio * 360;
49
+
50
+ // Fix for 100% slice (SVG arcs don't draw well at exactly 360)
51
+ const isFullCircle = angleDelta === 360;
52
+ const adjustedAngleDelta = isFullCircle ? 359.99 : angleDelta;
53
+
54
+ const startAngle = currentAngle;
55
+ const endAngle = currentAngle + adjustedAngleDelta;
56
+ currentAngle += adjustedAngleDelta;
57
+
58
+ const pathD = describeArc(center, center, outerR, startAngle, endAngle, innerR);
59
+ const color = getChartColor(index);
60
+ const label = props.data.labels[index] || `Item ${index + 1}`;
61
+
62
+ const handleMouseEnter = (e: any) => {
63
+ const percentage = (ratio * 100).toFixed(1) + '%';
64
+ Tooltip.show(
65
+ e,
66
+ <div>
67
+ <div style={{ fontWeight: 'bold' }}>{series.name ? `${series.name} - ` : ''}{label}</div>
68
+ <div>Value: {val}</div>
69
+ <div>Share: {percentage}</div>
70
+ </div>,
71
+ { position: 'auto' }
72
+ );
73
+ e.target.style.opacity = 0.8;
74
+ };
75
+
76
+ const handleMouseLeave = (e: any) => {
77
+ Tooltip.hide();
78
+ e.target.style.opacity = 1;
79
+ };
80
+
81
+ allSlices.push(
82
+ <path
83
+ class='chart-element'
84
+ d={pathD}
85
+ fill={color}
86
+ stroke='var(--primary-bg-color)' // border color between slices
87
+ stroke-width={isFullCircle ? '0' : '1'}
88
+ onMouseEnter={handleMouseEnter}
89
+ onMouseLeave={handleMouseLeave}
90
+ style={{ transition: 'opacity 0.2s' }}
91
+ />
60
92
  );
61
- // Optional: highlight slice
62
- e.target.style.opacity = 0.8;
63
- };
64
-
65
- const handleMouseLeave = (e: any) => {
66
- Tooltip.hide();
67
- e.target.style.opacity = 1;
68
- };
69
-
70
- return (
71
- <path
72
- class='chart-element'
73
- d={pathD}
74
- fill={color}
75
- stroke='var(--primary-bg-color)' // border color between slices
76
- stroke-width={isFullCircle ? '0' : '1'}
77
- onMouseEnter={handleMouseEnter}
78
- onMouseLeave={handleMouseLeave}
79
- />
80
- );
93
+ });
81
94
  });
82
95
 
83
96
  const ref: RefProps = {
84
97
  globalCssId,
85
98
  };
86
99
 
87
- const styleStr = `width: ${props.width || '100%'}; height: ${props.height || '100%'}; min-height: 200px;`;
100
+ const ratio = props.aspectRatio ?? 16 / 9;
101
+ const paddingTop = `${(1 / ratio) * 100}%`;
102
+
103
+ const styleStr = `width: ${props.width || '100%'}; min-height: 200px;`;
88
104
 
89
105
  return (
90
106
  <div ref={ref} class='&-container' style={styleStr}>
@@ -98,6 +114,7 @@ export const PieChart = (props: PieChartProps) => {
98
114
  justifyContent: 'center',
99
115
  alignItems: 'center',
100
116
  minHeight: '0',
117
+ paddingTop,
101
118
  }}
102
119
  >
103
120
  <svg
@@ -106,13 +123,13 @@ export const PieChart = (props: PieChartProps) => {
106
123
  viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
107
124
  preserveAspectRatio='xMidYMid meet'
108
125
  >
109
- <g>{slices}</g>
126
+ <g>{allSlices}</g>
110
127
  </svg>
111
128
  </div>
112
129
 
113
130
  {showLegend && (
114
131
  <div class='chart-legend'>
115
- {values.map((_, i) => (
132
+ {props.data.labels.map((_, i) => (
116
133
  <div class='legend-item'>
117
134
  <div class='legend-color' style={{ backgroundColor: getChartColor(i) }} />
118
135
  <div>{props.data.labels[i] || `Item ${i + 1}`}</div>
@@ -16,13 +16,11 @@ export const radarChartDemo: DemoStory<any> = {
16
16
  args: {
17
17
  title: 'Character Stats',
18
18
  width: '100%',
19
- height: '400px',
20
19
  showLegend: true,
21
20
  },
22
21
  argTypes: {
23
22
  title: { control: 'text' },
24
23
  width: { control: 'text' },
25
- height: { control: 'text' },
26
24
  showLegend: { control: 'boolean' },
27
25
  },
28
26
  render: (args: any) => {
@@ -68,7 +66,6 @@ export const radarChartDemo: DemoStory<any> = {
68
66
  data={statsData}
69
67
  title={args.title}
70
68
  width={args.width}
71
- height={args.height}
72
69
  showLegend={args.showLegend}
73
70
  />
74
71
  </div>
@@ -105,8 +105,20 @@ export const RadarChart = (props: RadarChartProps) => {
105
105
 
106
106
  const pointsStr = coordinates.map((c) => `${c.x},${c.y}`).join(' ');
107
107
 
108
+ const seriesGroupElements: any[] = [];
109
+
108
110
  // Fill area
109
- dataElements.push(<polygon points={pointsStr} fill={color} fillOpacity='0.3' stroke={color} strokeWidth='2' />);
111
+ seriesGroupElements.push(
112
+ <polygon
113
+ class="radar-polygon"
114
+ points={pointsStr}
115
+ fill={color}
116
+ fillOpacity='0.15'
117
+ stroke={color}
118
+ strokeWidth='2'
119
+ style={{ transition: 'fill-opacity 0.2s ease' }}
120
+ />
121
+ );
110
122
 
111
123
  // Points and Tooltips
112
124
  coordinates.forEach((c) => {
@@ -132,9 +144,9 @@ export const RadarChart = (props: RadarChartProps) => {
132
144
  e.target.setAttribute('r', '4');
133
145
  };
134
146
 
135
- dataElements.push(
147
+ seriesGroupElements.push(
136
148
  <circle
137
- class='chart-element'
149
+ class='chart-element radar-point'
138
150
  cx={c.x}
139
151
  cy={c.y}
140
152
  r='4'
@@ -147,6 +159,26 @@ export const RadarChart = (props: RadarChartProps) => {
147
159
  />
148
160
  );
149
161
  });
162
+
163
+ const handleGroupMouseEnter = (e: any) => {
164
+ const polygon = e.currentTarget.querySelector('.radar-polygon');
165
+ if (polygon) polygon.setAttribute('fill-opacity', '0.6');
166
+ };
167
+
168
+ const handleGroupMouseLeave = (e: any) => {
169
+ const polygon = e.currentTarget.querySelector('.radar-polygon');
170
+ if (polygon) polygon.setAttribute('fill-opacity', '0.15');
171
+ };
172
+
173
+ dataElements.push(
174
+ <g
175
+ class="radar-series"
176
+ onMouseEnter={handleGroupMouseEnter}
177
+ onMouseLeave={handleGroupMouseLeave}
178
+ >
179
+ {seriesGroupElements}
180
+ </g>
181
+ );
150
182
  });
151
183
 
152
184
  return dataElements;
@@ -156,13 +188,16 @@ export const RadarChart = (props: RadarChartProps) => {
156
188
  globalCssId,
157
189
  };
158
190
 
159
- const styleStr = `width: ${props.width || '100%'}; height: ${props.height || '100%'};`;
191
+ const ratio = props.aspectRatio ?? 16 / 9;
192
+ const paddingTop = `${(1 / ratio) * 100}%`;
193
+
194
+ const styleStr = `width: ${props.width || '100%'};`;
160
195
 
161
196
  return (
162
197
  <div ref={ref} class='&-container' style={styleStr}>
163
198
  {props.title && <div class='chart-title'>{props.title}</div>}
164
199
 
165
- <div style={{ flex: 1, position: 'relative', display: 'flex', justifyContent: 'center', minHeight: 0 }}>
200
+ <div style={{ flex: 1, position: 'relative', display: 'flex', justifyContent: 'center', minHeight: 0, paddingTop }}>
166
201
  <svg
167
202
  class='chart-svg'
168
203
  style={{
@@ -16,13 +16,11 @@ export const scatterChartDemo: DemoStory<any> = {
16
16
  args: {
17
17
  title: 'Value Correlation',
18
18
  width: '100%',
19
- height: '350px',
20
19
  showLegend: true,
21
20
  },
22
21
  argTypes: {
23
22
  title: { control: 'text' },
24
23
  width: { control: 'text' },
25
- height: { control: 'text' },
26
24
  showLegend: { control: 'boolean' },
27
25
  },
28
26
  render: (args: any) => {
@@ -68,7 +66,6 @@ export const scatterChartDemo: DemoStory<any> = {
68
66
  data={correlationData}
69
67
  title={args.title}
70
68
  width={args.width}
71
- height={args.height}
72
69
  showLegend={args.showLegend}
73
70
  />
74
71
  </div>
@@ -211,13 +211,16 @@ export const ScatterChart = (props: ScatterChartProps) => {
211
211
  },
212
212
  };
213
213
 
214
- const styleStr = `width: ${props.width || '100%'}; height: ${props.height || '100%'};`;
214
+ const ratio = props.aspectRatio ?? 16 / 9;
215
+ const paddingTop = `${(1 / ratio) * 100}%`;
216
+
217
+ const styleStr = `width: ${props.width || '100%'};`;
215
218
 
216
219
  return (
217
220
  <div ref={ref} class='&-container' style={styleStr}>
218
221
  {props.title && <div class='chart-title'>{props.title}</div>}
219
222
 
220
- <div style={{ flex: 1, position: 'relative', minHeight: 0 }}>{chartVar.node}</div>
223
+ <div style={{ flex: 1, position: 'relative', minHeight: 0, paddingTop }}>{chartVar.node}</div>
221
224
 
222
225
  {showLegend && (
223
226
  <div class='chart-legend'>
@@ -5,6 +5,7 @@ export * from './breadcrumbs';
5
5
  export * from './card';
6
6
  export * from './carousel';
7
7
  export * from './cascader';
8
+ export * from './charts';
8
9
  export * from './copy-button';
9
10
  export * from './date-picker';
10
11
  export * from './floating-icon-menu';
@@ -0,0 +1 @@
1
+ export * from './svg-graph';
@@ -0,0 +1,59 @@
1
+ import { RefProps } from 'lupine.web';
2
+ import { SvgGraph } from './svg-graph';
3
+ import { DemoStory } from '../../demo/demo-types';
4
+
5
+ const SvgGraphDemo = () => {
6
+ const ref: RefProps = {
7
+ onLoad: async (el) => {
8
+ const graph = new SvgGraph(el as HTMLElement);
9
+
10
+ // Draw a line
11
+ graph.drawLine('red', '2px', 50, 50, 250, 50, 'line1');
12
+
13
+ // Draw a rect
14
+ graph.draw('rect', '#e0f7fa', '#00acc1', '2px', 50, 100, 100, 50, 'rect1');
15
+
16
+ // Draw an ellipse
17
+ graph.draw('ellipse', '#fce4ec', '#d81b60', '2px', 200, 100, 100, 50, 'ellipse1');
18
+
19
+ // Draw a rounded rect
20
+ graph.draw('roundrect', '#e8f5e9', '#43a047', '2px', 50, 200, 150, 60, 'roundrect1');
21
+
22
+ // Test resize after 1s
23
+ setTimeout(() => {
24
+ graph.resize('line1', 50, 50, 300, 100);
25
+ }, 1000);
26
+ },
27
+ };
28
+
29
+ return (
30
+ <div style={{ padding: '16px' }}>
31
+ <h3>SvgGraph Component Demo</h3>
32
+ <p>This demonstrates the imperative SVG Graph API modernized from legacy JGraph.</p>
33
+
34
+ <div
35
+ ref={ref}
36
+ style={{
37
+ position: 'relative',
38
+ width: '100%',
39
+ height: '400px',
40
+ border: '1px solid #ccc',
41
+ marginTop: '16px',
42
+ backgroundColor: '#fafafa',
43
+ }}
44
+ >
45
+ {/* SVG will be injected here */}
46
+ </div>
47
+ </div>
48
+ );
49
+ };
50
+
51
+ export const svgGraphDemo: DemoStory<any> = {
52
+ id: 'svg-graph-demo',
53
+ text: 'Svg Graph',
54
+ args: {},
55
+ render: (args: any) => {
56
+ return <SvgGraphDemo />;
57
+ },
58
+ code: 'import { SvgGraph } from "lupine.components/src/component-pool/svg-graph";\n// Check source code for usage.',
59
+ };
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Modernized SVG Graph Library
3
+ * Replaces the legacy JGraph library.
4
+ */
5
+
6
+ export class SvgGraph {
7
+ public svgRoot: SVGSVGElement;
8
+ private elements: Map<string, SVGElement> = new Map();
9
+
10
+ constructor(container: HTMLElement | string) {
11
+ let el: HTMLElement | null = null;
12
+ if (typeof container === 'string') {
13
+ el = document.getElementById(container);
14
+ } else {
15
+ el = container;
16
+ }
17
+
18
+ if (!el) {
19
+ throw new Error('SvgGraph: container not found');
20
+ }
21
+
22
+ el.style.userSelect = 'none';
23
+
24
+ this.svgRoot = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
25
+ this.svgRoot.style.width = '100%';
26
+ this.svgRoot.style.height = '100%';
27
+ this.svgRoot.style.position = 'absolute';
28
+ this.svgRoot.style.left = '0';
29
+ this.svgRoot.style.top = '0';
30
+ this.svgRoot.style.pointerEvents = 'none'; // usually good for overlays
31
+
32
+ el.appendChild(this.svgRoot);
33
+ }
34
+
35
+ draw(
36
+ shape: 'rect' | 'ellipse' | 'roundrect' | 'line',
37
+ fillColor: string,
38
+ lineColor: string,
39
+ lineWidth: string,
40
+ left: number,
41
+ top: number,
42
+ width: number,
43
+ height: number,
44
+ id?: string
45
+ ): SVGElement {
46
+ const svgNamespace = 'http://www.w3.org/2000/svg';
47
+ let svg: SVGElement;
48
+
49
+ if (shape === 'rect') {
50
+ svg = document.createElementNS(svgNamespace, 'rect');
51
+ svg.setAttributeNS(null, 'x', left + 'px');
52
+ svg.setAttributeNS(null, 'y', top + 'px');
53
+ svg.setAttributeNS(null, 'width', width + 'px');
54
+ svg.setAttributeNS(null, 'height', height + 'px');
55
+ } else if (shape === 'ellipse') {
56
+ svg = document.createElementNS(svgNamespace, 'ellipse');
57
+ svg.setAttributeNS(null, 'cx', left + width / 2 + 'px');
58
+ svg.setAttributeNS(null, 'cy', top + height / 2 + 'px');
59
+ svg.setAttributeNS(null, 'rx', width / 2 + 'px');
60
+ svg.setAttributeNS(null, 'ry', height / 2 + 'px');
61
+ } else if (shape === 'roundrect') {
62
+ svg = document.createElementNS(svgNamespace, 'rect');
63
+ svg.setAttributeNS(null, 'x', left + 'px');
64
+ svg.setAttributeNS(null, 'y', top + 'px');
65
+ svg.setAttributeNS(null, 'rx', '20px');
66
+ svg.setAttributeNS(null, 'ry', '20px');
67
+ svg.setAttributeNS(null, 'width', width + 'px');
68
+ svg.setAttributeNS(null, 'height', height + 'px');
69
+ } else if (shape === 'line') {
70
+ svg = document.createElementNS(svgNamespace, 'line');
71
+ svg.setAttributeNS(null, 'x1', left + 'px');
72
+ svg.setAttributeNS(null, 'y1', top + 'px');
73
+ svg.setAttributeNS(null, 'x2', width + 'px');
74
+ svg.setAttributeNS(null, 'y2', height + 'px');
75
+ } else {
76
+ throw new Error(`Unsupported shape: ${shape}`);
77
+ }
78
+
79
+ svg.style.position = 'absolute';
80
+ svg.setAttributeNS(null, 'fill', fillColor || 'none');
81
+ svg.setAttributeNS(null, 'stroke', lineColor || 'none');
82
+ svg.setAttributeNS(null, 'stroke-width', lineWidth || '1px');
83
+ svg.style.pointerEvents = 'auto'; // allow interaction with shape
84
+
85
+ if (id) {
86
+ svg.id = id;
87
+ this.elements.set(id, svg);
88
+ }
89
+ this.svgRoot.appendChild(svg);
90
+ return svg;
91
+ }
92
+
93
+ drawLine(
94
+ lineColor: string,
95
+ lineWidth: string,
96
+ left: number,
97
+ top: number,
98
+ width: number,
99
+ height: number,
100
+ id?: string
101
+ ): SVGElement {
102
+ return this.draw('line', '', lineColor, lineWidth, left, top, width, height, id);
103
+ }
104
+
105
+ resize(idOrObj: string | SVGElement, left: number, top: number, width: number, height: number): SVGElement | void {
106
+ const svg =
107
+ typeof idOrObj === 'string' ? this.elements.get(idOrObj) || (document.getElementById(idOrObj) as any) : idOrObj;
108
+ if (!svg) return;
109
+
110
+ if (svg.tagName === 'rect') {
111
+ svg.setAttributeNS(null, 'x', left + 'px');
112
+ svg.setAttributeNS(null, 'y', top + 'px');
113
+ svg.setAttributeNS(null, 'width', width + 'px');
114
+ svg.setAttributeNS(null, 'height', height + 'px');
115
+ } else if (svg.tagName === 'ellipse') {
116
+ svg.setAttributeNS(null, 'cx', left + width / 2 + 'px');
117
+ svg.setAttributeNS(null, 'cy', top + height / 2 + 'px');
118
+ svg.setAttributeNS(null, 'rx', width / 2 + 'px');
119
+ svg.setAttributeNS(null, 'ry', height / 2 + 'px');
120
+ } else if (svg.tagName === 'line') {
121
+ svg.setAttributeNS(null, 'x1', left + 'px');
122
+ svg.setAttributeNS(null, 'y1', top + 'px');
123
+ svg.setAttributeNS(null, 'x2', width + 'px');
124
+ svg.setAttributeNS(null, 'y2', height + 'px');
125
+ }
126
+ return svg;
127
+ }
128
+
129
+ remove(idOrObj: string | SVGElement): void {
130
+ const svg = typeof idOrObj === 'string' ? this.elements.get(idOrObj) || document.getElementById(idOrObj) : idOrObj;
131
+ if (svg && svg.parentNode) {
132
+ svg.parentNode.removeChild(svg);
133
+ if (typeof idOrObj === 'string') this.elements.delete(idOrObj);
134
+ }
135
+ }
136
+
137
+ setAttribute(shape: SVGElement, cmd: string, value: string): void {
138
+ if (shape != null) {
139
+ if (cmd === 'fillcolor') {
140
+ shape.setAttributeNS(null, 'fill', value || 'none');
141
+ } else if (cmd === 'linecolor') {
142
+ shape.setAttributeNS(null, 'stroke', value || 'none');
143
+ } else if (cmd === 'linewidth') {
144
+ shape.setAttributeNS(null, 'stroke-width', parseInt(value) + 'px');
145
+ }
146
+ }
147
+ }
148
+
149
+ getAttribute(shape: SVGElement, cmd: string): string {
150
+ let result = '';
151
+ if (shape != null) {
152
+ if (cmd === 'fillcolor') {
153
+ result = shape.getAttributeNS(null, 'fill') || '';
154
+ if (result === 'none') result = '';
155
+ } else if (cmd === 'linecolor') {
156
+ result = shape.getAttributeNS(null, 'stroke') || '';
157
+ if (result === 'none') result = '';
158
+ } else if (cmd === 'linewidth') {
159
+ result = shape.getAttributeNS(null, 'stroke') || '';
160
+ if (result === 'none') result = '';
161
+ else result = shape.getAttributeNS(null, 'stroke-width') || '';
162
+ }
163
+ }
164
+ return result;
165
+ }
166
+ }
@@ -5,13 +5,13 @@ export const youtubePlayerDemo: DemoStory<YouTubePlayerProps> = {
5
5
  id: 'youtubePlayerDemo',
6
6
  text: 'YouTube Player',
7
7
  args: {
8
- videoId: 'dQw4w9WgXcQ',
8
+ srcOrVideoId: 'dQw4w9WgXcQ',
9
9
  width: '100%',
10
10
  height: '400px',
11
11
  autoplay: false,
12
12
  },
13
13
  argTypes: {
14
- videoId: { control: 'text', description: 'YouTube Video ID' },
14
+ srcOrVideoId: { control: 'text', description: 'YouTube Video ID' },
15
15
  width: { control: 'text' },
16
16
  height: { control: 'text' },
17
17
  autoplay: { control: 'boolean' },
@@ -39,8 +39,8 @@ export const youtubePlayerDemo: DemoStory<YouTubePlayerProps> = {
39
39
  <section>
40
40
  <div class='section-title'>Custom Size</div>
41
41
  <div style={{ display: 'flex', gap: '20px', alignItems: 'center' }}>
42
- <YouTubePlayer videoId='jNQXAC9IVRw' width='300px' height='200px' />
43
- <YouTubePlayer videoId='M7lc1UVf-VE' width='400px' height='250px' />
42
+ <YouTubePlayer srcOrVideoId='jNQXAC9IVRw' width='300px' height='200px' />
43
+ <YouTubePlayer srcOrVideoId='M7lc1UVf-VE' width='400px' height='250px' />
44
44
  </div>
45
45
  </section>
46
46
  </div>
@@ -3,38 +3,114 @@ import { CssProps } from 'lupine.web';
3
3
  export type YouTubePlayerProps = {
4
4
  class?: string;
5
5
  style?: CssProps;
6
- videoId: string;
6
+ srcOrVideoId: string;
7
7
  width?: string | number;
8
8
  height?: string | number;
9
9
  autoplay?: boolean;
10
10
  allowFullScreen?: boolean;
11
+ controls?: boolean;
12
+ loop?: boolean;
13
+ muted?: boolean;
14
+ rel?: boolean;
15
+ modestbranding?: boolean;
11
16
  };
12
17
 
18
+ function parseYouTubeUrl(urlOrId: string) {
19
+ if (!urlOrId) return { videoId: '' };
20
+ if (/^[a-zA-Z0-9_-]{11}$/.test(urlOrId)) return { videoId: urlOrId };
21
+
22
+ let videoId = '';
23
+ let list = '';
24
+ let start = '';
25
+
26
+ try {
27
+ const urlObj = new URL(urlOrId);
28
+ if (urlObj.hostname.includes('youtube.com')) {
29
+ if (urlObj.pathname === '/watch') {
30
+ videoId = urlObj.searchParams.get('v') || '';
31
+ } else if (urlObj.pathname.startsWith('/embed/')) {
32
+ videoId = urlObj.pathname.split('/')[2] || '';
33
+ } else if (urlObj.pathname.startsWith('/v/')) {
34
+ videoId = urlObj.pathname.split('/')[2] || '';
35
+ }
36
+ list = urlObj.searchParams.get('list') || '';
37
+ start = urlObj.searchParams.get('t') || urlObj.searchParams.get('start') || '';
38
+ } else if (urlObj.hostname.includes('youtu.be')) {
39
+ videoId = urlObj.pathname.slice(1);
40
+ list = urlObj.searchParams.get('list') || '';
41
+ start = urlObj.searchParams.get('t') || urlObj.searchParams.get('start') || '';
42
+ }
43
+ } catch (e) {
44
+ const match = urlOrId.match(/(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([\w-]{11})/);
45
+ if (match) videoId = match[1];
46
+ }
47
+
48
+ if (start && isNaN(Number(start))) {
49
+ const timeMatch = start.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/);
50
+ if (timeMatch && timeMatch[0]) {
51
+ let seconds = 0;
52
+ if (timeMatch[1]) seconds += parseInt(timeMatch[1]) * 3600;
53
+ if (timeMatch[2]) seconds += parseInt(timeMatch[2]) * 60;
54
+ if (timeMatch[3]) seconds += parseInt(timeMatch[3]);
55
+ if (seconds > 0) start = seconds.toString();
56
+ }
57
+ }
58
+
59
+ return { videoId, list, start };
60
+ }
61
+
13
62
  export const YouTubePlayer = (props: YouTubePlayerProps) => {
14
63
  const {
15
- videoId,
64
+ srcOrVideoId,
16
65
  width = '100%',
17
66
  height = '100%',
18
67
  autoplay = false,
19
68
  allowFullScreen = true,
69
+ controls = true,
70
+ loop = false,
71
+ muted = false,
72
+ rel = false,
73
+ modestbranding = true,
20
74
  style,
21
75
  class: className,
22
76
  } = props;
23
77
 
24
- const src = `https://www.youtube.com/embed/${videoId}?autoplay=${autoplay ? 1 : 0}`;
78
+ const { videoId, list, start } = parseYouTubeUrl(srcOrVideoId);
79
+
80
+ if (typeof document === 'undefined') {
81
+ return <div class={className} css={{ position: 'relative', width, height, ...style }}></div>;
82
+ }
83
+
84
+ let src = '';
85
+ if (videoId || list) {
86
+ src = `https://www.youtube.com/embed/${videoId}?autoplay=${autoplay ? 1 : 0}`;
87
+ if (!controls) src += '&controls=0';
88
+ if (loop) src += `&loop=1&playlist=${videoId}`; // YouTube requires playlist=VIDEO_ID for loop to work
89
+ if (muted) src += '&mute=1';
90
+ if (!rel) src += '&rel=0';
91
+ if (modestbranding) src += '&modestbranding=1';
92
+ if (list) src += `&list=${list}`;
93
+ if (start) src += `&start=${start}`;
94
+ }
25
95
 
26
96
  return (
27
97
  <div class={className} css={{ position: 'relative', width, height, ...style }}>
28
- <iframe
29
- width='100%'
30
- height='100%'
31
- src={src}
32
- title='YouTube video player'
33
- frameBorder='0'
34
- allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'
35
- allowFullScreen={allowFullScreen}
36
- style={{ border: 'none', display: 'block' }}
37
- ></iframe>
98
+ {src ? (
99
+ <iframe
100
+ width='100%'
101
+ height='100%'
102
+ src={src}
103
+ title='YouTube video player'
104
+ frameBorder='0'
105
+ allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'
106
+ allowFullScreen={allowFullScreen}
107
+ style={{ border: 'none', display: 'block' }}
108
+ ></iframe>
109
+ ) : (
110
+ <div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f0f0f0', color: '#999', border: '1px solid #ddd' }}>
111
+ Invalid YouTube URL
112
+ </div>
113
+ )}
38
114
  </div>
39
115
  );
40
116
  };
@@ -23,13 +23,17 @@ export const MenuBar = ({
23
23
  menuId,
24
24
  items,
25
25
  className,
26
- textColor = 'var(--menubar-color)',
27
- backgroundColor = 'var(--menubar-bg-color)', //'black',
28
- hoverColor = 'var(--activatable-color-hover)', //'#ffffff',
29
- hoverBgColor = 'var(--activatable-bg-color-hover)', //'#d12121',
26
+ textColor,
27
+ backgroundColor,
28
+ hoverColor,
29
+ hoverBgColor,
30
30
  maxWidth = '100%',
31
31
  maxWidthMobileMenu = MediaQueryMaxWidth.TabletMax,
32
32
  }: MenuBarProps) => {
33
+ backgroundColor = backgroundColor || 'var(--menubar-bg-color)';
34
+ textColor = textColor || 'var(--menubar-color)';
35
+ hoverColor = hoverColor || 'var(--activatable-color-hover)';
36
+ hoverBgColor = hoverBgColor || 'var(--activatable-bg-color-hover)';
33
37
  const css: any = {
34
38
  width: '100%',
35
39
  maxWidth: maxWidth,
@@ -140,6 +144,13 @@ export const MenuBar = ({
140
144
  '.menu-bar-top.open': {
141
145
  display: 'flex',
142
146
  flexDirection: 'column',
147
+ position: 'absolute',
148
+ top: '100%',
149
+ left: 0,
150
+ right: 0,
151
+ zIndex: 100,
152
+ backgroundColor: backgroundColor,
153
+ boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
143
154
  },
144
155
  '.menu-bar-top.open .menu-bar-sub-box > .menu-bar-sub': {
145
156
  display: 'flex',
@@ -19,6 +19,7 @@ export type MenuSidebarProps = {
19
19
  color?: string;
20
20
  backgroundColor?: string;
21
21
  isDevAdmin?: boolean;
22
+ defaultOpenAll?: boolean;
22
23
  };
23
24
  export const MenuSidebar = ({
24
25
  mobileMenu,
@@ -31,6 +32,7 @@ export const MenuSidebar = ({
31
32
  maxWidth = '100%',
32
33
  maxWidthMobileMenu = MediaQueryMaxWidth.TabletMax,
33
34
  isDevAdmin = false,
35
+ defaultOpenAll = false,
34
36
  }: MenuSidebarProps) => {
35
37
  const css: CssProps = {
36
38
  // backgroundColor,
@@ -228,7 +230,7 @@ export const MenuSidebar = ({
228
230
  }
229
231
  let ref: RefProps = {};
230
232
  return item.items ? (
231
- <div ref={ref} class='menu-sidebar-sub-box' onClick={() => onItemToggleClick(ref)}>
233
+ <div ref={ref} class={`menu-sidebar-sub-box ${defaultOpenAll ? 'open' : ''}`} onClick={() => onItemToggleClick(ref)}>
232
234
  <div class='menu-sidebar-item'>{item.text}</div>
233
235
  {renderItems(item.items, 'menu-sidebar-sub')}
234
236
  </div>
@@ -124,12 +124,6 @@ export const Tabs = ({ pages, defaultIndex, topClassName, pagePadding, hook: ref
124
124
  }
125
125
  };
126
126
  if (refUpdate) {
127
- refUpdate.updateTitle = (index: number, title: string) => {
128
- const doms = ref.$all(`.&tabs > div > .tab`);
129
- if (index >= 0 && index < doms.length) {
130
- doms[index].innerHTML = title;
131
- }
132
- };
133
127
  refUpdate.updateIndex = updateIndex;
134
128
  refUpdate.removePage = removePage;
135
129
  refUpdate.newPage = newPage;
@@ -141,6 +135,15 @@ export const Tabs = ({ pages, defaultIndex, topClassName, pagePadding, hook: ref
141
135
  const doms = ref.$all(`.&tabs > div > .tab`);
142
136
  return doms.length;
143
137
  };
138
+ refUpdate.updateTitle = (index: number, title: string) => {
139
+ const doms = ref.$all(`.&tabs > div > .tab`);
140
+ if (index === -1) {
141
+ index = refUpdate.getIndex!();
142
+ }
143
+ if (index >= 0 && index < doms.length) {
144
+ doms[index].innerHTML = title;
145
+ }
146
+ };
144
147
  refUpdate.findAndActivate = (title: string) => {
145
148
  const doms = ref.$all(`.&tabs > div > .tab`);
146
149
  for (let i = 0; i < doms.length; i++) {
@@ -1,6 +1,6 @@
1
1
  import { CssProps, PageProps } from 'lupine.components';
2
2
 
3
- export const DemoAboutPage = async (props: PageProps) => {
3
+ export const DemoAboutPage = () => {
4
4
  const css: CssProps = {};
5
5
 
6
6
  return (
@@ -24,6 +24,7 @@ import { redirectDemo } from '../components/redirect-demo';
24
24
  import { textWaveDemo } from '../components/text-wave-demo';
25
25
  import { textScaleDemo } from '../components/text-scale-demo';
26
26
  import { textGlowDemo } from '../components/text-glow-demo';
27
+ import { svgGraphDemo } from '../component-pool/svg-graph/svg-graph-demo';
27
28
  import { toggleButtonDemo } from '../components/toggle-button-demo';
28
29
  import { messageBoxDemo } from '../components/message-box-demo';
29
30
  import { loadingSpinDemo } from '../components/loading-spin-demo';
@@ -611,6 +612,12 @@ export class DemoFrameHelper {
611
612
  url: '',
612
613
  js: () => this.addPanel(redirectDemo.text, <DemoPage story={redirectDemo} />),
613
614
  },
615
+ {
616
+ id: svgGraphDemo.id,
617
+ text: svgGraphDemo.text,
618
+ url: '',
619
+ js: () => this.addPanel(svgGraphDemo.text, <DemoPage story={svgGraphDemo} />),
620
+ },
614
621
  ],
615
622
  },
616
623
  {
@@ -622,7 +629,7 @@ export class DemoFrameHelper {
622
629
  id: 'about',
623
630
  text: 'About',
624
631
  url: '',
625
- js: () => this.addPanel('About', DemoAboutPage()),
632
+ js: () => this.addPanel('About', <DemoAboutPage />),
626
633
  },
627
634
  ],
628
635
  },
@@ -68,6 +68,7 @@ import {
68
68
  gaugeChartDemo,
69
69
  scatterChartDemo,
70
70
  } from '../component-pool/charts';
71
+ import { svgGraphDemo } from '../component-pool/svg-graph/svg-graph-demo';
71
72
 
72
73
  export const demoRegistry: Record<string, DemoStory<any>> = {
73
74
  [buttonDemo.id]: buttonDemo,
@@ -138,4 +139,5 @@ export const demoRegistry: Record<string, DemoStory<any>> = {
138
139
  [radarChartDemo.id]: radarChartDemo,
139
140
  [gaugeChartDemo.id]: gaugeChartDemo,
140
141
  [scatterChartDemo.id]: scatterChartDemo,
142
+ [svgGraphDemo.id]: svgGraphDemo,
141
143
  };