+
+
Color presets
+
+ {COLOR_PRESETS.map((preset) => (
+
+ ))}
+
+
+
+
Number of arcs
+
+ {[3, 10, 50, 100].map((n) => (
+
+ ))}
+
+
+
+
Colors
+
+
+ {(cfg?.arc?.colorArray || ['#5BE12C', '#F5CD19', '#EA4228']).map((color: string, i: number) => {
+ const colors = cfg?.arc?.colorArray || ['#5BE12C', '#F5CD19', '#EA4228'];
+ return (
+
+ {
+ const newColors = [...colors];
+ newColors[i] = e.target.value;
+ onConfigChange({ ...config, arc: { ...cfg?.arc, colorArray: newColors, nbSubArcs: newColors.length, subArcs: [] } });
+ }}
+ style={styles.colorPicker}
+ title={`Color ${i + 1}`}
+ />
+ {colors.length > 2 && (
+
+ )}
+
+ );
+ })}
+
+
+
+
+ Width
+ onConfigChange({ ...config, arc: { ...cfg?.arc, width: Number(e.target.value) } })}
+ style={{ ...styles.slider, flex: 1 }} title="Arc width" />
+
+
+ Corner
+ onConfigChange({ ...config, arc: { ...cfg?.arc, cornerRadius: Number(e.target.value) } })}
+ style={{ ...styles.slider, flex: 1 }} title="Corner radius" />
+
+
+ Pad
+ onConfigChange({ ...config, arc: { ...cfg?.arc, padding: Number(e.target.value) } })}
+ style={{ ...styles.slider, flex: 1 }} title="Padding between arcs" />
+
+
+
+ Border
+ onConfigChange({ ...config, arc: { ...cfg?.arc, subArcsStrokeWidth: Number(e.target.value) } })}
+ style={{ ...styles.slider, flex: 1 }} title="SubArc border width" />
+ {(cfg?.arc?.subArcsStrokeWidth ?? 0) > 0 && (
+ onConfigChange({ ...config, arc: { ...cfg?.arc, subArcsStrokeColor: e.target.value } })}
+ style={styles.colorPicker} title="SubArc border color" />
+ )}
+
+ {/* Custom Angle Controls */}
+
+
+ Angles
+
+
+
+ Start
+ {
+ const start = Number(e.target.value);
+ const currentEnd = cfg?.endAngle ?? (cfg?.type === 'semicircle' ? 90 : cfg?.type === 'radial' ? 130 : 112);
+ onConfigChange({ ...config, startAngle: start, endAngle: currentEnd });
+ }}
+ style={{ ...styles.slider, flex: 1 }} title="Start angle (degrees)" />
+ {cfg?.startAngle ?? (cfg?.type === 'semicircle' ? -90 : cfg?.type === 'radial' ? -130 : -112)}°
+
+
+ End
+ {
+ const end = Number(e.target.value);
+ const currentStart = cfg?.startAngle ?? (cfg?.type === 'semicircle' ? -90 : cfg?.type === 'radial' ? -130 : -112);
+ onConfigChange({ ...config, startAngle: currentStart, endAngle: end });
+ }}
+ style={{ ...styles.slider, flex: 1 }} title="End angle (degrees)" />
+ {cfg?.endAngle ?? (cfg?.type === 'semicircle' ? 90 : cfg?.type === 'radial' ? 130 : 112)}°
+
+
+
+
+
+ {/* */}
+
+ {/* Grafana-specific: outer arc & empty color */}
+ {cfg?.type === 'grafana' && (
+ <>
+
+
+ Outer Arc (Grafana)
+
+
+ Corner
+ onConfigChange({ ...config, arc: { ...cfg?.arc, outerArc: { ...cfg?.arc?.outerArc, cornerRadius: Number(e.target.value) } } })}
+ style={{ ...styles.slider, flex: 1 }} title="Outer arc corner radius" />
+
+
+ Pad
+ onConfigChange({ ...config, arc: { ...cfg?.arc, outerArc: { ...cfg?.arc?.outerArc, padding: Number(e.target.value) } } })}
+ style={{ ...styles.slider, flex: 1 }} title="Outer arc padding" />
+
+
+ Width
+ onConfigChange({ ...config, arc: { ...cfg?.arc, outerArc: { ...cfg?.arc?.outerArc, width: Number(e.target.value) } } })}
+ style={{ ...styles.slider, flex: 1 }} title="Outer arc width" />
+
+
+ Empty color
+ onConfigChange({ ...config, arc: { ...cfg?.arc, emptyColor: e.target.value } })}
+ style={styles.colorPicker} title="Empty arc color" />
+
+ >
+ )}
+
+ {/* Arc Effects */}
+
+
+ Effects
+
+
+
+
+
+
+ {cfg?.arc?.effects?.glow && (
+ <>
+
+ Blur
+ onConfigChange({ ...config, arc: { ...cfg?.arc, effects: { ...cfg?.arc?.effects, glowBlur: Number(e.target.value) } } })}
+ style={{ ...styles.slider, flex: 1 }} title="Glow blur intensity" />
+ {cfg?.arc?.effects?.glowBlur ?? 10}
+
+
+ Spread
+ onConfigChange({ ...config, arc: { ...cfg?.arc, effects: { ...cfg?.arc?.effects, glowSpread: Number(e.target.value) } } })}
+ style={{ ...styles.slider, flex: 1 }} title="Glow spread/intensity" />
+ {cfg?.arc?.effects?.glowSpread ?? 1}
+
+ >
+ )}
+ {cfg?.arc?.effects?.dropShadow && (
+
+ Shadow
+ onConfigChange({ ...config, arc: { ...cfg?.arc, effects: { ...cfg?.arc?.effects, dropShadow: { ...cfg?.arc?.effects?.dropShadow, blur: Number(e.target.value) } } } })}
+ style={{ ...styles.slider, flex: 1 }} title="Shadow blur" />
+ onConfigChange({ ...config, arc: { ...cfg?.arc, effects: { ...cfg?.arc?.effects, dropShadow: { ...cfg?.arc?.effects?.dropShadow, opacity: Number(e.target.value) } } } })}
+ style={{ ...styles.slider, flex: 1 }} title="Shadow opacity" />
+
+ )}
+
+
+
+
+
+ {/* Pointers List Header */}
+
+
+ {`${(cfg?.pointers?.length || 1)} pointer${(cfg?.pointers?.length || 1) > 1 ? 's' : ''}`}
+
+
+
+
+ {/* Value Display Mode (when multiple pointers) */}
+ {(cfg?.pointers?.length ?? 0) > 1 && (
+
+ Show values:
+ {['primary', 'all', 'none'].map((mode) => (
+
+ ))}
+
+ )}
+
+ {/* Pointer Accordions - use pointers array if exists, otherwise create from single pointer */}
+ {/* When pointers is empty array (0 pointers), don't show any accordions */}
+ {(Array.isArray(cfg?.pointers) ? cfg.pointers : [{
+ value: value,
+ type: cfg?.pointer?.type || 'needle',
+ color: cfg?.pointer?.color,
+ baseColor: cfg?.pointer?.baseColor || '#ffffff',
+ length: cfg?.pointer?.length ?? 0.7,
+ width: cfg?.pointer?.width ?? 15,
+ strokeWidth: cfg?.pointer?.strokeWidth ?? 0,
+ strokeColor: cfg?.pointer?.strokeColor,
+ arrowOffset: cfg?.pointer?.arrowOffset,
+ blobOffset: cfg?.pointer?.blobOffset,
+ }]).map((pointer: PointerWithValue, index: number) => (
+
{
+ // Check if we have pointers array in config
+ const hasPointers = cfg?.pointers && cfg.pointers.length > 0;
+
+ if (hasPointers) {
+ // Already in multi-pointer mode - update the array
+ const pointers = [...cfg.pointers];
+ pointers[index] = updated;
+ onConfigChange({ ...config, pointers });
+ // Sync first pointer value to main value for compatibility
+ if (index === 0) onValueChange(updated.value);
+ } else {
+ // Single pointer mode - update value and pointer props directly
+ // DON'T convert to pointers array - that causes mode transition and reinit
+ onValueChange(updated.value);
+ // Update pointer config (type, color, length, width, etc.) without converting to array
+ const { value: _, ...pointerProps } = updated;
+ onConfigChange({
+ ...config,
+ pointer: { ...cfg?.pointer, ...pointerProps }
+ });
+ }
+ }}
+ onRemove={() => {
+ if (cfg?.pointers && cfg.pointers.length > 1) {
+ // Remove pointer from array
+ const pointers = cfg.pointers.filter((_: any, i: number) => i !== index);
+ onConfigChange({ ...config, pointers });
+ } else {
+ // Last pointer - set to empty array (0 pointers)
+ onConfigChange({ ...config, pointers: [] });
+ }
+ }}
+ />
+ ))}
+
+ {/* Animation Settings (shared) */}
+
+
+
Animation
+
+
+
+
+ Duration
+ onConfigChange({ ...config, pointer: { ...cfg?.pointer, animationDuration: Number(e.target.value) } })}
+ style={{ ...styles.slider, flex: 1 }} title="Animation duration (ms)" />
+ {((cfg?.pointer?.animationDuration ?? 3000) / 1000).toFixed(1)}s
+
+
+ Delay
+ onConfigChange({ ...config, pointer: { ...cfg?.pointer, animationDelay: Number(e.target.value) } })}
+ style={{ ...styles.slider, flex: 1 }} />
+ {cfg?.pointer?.animationDelay ?? 100}ms
+
+ {/* Performance Controls */}
+
+ Max FPS
+ onConfigChange({ ...config, pointer: { ...cfg?.pointer, maxFps: Number(e.target.value) } })}
+ style={{ ...styles.slider, flex: 1 }} title="Maximum frames per second (lower = less GPU load)" />
+ {cfg?.pointer?.maxFps ?? 60}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Label + Tick Marks in same column */}
+
+
+
+
+
+
+
+ Size
+ onConfigChange({ ...config, labels: { ...cfg?.labels, valueLabel: { ...cfg?.labels?.valueLabel, hide: false, style: { ...cfg?.labels?.valueLabel?.style, fontSize: `${e.target.value}px` } } } })}
+ style={{ ...styles.slider, flex: 1 }} />
+
+
+ X
+ onConfigChange({ ...config, labels: { ...cfg?.labels, valueLabel: { ...cfg?.labels?.valueLabel, offsetX: Number(e.target.value) } } })}
+ style={{ ...styles.slider, flex: 1 }} />
+ Y
+ onConfigChange({ ...config, labels: { ...cfg?.labels, valueLabel: { ...cfg?.labels?.valueLabel, offsetY: Number(e.target.value) } } })}
+ style={{ ...styles.slider, flex: 1 }} />
+
+
+
+
+
+ Every
+ {[
+ { label: 'None', interval: 0 },
+ { label: '5', interval: 5 },
+ { label: '10', interval: 10 },
+ { label: '25', interval: 25 },
+ { label: '100', interval: 100 },
+ { label: '250', interval: 250 },
+ { label: '500', interval: 500 },
+ { label: '1k', interval: 1000 },
+ { label: '2.5k', interval: 2500 },
+ { label: '5k', interval: 5000 },
+ { label: '10k', interval: 10000 },
+ ].map((t) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ Line
+ onConfigChange({ ...config, labels: { ...cfg?.labels, tickLabels: { ...cfg?.labels?.tickLabels, defaultTickLineConfig: { ...cfg?.labels?.tickLabels?.defaultTickLineConfig, color: e.target.value } } } })}
+ style={styles.colorPicker} />
+ Text
+ onConfigChange({ ...config, labels: { ...cfg?.labels, tickLabels: { ...cfg?.labels?.tickLabels, defaultTickValueConfig: { ...cfg?.labels?.tickLabels?.defaultTickValueConfig, style: { ...cfg?.labels?.tickLabels?.defaultTickValueConfig?.style, fill: e.target.value } } } } })}
+ style={styles.colorPicker} />
+
+
+ Line W
+ onConfigChange({ ...config, labels: { ...cfg?.labels, tickLabels: { ...cfg?.labels?.tickLabels, defaultTickLineConfig: { ...cfg?.labels?.tickLabels?.defaultTickLineConfig, width: Number(e.target.value) } } } })}
+ style={{ ...styles.slider, flex: 1 }} />
+
+
+ Line L
+ onConfigChange({ ...config, labels: { ...cfg?.labels, tickLabels: { ...cfg?.labels?.tickLabels, defaultTickLineConfig: { ...cfg?.labels?.tickLabels?.defaultTickLineConfig, length: Number(e.target.value) } } } })}
+ style={{ ...styles.slider, flex: 1 }} />
+
+
+ Text Size
+ onConfigChange({ ...config, labels: { ...cfg?.labels, tickLabels: { ...cfg?.labels?.tickLabels, defaultTickValueConfig: { ...cfg?.labels?.tickLabels?.defaultTickValueConfig, style: { ...cfg?.labels?.tickLabels?.defaultTickValueConfig?.style, fontSize: `${e.target.value}px` } } } } })}
+ style={{ ...styles.slider, flex: 1 }} />
+
+
+ Distance
+ onConfigChange({ ...config, labels: { ...cfg?.labels, tickLabels: { ...cfg?.labels?.tickLabels, defaultTickLineConfig: { ...cfg?.labels?.tickLabels?.defaultTickLineConfig, distanceFromArc: Number(e.target.value) } } } })}
+ style={{ ...styles.slider, flex: 1 }} title="Distance from arc" />
+
+
+ Text Gap
+ onConfigChange({ ...config, labels: { ...cfg?.labels, tickLabels: { ...cfg?.labels?.tickLabels, defaultTickLineConfig: { ...cfg?.labels?.tickLabels?.defaultTickLineConfig, distanceFromText: Number(e.target.value) } } } })}
+ style={{ ...styles.slider, flex: 1 }} title="Distance between tick line and text" />
+
+
+
+
+