lupine.components 1.1.41 → 1.1.43
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/package.json +1 -1
- package/src/component-pool/charts/area-chart-demo.tsx +0 -4
- package/src/component-pool/charts/area-chart.tsx +5 -2
- package/src/component-pool/charts/bar-chart-demo.tsx +1 -4
- package/src/component-pool/charts/bar-chart.tsx +5 -2
- package/src/component-pool/charts/chart-utils.ts +4 -3
- package/src/component-pool/charts/column-chart-demo.tsx +1 -4
- package/src/component-pool/charts/column-chart.tsx +5 -2
- package/src/component-pool/charts/donut-chart-demo.tsx +2 -5
- package/src/component-pool/charts/donut-chart.tsx +1 -23
- package/src/component-pool/charts/gauge-chart-demo.tsx +0 -4
- package/src/component-pool/charts/gauge-chart.tsx +5 -2
- package/src/component-pool/charts/line-chart-demo.tsx +0 -4
- package/src/component-pool/charts/line-chart.tsx +5 -2
- package/src/component-pool/charts/pie-chart-demo.tsx +4 -10
- package/src/component-pool/charts/pie-chart.tsx +78 -61
- package/src/component-pool/charts/radar-chart-demo.tsx +0 -3
- package/src/component-pool/charts/radar-chart.tsx +40 -5
- package/src/component-pool/charts/scatter-chart-demo.tsx +0 -3
- package/src/component-pool/charts/scatter-chart.tsx +5 -2
- package/src/component-pool/index.ts +1 -0
- package/src/component-pool/svg-graph/index.ts +1 -0
- package/src/component-pool/svg-graph/svg-graph-demo.tsx +59 -0
- package/src/component-pool/svg-graph/svg-graph.ts +166 -0
- package/src/component-pool/youtube-player/youtube-player-demo.tsx +4 -4
- package/src/component-pool/youtube-player/youtube-player.tsx +90 -14
- package/src/components/action-sheet-demo.tsx +12 -0
- package/src/components/action-sheet-theme.tsx +150 -0
- package/src/components/index.ts +1 -0
- package/src/components/menu-bar.tsx +15 -4
- package/src/components/menu-sidebar.tsx +24 -12
- package/src/components/message-box.tsx +18 -0
- package/src/components/popup-menu.tsx +4 -4
- package/src/components/tabs.tsx +9 -6
- package/src/demo/demo-about.tsx +1 -1
- package/src/demo/demo-frame-helper.tsx +8 -1
- package/src/demo/demo-registry.ts +2 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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'>
|
|
@@ -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
|
-
|
|
8
|
+
srcOrVideoId: 'dQw4w9WgXcQ',
|
|
9
9
|
width: '100%',
|
|
10
10
|
height: '400px',
|
|
11
11
|
autoplay: false,
|
|
12
12
|
},
|
|
13
13
|
argTypes: {
|
|
14
|
-
|
|
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
|
|
43
|
-
<YouTubePlayer
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
<div class={className} css={{ position: 'relative', width, height, ...style }}>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
97
|
+
<div class={className} css={{ position: 'relative', width, height, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden', ...style }}>
|
|
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', aspectRatio: '16 / 9', height: 'auto', maxHeight: '100%' }}
|
|
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
|
};
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { ActionSheetTimePicker } from './action-sheet-time';
|
|
11
11
|
import { ActionSheetDatePicker } from './action-sheet-date';
|
|
12
12
|
import { ActionSheetColorPicker } from './action-sheet-color';
|
|
13
|
+
import { ActionSheetThemePicker } from './action-sheet-theme';
|
|
13
14
|
import { Button, ButtonSize } from './button';
|
|
14
15
|
|
|
15
16
|
export const actionSheetDemo: DemoStory<any> = {
|
|
@@ -180,6 +181,17 @@ export const actionSheetDemo: DemoStory<any> = {
|
|
|
180
181
|
if (result) console.log('Color selected:', result);
|
|
181
182
|
}}
|
|
182
183
|
/>
|
|
184
|
+
<Button
|
|
185
|
+
text='Show Theme Picker'
|
|
186
|
+
size={ButtonSize.Medium}
|
|
187
|
+
onClick={async () => {
|
|
188
|
+
const result = await ActionSheetThemePicker({
|
|
189
|
+
title: 'Pick a theme variable',
|
|
190
|
+
value: 'var(--primary-color)',
|
|
191
|
+
});
|
|
192
|
+
if (result) console.log('Theme variable selected:', result);
|
|
193
|
+
}}
|
|
194
|
+
/>
|
|
183
195
|
</div>
|
|
184
196
|
);
|
|
185
197
|
},
|