@sailingrotevista/rotevista-dash 8.0.1 → 8.0.2

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.

Potentially problematic release.


This version of @sailingrotevista/rotevista-dash might be problematic. Click here for more details.

package/src/App.tsx DELETED
@@ -1,346 +0,0 @@
1
- import React, { useCallback } from 'react';
2
- import { useDashboard } from './hooks/useDashboard';
3
- import { DataBox } from './components/DataBox';
4
- import { Sparkline } from './components/Sparkline';
5
- import { AnalogGauge } from './components/AnalogGauge';
6
- import { WeatherRadar } from './components/WeatherRadar';
7
- import { MiniCompass } from './components/MiniCompass';
8
- import { BoxType } from './types';
9
-
10
- export const App: React.FC = () => {
11
- const {
12
- config,
13
- displayModeSog,
14
- setDisplayModeSog,
15
- displayModeTws,
16
- setDisplayModeTws,
17
- activeInstrument,
18
- setActiveInstrument,
19
- isNightMode,
20
- setIsNightMode,
21
- isFocusActive,
22
- focusedBox,
23
- graphModes,
24
- connectionStatus,
25
- displayValues,
26
- histories,
27
- windRadarSlots,
28
- futureForecast,
29
- toggleFocusMode,
30
- toggleGraphMode,
31
- } = useDashboard();
32
-
33
- // Fullscreen toggle on center hotspot tap
34
- const handleToggleFullscreen = useCallback(() => {
35
- if (!document.fullscreenElement) {
36
- document.documentElement.requestFullscreen().catch(() => {});
37
- } else {
38
- document.exitFullscreen().catch(() => {});
39
- }
40
- }, []);
41
-
42
- // Night Mode toggle on 1s press of center hotspot
43
- const handleToggleNightMode = useCallback(() => {
44
- setIsNightMode(prev => {
45
- const next = !prev;
46
- try {
47
- const saved = localStorage.getItem('rotevista_dash_state');
48
- const state = saved ? JSON.parse(saved) : {};
49
- state.isNightMode = next;
50
- localStorage.setItem('rotevista_dash_state', JSON.stringify(state));
51
- } catch (e) {}
52
- return next;
53
- });
54
- }, [setIsNightMode]);
55
-
56
- // Handle Box TWD Long Press -> Switch to Weather Radar
57
- const handleTwdLongPress = useCallback(() => {
58
- setActiveInstrument('radar');
59
- }, [setActiveInstrument]);
60
-
61
- // Handle Box TWD Single Tap in Radar Mode -> Switch back to Gauge
62
- const handleTwdSingleTap = useCallback(() => {
63
- if (activeInstrument === 'radar') {
64
- setActiveInstrument('gauge');
65
- }
66
- }, [activeInstrument, setActiveInstrument]);
67
-
68
- const isLeftFocused = focusedBox && ['stw', 'sog', 'hdg', 'cog', 'tack'].includes(focusedBox);
69
- const containerClasses = [
70
- 'main-container',
71
- isFocusActive ? 'focus-active' : '',
72
- isFocusActive && isLeftFocused ? 'focus-side-left' : '',
73
- isFocusActive && !isLeftFocused ? 'focus-side-right' : '',
74
- ]
75
- .filter(Boolean)
76
- .join(' ');
77
-
78
- return (
79
- <div className={containerClasses}>
80
- {/* ================= COLONNA SINISTRA ================= */}
81
- {/* 1. STW */}
82
- <DataBox
83
- type="stw"
84
- label="STW"
85
- unit="kts"
86
- value={displayValues.stw}
87
- isFocused={focusedBox === 'stw'}
88
- isHercules={graphModes.stw === 'hercules'}
89
- onLongPress={() => toggleFocusMode('stw')}
90
- onDoubleTap={() => toggleGraphMode('stw')}
91
- >
92
- <div className="graph-wrapper">
93
- <Sparkline
94
- id="stw-graph"
95
- type="stw"
96
- data={histories.stw}
97
- config={config}
98
- mode={graphModes.stw}
99
- isNavigating={displayValues.isNavigating}
100
- displayMode="STW"
101
- isFocused={focusedBox === 'stw'}
102
- />
103
- </div>
104
- </DataBox>
105
-
106
- {/* 2. SOG / VMG */}
107
- <DataBox
108
- type="sog"
109
- label={displayModeSog}
110
- unit="kts"
111
- value={displayValues.sog}
112
- valueColor={displayValues.sogColor}
113
- isFocused={focusedBox === 'sog'}
114
- isHercules={graphModes.sog === 'hercules'}
115
- onLongPress={() => toggleFocusMode('sog')}
116
- onDoubleTap={() => toggleGraphMode('sog')}
117
- onSingleTap={() => {
118
- if (!isFocusActive) {
119
- setDisplayModeSog(prev => (prev === 'SOG' ? 'VMG' : 'SOG'));
120
- } else {
121
- toggleFocusMode('sog');
122
- }
123
- }}
124
- >
125
- <div className="graph-wrapper">
126
- <Sparkline
127
- id="sog-graph"
128
- type="sog"
129
- data={displayModeSog === 'VMG' ? histories.vmg : histories.sog}
130
- config={config}
131
- mode={graphModes.sog}
132
- isNavigating={displayValues.isNavigating}
133
- displayMode={displayModeSog}
134
- isFocused={focusedBox === 'sog'}
135
- />
136
- </div>
137
- </DataBox>
138
-
139
- {/* 3. HEADING (MEAN) */}
140
- <DataBox
141
- type="hdg"
142
- label="HEADING (MEAN)"
143
- value={displayValues.hdg}
144
- isLargeValue
145
- unstable={displayValues.hdgUnstable}
146
- isFocused={focusedBox === 'hdg'}
147
- onLongPress={() => toggleFocusMode('hdg')}
148
- onSingleTap={() => {
149
- if (isFocusActive) toggleFocusMode('hdg');
150
- }}
151
- />
152
-
153
- {/* 4. COG (MEAN) */}
154
- <DataBox
155
- type="cog"
156
- label="COG (MEAN)"
157
- value={displayValues.cog}
158
- isLargeValue
159
- unstable={displayValues.cogUnstable}
160
- isFocused={focusedBox === 'cog'}
161
- onLongPress={() => toggleFocusMode('cog')}
162
- onSingleTap={() => {
163
- if (isFocusActive) toggleFocusMode('cog');
164
- }}
165
- />
166
-
167
- {/* 5. TACK / GYBE OPPOSITE COURSE */}
168
- <DataBox
169
- type="tack"
170
- label={displayValues.tackLabel}
171
- isFocused={focusedBox === 'tack'}
172
- onLongPress={() => toggleFocusMode('tack')}
173
- onSingleTap={() => {
174
- if (isFocusActive) toggleFocusMode('tack');
175
- }}
176
- >
177
- <div className="dual-value-container">
178
- <div className="dual-value-col tack-hdg-col">
179
- <span className="dual-label">HDG</span>
180
- <span
181
- className={`value dual-val ${displayValues.tackHdgUnstable ? 'unstable-data' : ''}`}
182
- id="tack-hdg"
183
- >
184
- {displayValues.tackHdg}
185
- </span>
186
- </div>
187
- <div className="dual-value-col tack-cog-col">
188
- <span className="dual-label">COG</span>
189
- <span
190
- className={`value dual-val ${displayValues.tackCogUnstable ? 'unstable-data' : ''}`}
191
- id="tack-cog"
192
- >
193
- {displayValues.tackCog}
194
- </span>
195
- </div>
196
- </div>
197
- </DataBox>
198
-
199
- {/* ================= BUSSOLA CENTRALE ================= */}
200
- <div className="box-gauge">
201
- {/* STATO CONNESSIONE */}
202
- <div id="status" className={connectionStatus.className}>
203
- {connectionStatus.text}
204
- </div>
205
-
206
- {activeInstrument === 'gauge' ? (
207
- <AnalogGauge
208
- awsVal={parseFloat(displayValues.awsSvg) || 0}
209
- awaDeg={displayValues.awaPointerDeg}
210
- twaDeg={displayValues.twaPointerDeg}
211
- leewayDeg={displayValues.leewayDeg}
212
- trackDeg={displayValues.trackRotDeg}
213
- tacticCwDot={displayValues.tacticCwDot}
214
- tacticCcwDot={displayValues.tacticCcwDot}
215
- onCenterHotspotLongPress={handleToggleNightMode}
216
- onCenterHotspotClick={handleToggleFullscreen}
217
- />
218
- ) : (
219
- <WeatherRadar
220
- twdHistory={histories.twd}
221
- twsHistory={histories.tws}
222
- windRadarSlots={windRadarSlots}
223
- futureForecast={futureForecast}
224
- headingTrueDeg={displayValues.boatCompassRotDeg}
225
- config={config}
226
- meteoLed={displayValues.meteoLed}
227
- onSwitchToGauge={() => setActiveInstrument('gauge')}
228
- />
229
- )}
230
- </div>
231
-
232
- {/* ================= COLONNA DESTRA ================= */}
233
- {/* 6. DEPTH */}
234
- <DataBox
235
- type="depth"
236
- label="DEPTH"
237
- unit="m"
238
- value={displayValues.depth}
239
- alarmClass={displayValues.depthAlarmClass}
240
- isFocused={focusedBox === 'depth'}
241
- isHercules={graphModes.depth === 'hercules'}
242
- onLongPress={() => toggleFocusMode('depth')}
243
- onDoubleTap={() => toggleGraphMode('depth')}
244
- >
245
- <div className="graph-wrapper">
246
- <Sparkline
247
- id="depth-graph"
248
- type="depth"
249
- data={histories.depth}
250
- config={config}
251
- mode={graphModes.depth}
252
- isNavigating={displayValues.isNavigating}
253
- displayMode="DEPTH"
254
- isFocused={focusedBox === 'depth'}
255
- />
256
- </div>
257
- </DataBox>
258
-
259
- {/* 7. TWS / AWS */}
260
- <DataBox
261
- type="tws"
262
- label={displayModeTws}
263
- unit="kts"
264
- value={displayValues.tws}
265
- valueColor={displayValues.twsColor}
266
- isFocused={focusedBox === 'tws'}
267
- isHercules={graphModes.tws === 'hercules'}
268
- onLongPress={() => toggleFocusMode('tws')}
269
- onDoubleTap={() => toggleGraphMode('tws')}
270
- onSingleTap={() => {
271
- if (!isFocusActive) {
272
- setDisplayModeTws(prev => (prev === 'TWS' ? 'AWS' : 'TWS'));
273
- } else {
274
- toggleFocusMode('tws');
275
- }
276
- }}
277
- >
278
- <div className="graph-wrapper">
279
- <Sparkline
280
- id="tws-graph"
281
- type="tws"
282
- data={displayModeTws === 'AWS' ? histories.aws : histories.tws}
283
- config={config}
284
- mode={graphModes.tws}
285
- isNavigating={displayValues.isNavigating}
286
- displayMode={displayModeTws}
287
- isFocused={focusedBox === 'tws'}
288
- />
289
- </div>
290
- </DataBox>
291
-
292
- {/* 8. TWA (MEAN) */}
293
- <DataBox
294
- type="twa"
295
- label="TWA (MEAN)"
296
- value={displayValues.twaAvg}
297
- isLargeValue
298
- unstable={displayValues.twaUnstable}
299
- isFocused={focusedBox === 'twa'}
300
- onLongPress={() => toggleFocusMode('twa')}
301
- onSingleTap={() => {
302
- if (isFocusActive) toggleFocusMode('twa');
303
- }}
304
- />
305
-
306
- {/* 9. AWA (MEAN) */}
307
- <DataBox
308
- type="awa"
309
- label="AWA (MEAN)"
310
- value={displayValues.awaAvg}
311
- isLargeValue
312
- unstable={displayValues.awaUnstable}
313
- isFocused={focusedBox === 'awa'}
314
- onLongPress={() => toggleFocusMode('awa')}
315
- onSingleTap={() => {
316
- if (isFocusActive) toggleFocusMode('awa');
317
- }}
318
- />
319
-
320
- {/* 10. TWD (MEAN) WITH MINI COMPASS */}
321
- <DataBox
322
- type="twd"
323
- label="TWD (MEAN)"
324
- isFocused={focusedBox === 'twd'}
325
- onLongPress={handleTwdLongPress}
326
- onSingleTap={handleTwdSingleTap}
327
- >
328
- <div className="value-with-compass">
329
- <MiniCompass
330
- boatRotDeg={displayValues.boatCompassRotDeg}
331
- windRotDeg={displayValues.windCompassRotDeg}
332
- compassCwDot={displayValues.compassCwDot}
333
- compassCcwDot={displayValues.compassCcwDot}
334
- />
335
- <span
336
- className={`value-large ${displayValues.twdUnstable ? 'unstable-data' : ''}`}
337
- id="twd-avg"
338
- >
339
- {displayValues.twdAvg}
340
- </span>
341
- </div>
342
- </DataBox>
343
- </div>
344
- );
345
- };
346
- export default App;
@@ -1,243 +0,0 @@
1
- import React, { useMemo, useEffect, useRef } from 'react';
2
-
3
- interface AnalogGaugeProps {
4
- awsVal: number;
5
- awaDeg: number;
6
- twaDeg: number;
7
- leewayDeg: number;
8
- trackDeg: number;
9
- tacticCwDot: { active: boolean; color: string; gybing?: boolean };
10
- tacticCcwDot: { active: boolean; color: string; gybing?: boolean };
11
- onCenterHotspotLongPress: () => void;
12
- onCenterHotspotClick: () => void;
13
- }
14
-
15
- export const AnalogGauge: React.FC<AnalogGaugeProps> = ({
16
- awsVal,
17
- awaDeg,
18
- twaDeg,
19
- leewayDeg,
20
- trackDeg,
21
- tacticCwDot,
22
- tacticCcwDot,
23
- onCenterHotspotLongPress,
24
- onCenterHotspotClick,
25
- }) => {
26
- const pressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
27
- const isLongPressRef = useRef(false);
28
-
29
- // Generate compass ticks
30
- const ticks = useMemo(() => {
31
- const list: React.ReactNode[] = [];
32
- for (let i = 0; i < 360; i += 10) {
33
- const isMajor = i % 30 === 0;
34
- list.push(
35
- <line
36
- key={`tick-${i}`}
37
- x1="200"
38
- y1="40"
39
- x2="200"
40
- y2={isMajor ? 60 : 50}
41
- stroke={isMajor ? '#000' : '#bbb'}
42
- strokeWidth={isMajor ? 2 : 1}
43
- transform={`rotate(${i}, 200, 200)`}
44
- />
45
- );
46
- }
47
- return list;
48
- }, []);
49
-
50
- const clampedLeeway = Math.max(-20, Math.min(20, leewayDeg));
51
- const c = 125;
52
- const px = 125 / 20;
53
- const leewayWidth = Math.min(Math.abs(clampedLeeway) * px, 125);
54
- const leewayX = clampedLeeway >= 0 ? c : c - leewayWidth;
55
-
56
- const handlePointerDown = () => {
57
- isLongPressRef.current = false;
58
- pressTimerRef.current = setTimeout(() => {
59
- isLongPressRef.current = true;
60
- onCenterHotspotLongPress();
61
- }, 1000);
62
- };
63
-
64
- const handlePointerUp = () => {
65
- if (pressTimerRef.current) {
66
- clearTimeout(pressTimerRef.current);
67
- pressTimerRef.current = null;
68
- }
69
- if (!isLongPressRef.current) {
70
- onCenterHotspotClick();
71
- }
72
- isLongPressRef.current = false;
73
- };
74
-
75
- const handlePointerLeave = () => {
76
- if (pressTimerRef.current) {
77
- clearTimeout(pressTimerRef.current);
78
- pressTimerRef.current = null;
79
- }
80
- isLongPressRef.current = false;
81
- };
82
-
83
- return (
84
- <svg id="wind-gauge" viewBox="35 38 330 395" preserveAspectRatio="xMidYMid meet">
85
- <defs>
86
- <clipPath id="boat-clip">
87
- <circle cx="200" cy="200" r="50" />
88
- </clipPath>
89
- <linearGradient id="axiom-grad" x1="0%" y1="0%" x2="0%" y2="100%">
90
- <stop offset="0%" stopColor="#ffffff" stopOpacity={1} />
91
- <stop offset="100%" stopColor="#888888" stopOpacity={1} />
92
- </linearGradient>
93
- <linearGradient id="leeway-grad" x1="0%" y1="0%" x2="100%" y2="0%">
94
- <stop offset="0%" stopColor="#ff0000" stopOpacity={1} />
95
- <stop offset="25%" stopColor="#ff8800" stopOpacity={1} />
96
- <stop offset="50%" stopColor="#00ff00" stopOpacity={1} />
97
- <stop offset="75%" stopColor="#ff8800" stopOpacity={1} />
98
- <stop offset="100%" stopColor="#ff0000" stopOpacity={1} />
99
- </linearGradient>
100
- <linearGradient id="leeway-night-grad" x1="0%" y1="0%" x2="100%" y2="0%">
101
- <stop offset="0%" stopColor="#ff0000" stopOpacity={1} />
102
- <stop offset="50%" stopColor="#330000" stopOpacity={1} />
103
- <stop offset="100%" stopColor="#ff0000" stopOpacity={1} />
104
- </linearGradient>
105
- <clipPath id="leeway-clip">
106
- <rect id="leeway-mask-rect" x={leewayX} y="0" width={leewayWidth} height="12" rx="2" />
107
- </clipPath>
108
- <filter id="center-glow" x="-20%" y="-20%" width="140%" height="140%">
109
- <feDropShadow dx="0" dy="0" stdDeviation="8" floodColor="#aaaaaa" floodOpacity={0.5} />
110
- </filter>
111
- </defs>
112
-
113
- {/* Dials */}
114
- <circle cx="200" cy="200" r="160" fill="#050505" />
115
- <circle cx="200" cy="200" r="125" fill="#121212" />
116
-
117
- {/* Beating & Running Arcs */}
118
- <path d="M 82.0 101.0 A 154 154 0 0 1 142.3 57.2" fill="none" stroke="#ff0000" strokeWidth="12" />
119
- <path d="M 257.7 57.2 A 154 154 0 0 1 318.0 101.0" fill="none" stroke="#00ff00" strokeWidth="12" />
120
- <path d="M 265.1 339.6 A 154 154 0 0 1 134.9 339.6" fill="none" stroke="#ff8800" strokeWidth="12" />
121
-
122
- {/* Ticks */}
123
- <g id="ticks">{ticks}</g>
124
-
125
- {/* Tick Labels */}
126
- <g id="tick-labels" fill="#bbb" textAnchor="middle" dominantBaseline="middle" fontFamily="Arial" fontWeight="bold">
127
- <text fontSize="16" transform="translate(200, 74)">0</text>
128
- <text fontSize="16" transform="translate(326, 200) rotate(90)">90</text>
129
- <text fontSize="16" transform="translate(74, 200) rotate(-90)">90</text>
130
- <text fontSize="16" transform="translate(200, 326) rotate(180)">180</text>
131
- <text fontSize="11" transform="translate(262.5, 91.7) rotate(30)">30</text>
132
- <text fontSize="11" transform="translate(308.3, 137.5) rotate(60)">60</text>
133
- <text fontSize="11" transform="translate(308.3, 262.5) rotate(120)">120</text>
134
- <text fontSize="11" transform="translate(262.5, 308.3) rotate(150)">150</text>
135
- <text fontSize="11" transform="translate(137.5, 91.7) rotate(-30)">30</text>
136
- <text fontSize="11" transform="translate(91.7, 137.5) rotate(-60)">60</text>
137
- <text fontSize="11" transform="translate(91.7, 262.5) rotate(-120)">120</text>
138
- <text fontSize="11" transform="translate(137.5, 308.3) rotate(-150)">150</text>
139
- </g>
140
-
141
- {/* Track / Leeway Drift Pointer */}
142
- <g id="track-pointer" transform={`rotate(${trackDeg}, 200, 200)`}>
143
- <path d="M200,42 L194,58 L206,58 Z" fill="#007aff" stroke="#fff" strokeWidth="0.5" />
144
- </g>
145
-
146
- {/* Center Interactive Hotspot */}
147
- <circle
148
- id="fullscreen-hotspot"
149
- cx="200"
150
- cy="200"
151
- r="55"
152
- fill="#181818"
153
- stroke="#333"
154
- strokeWidth="1"
155
- filter="url(#center-glow)"
156
- cursor="pointer"
157
- onPointerDown={handlePointerDown}
158
- onPointerUp={handlePointerUp}
159
- onPointerLeave={handlePointerLeave}
160
- />
161
-
162
- {/* Boat Silhouette */}
163
- <path
164
- id="boat-icon"
165
- d="M200,150 Q165,185 170,250 Q165,190 200,173 Q235,190 230,250 Q235,185 200,150 Z"
166
- fill="url(#axiom-grad)"
167
- transform="translate(0, 5)"
168
- clipPath="url(#boat-clip)"
169
- style={{ pointerEvents: 'none' }}
170
- />
171
-
172
- {/* AWS Center Display */}
173
- <g id="aws-display-group" transform="translate(200, 265)">
174
- <text x="0" y="0" fill="#777" fontSize="10" fontWeight="bold" textAnchor="middle" style={{ textTransform: 'uppercase' }}>
175
- Apparent Wind kts
176
- </text>
177
- <text id="aws-val-svg" x="0" y="42" fill="#fff" fontSize="52" fontWeight="bold" textAnchor="middle">
178
- {awsVal.toFixed(1)}
179
- </text>
180
- </g>
181
-
182
- {/* AWA Pointer (Orange with 'A') */}
183
- <g id="awa-pointer" transform={`rotate(${awaDeg}, 200, 200)`} opacity={0.9}>
184
- <path d="M 200,70 L 213,95 L 200,145 L 187,95 Z" fill="#ff8c00" stroke="#000" strokeWidth="1" />
185
- <text x="200" y="90" fill="#000" fontSize="10" fontWeight="900" textAnchor="middle" fontFamily="Arial Black">
186
- A
187
- </text>
188
- </g>
189
-
190
- {/* TWA Pointer (Yellow with 'T') and Tactical Trend Dots */}
191
- <g id="twa-pointer" transform={`rotate(${twaDeg}, 200, 200)`} opacity={0.9}>
192
- <path
193
- d="M 200,92 A 8,8 0 0 1 208,100 C 208,108 200,128 200,128 C 200,128 192,108 192,100 A 8,8 0 0 1 200,92 Z"
194
- fill="#ffff00"
195
- stroke="#000"
196
- strokeWidth="0.8"
197
- />
198
- <text x="200" y="106" fill="#000" fontSize="9" fontWeight="900" textAnchor="middle" fontFamily="Arial Black">
199
- T
200
- </text>
201
- <circle
202
- id="trend-gauge-cw"
203
- cx="215"
204
- cy="110"
205
- r="4"
206
- fill={tacticCwDot.color}
207
- className={`${tacticCwDot.active ? 'is-trending' : ''} ${tacticCwDot.gybing ? 'is-gybing' : ''}`}
208
- />
209
- <circle
210
- id="trend-gauge-ccw"
211
- cx="185"
212
- cy="110"
213
- r="4"
214
- fill={tacticCcwDot.color}
215
- className={`${tacticCcwDot.active ? 'is-trending' : ''} ${tacticCcwDot.gybing ? 'is-gybing' : ''}`}
216
- />
217
- </g>
218
-
219
- {/* Leeway Bar Indicator */}
220
- <g transform="translate(75, 395)">
221
- <text x="125" y="-12" id="leeway-val" fill="#aaa" fontSize="11" textAnchor="middle" fontWeight="bold">
222
- {`LEEWAY: ${clampedLeeway.toFixed(1)}°`}
223
- </text>
224
- <rect x="0" y="0" width="250" height="12" fill="#222" rx="3" />
225
- <rect x="0" y="0" width="250" height="12" fill="url(#leeway-grad)" clipPath="url(#leeway-clip)" rx="3" />
226
- <g stroke="#555" strokeWidth="1">
227
- <line x1="0" y1="-2" x2="0" y2="14" />
228
- <line x1="62.5" y1="2" x2="62.5" y2="10" />
229
- <line x1="125" y1="-2" x2="125" y2="14" />
230
- <line x1="187.5" y1="2" x2="187.5" y2="10" />
231
- <line x1="250" y1="-2" x2="250" y2="14" />
232
- </g>
233
- <g fill="#555" fontSize="8" textAnchor="middle" fontWeight="bold">
234
- <text x="0" y="24">-20°</text>
235
- <text x="62.5" y="24">-10</text>
236
- <text x="125" y="24">0°</text>
237
- <text x="187.5" y="24">10</text>
238
- <text x="250" y="24">20°</text>
239
- </g>
240
- </g>
241
- </svg>
242
- );
243
- };