| 1 | "use client"; |
| 2 | |
| 3 | import { cn } from "@/lib/utils"; |
| 4 | import { Settings } from "lucide-react"; |
| 5 | |
| 6 | import { ToolbarButton, ToolbarGroup } from "@/components/plate/ui/toolbar"; |
| 7 | import { DebouncedInput } from "@/components/ui/debounced-input"; |
| 8 | import { Input } from "@/components/ui/input"; |
| 9 | import { Label } from "@/components/ui/label"; |
| 10 | import { |
| 11 | Popover, |
| 12 | PopoverContent, |
| 13 | PopoverTrigger, |
| 14 | } from "@/components/ui/popover"; |
| 15 | import { |
| 16 | Select, |
| 17 | SelectContent, |
| 18 | SelectItem, |
| 19 | SelectTrigger, |
| 20 | SelectValue, |
| 21 | } from "@/components/ui/select"; |
| 22 | import { Slider } from "@/components/ui/slider"; |
| 23 | import { Switch } from "@/components/ui/switch"; |
| 24 | import { useToolbarContext } from "./ToolbarContext"; |
| 25 | |
| 26 | // Type definition for axis configuration |
| 27 | interface AxisConfig { |
| 28 | title?: string | { text?: string; enabled?: boolean }; |
| 29 | label?: { enabled?: boolean }; |
| 30 | gridLine?: { enabled?: boolean }; |
| 31 | } |
| 32 | |
| 33 | export function ChartSettingsPopover() { |
| 34 | const { |
| 35 | element, |
| 36 | elementType, |
| 37 | handleNodePropertyUpdate, |
| 38 | isCurrentElementChart, |
| 39 | } = useToolbarContext(); |
| 40 | |
| 41 | if (!isCurrentElementChart) { |
| 42 | return null; |
| 43 | } |
| 44 | |
| 45 | // Get current values from element |
| 46 | const title = (element as { title?: { text?: string } })?.title?.text ?? ""; |
| 47 | const subtitle = |
| 48 | (element as { subtitle?: { text?: string } })?.subtitle?.text ?? ""; |
| 49 | |
| 50 | // Get per-axis configuration |
| 51 | const xAxisConfig = (element as { xAxis?: AxisConfig })?.xAxis ?? {}; |
| 52 | const yAxisConfig = (element as { yAxis?: AxisConfig })?.yAxis ?? {}; |
| 53 | |
| 54 | const normalizeTitle = (title?: AxisConfig["title"]) => |
| 55 | typeof title === "string" ? { text: title } : (title ?? {}); |
| 56 | |
| 57 | // Legacy fallbacks |
| 58 | const showGrid = (element as { showGrid?: boolean })?.showGrid ?? true; |
| 59 | const showAxisLabels = |
| 60 | (element as { showAxisLabels?: boolean })?.showAxisLabels ?? true; |
| 61 | |
| 62 | // Per-axis values with fallback to legacy |
| 63 | const xAxisTitleConfig = normalizeTitle(xAxisConfig.title); |
| 64 | const xAxisTitleEnabled = |
| 65 | xAxisTitleConfig.enabled ?? Boolean(xAxisTitleConfig.text); |
| 66 | const xAxisTitle = xAxisTitleConfig.text ?? ""; |
| 67 | const xAxisLabelEnabled = xAxisConfig.label?.enabled ?? showAxisLabels; |
| 68 | const xAxisGridEnabled = xAxisConfig.gridLine?.enabled ?? false; |
| 69 | |
| 70 | const yAxisTitleConfig = normalizeTitle(yAxisConfig.title); |
| 71 | const yAxisTitleEnabled = |
| 72 | yAxisTitleConfig.enabled ?? Boolean(yAxisTitleConfig.text); |
| 73 | const yAxisTitle = yAxisTitleConfig.text ?? ""; |
| 74 | const yAxisLabelEnabled = yAxisConfig.label?.enabled ?? showAxisLabels; |
| 75 | const yAxisGridEnabled = yAxisConfig.gridLine?.enabled ?? showGrid; |
| 76 | |
| 77 | // Animation settings |
| 78 | const animationEnabled = |
| 79 | (element as { disableAnimation?: boolean })?.disableAnimation === false || |
| 80 | (element as { animation?: { enabled?: boolean } })?.animation?.enabled === |
| 81 | true; |
| 82 | const animationDuration = |
| 83 | (element as { animation?: { duration?: number } })?.animation?.duration ?? |
| 84 | 500; |
| 85 | |
| 86 | // Legend settings |
| 87 | const legendEnabled = |
| 88 | (element as { showLegend?: boolean })?.showLegend ?? |
| 89 | (element as { legend?: { enabled?: boolean } })?.legend?.enabled ?? |
| 90 | true; |
| 91 | const legendPosition = |
| 92 | (element as { legend?: { position?: string } })?.legend?.position ?? |
| 93 | "bottom"; |
| 94 | |
| 95 | // Background settings |
| 96 | const backgroundFill = |
| 97 | (element as { background?: { fill?: string } })?.background?.fill ?? ""; |
| 98 | const backgroundVisible = |
| 99 | (element as { background?: { visible?: boolean } })?.background?.visible ?? |
| 100 | false; |
| 101 | |
| 102 | // Check if chart type supports axes (not applicable to pie/donut/gauge/etc) |
| 103 | const supportsAxes = ![ |
| 104 | "chart-pie", |
| 105 | "chart-donut", |
| 106 | "chart-radar", |
| 107 | "chart-radial-bar", |
| 108 | "chart-nightingale", |
| 109 | "chart-radial-column", |
| 110 | "chart-sunburst", |
| 111 | "chart-sankey", |
| 112 | "chart-chord", |
| 113 | "chart-funnel", |
| 114 | "chart-cone-funnel", |
| 115 | "chart-pyramid", |
| 116 | "chart-radial-gauge", |
| 117 | "chart-linear-gauge", |
| 118 | "chart-treemap", |
| 119 | ].includes(elementType); |
| 120 | |
| 121 | // Check if this is a donut chart (for inner labels) |
| 122 | const isDonutChart = elementType === "chart-donut"; |
| 123 | |
| 124 | // Donut inner labels configuration |
| 125 | type InnerLabelConfig = { |
| 126 | text: string; |
| 127 | fontWeight?: "normal" | "bold"; |
| 128 | fontSize?: number; |
| 129 | color?: string; |
| 130 | spacing?: number; |
| 131 | }; |
| 132 | const innerLabels = |
| 133 | (element as { innerLabels?: InnerLabelConfig[] })?.innerLabels ?? []; |
| 134 | const innerCircleFill = |
| 135 | (element as { innerCircle?: { fill?: string } })?.innerCircle?.fill ?? ""; |
| 136 | const innerRadiusRatio = |
| 137 | (element as { innerRadiusRatio?: number })?.innerRadiusRatio ?? 0.7; |
| 138 | |
| 139 | // Get first two inner labels for UI (title and value) |
| 140 | const innerLabelTitle = innerLabels[0]?.text ?? ""; |
| 141 | const innerLabelValue = innerLabels[1]?.text ?? ""; |
| 142 | |
| 143 | // Handlers for nested object updates |
| 144 | const updateTitle = (text: string) => { |
| 145 | handleNodePropertyUpdate("title", { text }); |
| 146 | }; |
| 147 | |
| 148 | const updateSubtitle = (text: string) => { |
| 149 | handleNodePropertyUpdate("subtitle", { text }); |
| 150 | }; |
| 151 | |
| 152 | // Update X-axis settings |
| 153 | const updateXAxis = (updates: Partial<AxisConfig>) => { |
| 154 | const nextTitle = normalizeTitle( |
| 155 | updates.title ?? xAxisConfig.title ?? undefined, |
| 156 | ); |
| 157 | handleNodePropertyUpdate("xAxis", { |
| 158 | ...xAxisConfig, |
| 159 | ...updates, |
| 160 | label: { |
| 161 | ...xAxisConfig.label, |
| 162 | ...updates.label, |
| 163 | }, |
| 164 | gridLine: { |
| 165 | ...xAxisConfig.gridLine, |
| 166 | ...updates.gridLine, |
| 167 | }, |
| 168 | title: |
| 169 | updates.title !== undefined |
| 170 | ? nextTitle |
| 171 | : (xAxisConfig.title ?? nextTitle), |
| 172 | }); |
| 173 | }; |
| 174 | |
| 175 | // Update Y-axis settings |
| 176 | const updateYAxis = (updates: Partial<AxisConfig>) => { |
| 177 | const nextTitle = normalizeTitle( |
| 178 | updates.title ?? yAxisConfig.title ?? undefined, |
| 179 | ); |
| 180 | handleNodePropertyUpdate("yAxis", { |
| 181 | ...yAxisConfig, |
| 182 | ...updates, |
| 183 | label: { |
| 184 | ...yAxisConfig.label, |
| 185 | ...updates.label, |
| 186 | }, |
| 187 | gridLine: { |
| 188 | ...yAxisConfig.gridLine, |
| 189 | ...updates.gridLine, |
| 190 | }, |
| 191 | title: |
| 192 | updates.title !== undefined |
| 193 | ? nextTitle |
| 194 | : (yAxisConfig.title ?? nextTitle), |
| 195 | }); |
| 196 | }; |
| 197 | |
| 198 | const updateAnimation = (enabled: boolean, duration?: number) => { |
| 199 | // Update both disableAnimation (backward compat) and animation object |
| 200 | handleNodePropertyUpdate("disableAnimation", !enabled); |
| 201 | handleNodePropertyUpdate("animation", { |
| 202 | enabled, |
| 203 | duration: duration ?? animationDuration, |
| 204 | }); |
| 205 | }; |
| 206 | |
| 207 | const updateLegend = (enabled: boolean, position?: string) => { |
| 208 | // Update both showLegend (backward compat) and legend object |
| 209 | handleNodePropertyUpdate("showLegend", enabled); |
| 210 | handleNodePropertyUpdate("legend", { |
| 211 | enabled, |
| 212 | position: position ?? legendPosition, |
| 213 | }); |
| 214 | }; |
| 215 | |
| 216 | const updateBackground = (fill: string, visible?: boolean) => { |
| 217 | handleNodePropertyUpdate("background", { |
| 218 | fill, |
| 219 | visible: visible ?? backgroundVisible, |
| 220 | }); |
| 221 | }; |
| 222 | |
| 223 | const updateInnerLabels = (titleText: string, valueText: string) => { |
| 224 | const newLabels: InnerLabelConfig[] = []; |
| 225 | if (titleText) { |
| 226 | newLabels.push({ |
| 227 | text: titleText, |
| 228 | fontWeight: "bold", |
| 229 | }); |
| 230 | } |
| 231 | if (valueText) { |
| 232 | newLabels.push({ |
| 233 | text: valueText, |
| 234 | spacing: 4, |
| 235 | fontSize: 32, |
| 236 | }); |
| 237 | } |
| 238 | handleNodePropertyUpdate("innerLabels", newLabels); |
| 239 | }; |
| 240 | |
| 241 | return ( |
| 242 | <ToolbarGroup> |
| 243 | <Popover> |
| 244 | <PopoverTrigger asChild> |
| 245 | <ToolbarButton tooltip="Chart Settings" size="sm"> |
| 246 | <Settings className="h-4 w-4" /> |
| 247 | </ToolbarButton> |
| 248 | </PopoverTrigger> |
| 249 | <PopoverContent |
| 250 | className="ignore-click-outside/toolbar max-h-[70vh] w-80 overflow-y-auto" |
| 251 | align="start" |
| 252 | > |
| 253 | <div className="grid gap-4"> |
| 254 | <div className="space-y-2"> |
| 255 | <h4 className="leading-none font-medium">Chart Settings</h4> |
| 256 | <p className="text-sm text-muted-foreground"> |
| 257 | Configure chart appearance |
| 258 | </p> |
| 259 | </div> |
| 260 | |
| 261 | {/* Title Section */} |
| 262 | <div className="grid gap-2"> |
| 263 | <Label htmlFor="chart-title">Title</Label> |
| 264 | <DebouncedInput |
| 265 | id="chart-title" |
| 266 | placeholder="Chart title" |
| 267 | value={title} |
| 268 | onChange={(value) => updateTitle(String(value))} |
| 269 | /> |
| 270 | </div> |
| 271 | |
| 272 | <div className="grid gap-2"> |
| 273 | <Label htmlFor="chart-subtitle">Subtitle</Label> |
| 274 | <DebouncedInput |
| 275 | id="chart-subtitle" |
| 276 | placeholder="Chart subtitle" |
| 277 | value={subtitle} |
| 278 | onChange={(value) => updateSubtitle(String(value))} |
| 279 | /> |
| 280 | </div> |
| 281 | |
| 282 | {/* X-Axis Section - Only for applicable charts */} |
| 283 | {supportsAxes && ( |
| 284 | <div className="space-y-3 rounded-lg border p-3"> |
| 285 | <h5 className="text-sm font-medium">X-Axis</h5> |
| 286 | |
| 287 | <div className="flex items-center justify-between"> |
| 288 | <Label htmlFor="x-axis-labels" className="text-xs"> |
| 289 | Show Labels |
| 290 | </Label> |
| 291 | <Switch |
| 292 | id="x-axis-labels" |
| 293 | checked={xAxisLabelEnabled} |
| 294 | onCheckedChange={(checked) => |
| 295 | updateXAxis({ label: { enabled: checked } }) |
| 296 | } |
| 297 | /> |
| 298 | </div> |
| 299 | |
| 300 | <div className="flex items-center justify-between"> |
| 301 | <Label htmlFor="x-axis-grid" className="text-xs"> |
| 302 | Show Grid Lines |
| 303 | </Label> |
| 304 | <Switch |
| 305 | id="x-axis-grid" |
| 306 | checked={xAxisGridEnabled} |
| 307 | onCheckedChange={(checked) => |
| 308 | updateXAxis({ gridLine: { enabled: checked } }) |
| 309 | } |
| 310 | /> |
| 311 | </div> |
| 312 | |
| 313 | <div className="flex items-center justify-between"> |
| 314 | <Label htmlFor="x-axis-title-enabled" className="text-xs"> |
| 315 | Show Title |
| 316 | </Label> |
| 317 | <Switch |
| 318 | id="x-axis-title-enabled" |
| 319 | checked={xAxisTitleEnabled} |
| 320 | onCheckedChange={(checked) => |
| 321 | updateXAxis({ |
| 322 | title: { ...xAxisTitleConfig, enabled: checked }, |
| 323 | }) |
| 324 | } |
| 325 | /> |
| 326 | </div> |
| 327 | |
| 328 | <div className="grid gap-1.5"> |
| 329 | <Label htmlFor="x-axis-title" className="text-xs"> |
| 330 | Title |
| 331 | </Label> |
| 332 | <DebouncedInput |
| 333 | id="x-axis-title" |
| 334 | placeholder="X-axis title" |
| 335 | value={xAxisTitle} |
| 336 | disabled={!xAxisTitleEnabled} |
| 337 | onChange={(value) => |
| 338 | updateXAxis({ |
| 339 | title: { ...xAxisTitleConfig, text: String(value) }, |
| 340 | }) |
| 341 | } |
| 342 | className="h-8 text-sm" |
| 343 | /> |
| 344 | </div> |
| 345 | </div> |
| 346 | )} |
| 347 | |
| 348 | {/* Y-Axis Section - Only for applicable charts */} |
| 349 | {supportsAxes && ( |
| 350 | <div className="space-y-3 rounded-lg border p-3"> |
| 351 | <h5 className="text-sm font-medium">Y-Axis</h5> |
| 352 | |
| 353 | <div className="flex items-center justify-between"> |
| 354 | <Label htmlFor="y-axis-labels" className="text-xs"> |
| 355 | Show Labels |
| 356 | </Label> |
| 357 | <Switch |
| 358 | id="y-axis-labels" |
| 359 | checked={yAxisLabelEnabled} |
| 360 | onCheckedChange={(checked) => |
| 361 | updateYAxis({ label: { enabled: checked } }) |
| 362 | } |
| 363 | /> |
| 364 | </div> |
| 365 | |
| 366 | <div className="flex items-center justify-between"> |
| 367 | <Label htmlFor="y-axis-grid" className="text-xs"> |
| 368 | Show Grid Lines |
| 369 | </Label> |
| 370 | <Switch |
| 371 | id="y-axis-grid" |
| 372 | checked={yAxisGridEnabled} |
| 373 | onCheckedChange={(checked) => |
| 374 | updateYAxis({ gridLine: { enabled: checked } }) |
| 375 | } |
| 376 | /> |
| 377 | </div> |
| 378 | |
| 379 | <div className="flex items-center justify-between"> |
| 380 | <Label htmlFor="y-axis-title-enabled" className="text-xs"> |
| 381 | Show Title |
| 382 | </Label> |
| 383 | <Switch |
| 384 | id="y-axis-title-enabled" |
| 385 | checked={yAxisTitleEnabled} |
| 386 | onCheckedChange={(checked) => |
| 387 | updateYAxis({ |
| 388 | title: { ...yAxisTitleConfig, enabled: checked }, |
| 389 | }) |
| 390 | } |
| 391 | /> |
| 392 | </div> |
| 393 | |
| 394 | <div className="grid gap-1.5"> |
| 395 | <Label htmlFor="y-axis-title" className="text-xs"> |
| 396 | Title |
| 397 | </Label> |
| 398 | <DebouncedInput |
| 399 | id="y-axis-title" |
| 400 | placeholder="Y-axis title" |
| 401 | value={yAxisTitle} |
| 402 | disabled={!yAxisTitleEnabled} |
| 403 | onChange={(value) => |
| 404 | updateYAxis({ |
| 405 | title: { ...yAxisTitleConfig, text: String(value) }, |
| 406 | }) |
| 407 | } |
| 408 | className="h-8 text-sm" |
| 409 | /> |
| 410 | </div> |
| 411 | </div> |
| 412 | )} |
| 413 | |
| 414 | {/* Animation Section - Uses grid-rows for smooth transition */} |
| 415 | <div className="space-y-2"> |
| 416 | <div className="flex items-center justify-between"> |
| 417 | <Label htmlFor="animation-toggle">Animation</Label> |
| 418 | <Switch |
| 419 | id="animation-toggle" |
| 420 | checked={animationEnabled} |
| 421 | onCheckedChange={(checked) => updateAnimation(checked)} |
| 422 | /> |
| 423 | </div> |
| 424 | |
| 425 | <div |
| 426 | className={cn( |
| 427 | "rounded-lg border p-3 transition-opacity", |
| 428 | !animationEnabled && "opacity-50", |
| 429 | )} |
| 430 | aria-disabled={!animationEnabled} |
| 431 | > |
| 432 | <div className="flex items-center justify-between"> |
| 433 | <Label htmlFor="animation-duration">Duration</Label> |
| 434 | <span className="text-sm text-muted-foreground"> |
| 435 | {animationDuration}ms |
| 436 | </span> |
| 437 | </div> |
| 438 | <Slider |
| 439 | id="animation-duration" |
| 440 | min={100} |
| 441 | max={2000} |
| 442 | step={100} |
| 443 | value={[animationDuration]} |
| 444 | disabled={!animationEnabled} |
| 445 | onValueChange={([value]) => |
| 446 | updateAnimation(animationEnabled, value) |
| 447 | } |
| 448 | className="mt-2" |
| 449 | /> |
| 450 | </div> |
| 451 | </div> |
| 452 | |
| 453 | {/* Legend Section */} |
| 454 | <div className="space-y-2"> |
| 455 | <div className="flex items-center justify-between"> |
| 456 | <Label htmlFor="legend-toggle">Legend</Label> |
| 457 | <Switch |
| 458 | id="legend-toggle" |
| 459 | checked={legendEnabled} |
| 460 | onCheckedChange={(checked) => updateLegend(checked)} |
| 461 | /> |
| 462 | </div> |
| 463 | |
| 464 | <div |
| 465 | className={cn( |
| 466 | "rounded-lg border p-3 transition-opacity", |
| 467 | !legendEnabled && "opacity-50", |
| 468 | )} |
| 469 | aria-disabled={!legendEnabled} |
| 470 | > |
| 471 | <Label htmlFor="legend-position">Legend Position</Label> |
| 472 | <Select |
| 473 | value={legendPosition} |
| 474 | disabled={!legendEnabled} |
| 475 | onValueChange={(value) => updateLegend(legendEnabled, value)} |
| 476 | > |
| 477 | <SelectTrigger id="legend-position" className="mt-1.5"> |
| 478 | <SelectValue placeholder="Position" /> |
| 479 | </SelectTrigger> |
| 480 | <SelectContent className="ignore-click-outside/toolbar"> |
| 481 | <SelectItem value="top">Top</SelectItem> |
| 482 | <SelectItem value="right">Right</SelectItem> |
| 483 | <SelectItem value="bottom">Bottom</SelectItem> |
| 484 | <SelectItem value="left">Left</SelectItem> |
| 485 | </SelectContent> |
| 486 | </Select> |
| 487 | </div> |
| 488 | </div> |
| 489 | |
| 490 | {/* Background Section */} |
| 491 | <div className="space-y-2"> |
| 492 | <div className="flex items-center justify-between"> |
| 493 | <Label htmlFor="background-toggle">Background</Label> |
| 494 | <Switch |
| 495 | id="background-toggle" |
| 496 | checked={backgroundVisible} |
| 497 | onCheckedChange={(checked) => |
| 498 | updateBackground(backgroundFill, checked) |
| 499 | } |
| 500 | /> |
| 501 | </div> |
| 502 | |
| 503 | <div |
| 504 | className={cn( |
| 505 | "rounded-lg border p-3 transition-opacity", |
| 506 | !backgroundVisible && "opacity-50", |
| 507 | )} |
| 508 | aria-disabled={!backgroundVisible} |
| 509 | > |
| 510 | <Label htmlFor="background-fill">Background Color</Label> |
| 511 | <div className="mt-1.5 flex gap-2"> |
| 512 | <Input |
| 513 | id="background-fill" |
| 514 | type="color" |
| 515 | value={backgroundFill || "#ffffff"} |
| 516 | disabled={!backgroundVisible} |
| 517 | onChange={(e) => |
| 518 | updateBackground(e.target.value, backgroundVisible) |
| 519 | } |
| 520 | className="h-8 w-12 p-1" |
| 521 | /> |
| 522 | <DebouncedInput |
| 523 | value={backgroundFill} |
| 524 | disabled={!backgroundVisible} |
| 525 | onChange={(value) => |
| 526 | updateBackground(String(value), backgroundVisible) |
| 527 | } |
| 528 | placeholder="#ffffff" |
| 529 | className="flex-1" |
| 530 | /> |
| 531 | </div> |
| 532 | </div> |
| 533 | </div> |
| 534 | |
| 535 | {/* Inner Labels Section - Only for Donut Charts */} |
| 536 | {isDonutChart && ( |
| 537 | <div className="space-y-3 rounded-lg border p-3"> |
| 538 | <h5 className="text-sm font-medium">Inner Labels</h5> |
| 539 | |
| 540 | <div className="grid gap-1.5"> |
| 541 | <Label htmlFor="inner-label-title" className="text-xs"> |
| 542 | Title |
| 543 | </Label> |
| 544 | <DebouncedInput |
| 545 | id="inner-label-title" |
| 546 | placeholder="e.g. Total" |
| 547 | value={innerLabelTitle} |
| 548 | onChange={(value) => |
| 549 | updateInnerLabels(String(value), innerLabelValue) |
| 550 | } |
| 551 | className="h-8 text-sm" |
| 552 | /> |
| 553 | </div> |
| 554 | |
| 555 | <div className="grid gap-1.5"> |
| 556 | <Label htmlFor="inner-label-value" className="text-xs"> |
| 557 | Value |
| 558 | </Label> |
| 559 | <DebouncedInput |
| 560 | id="inner-label-value" |
| 561 | placeholder="e.g. $100,000" |
| 562 | value={innerLabelValue} |
| 563 | onChange={(value) => |
| 564 | updateInnerLabels(innerLabelTitle, String(value)) |
| 565 | } |
| 566 | className="h-8 text-sm" |
| 567 | /> |
| 568 | </div> |
| 569 | |
| 570 | <div className="grid gap-1.5"> |
| 571 | <Label htmlFor="inner-circle-fill" className="text-xs"> |
| 572 | Background Color |
| 573 | </Label> |
| 574 | <div className="flex gap-2"> |
| 575 | <Input |
| 576 | id="inner-circle-fill" |
| 577 | type="color" |
| 578 | value={innerCircleFill || "#f0f0f0"} |
| 579 | onChange={(e) => |
| 580 | handleNodePropertyUpdate("innerCircle", { |
| 581 | fill: e.target.value, |
| 582 | }) |
| 583 | } |
| 584 | className="h-8 w-12 p-1" |
| 585 | /> |
| 586 | <DebouncedInput |
| 587 | value={innerCircleFill} |
| 588 | onChange={(value) => |
| 589 | handleNodePropertyUpdate("innerCircle", { |
| 590 | fill: String(value), |
| 591 | }) |
| 592 | } |
| 593 | placeholder="#f0f0f0" |
| 594 | className="flex-1" |
| 595 | /> |
| 596 | </div> |
| 597 | </div> |
| 598 | |
| 599 | <div className="flex items-center justify-between"> |
| 600 | <Label htmlFor="inner-radius-ratio" className="text-xs"> |
| 601 | Inner Radius: {Math.round(innerRadiusRatio * 100)}% |
| 602 | </Label> |
| 603 | </div> |
| 604 | <Slider |
| 605 | id="inner-radius-ratio" |
| 606 | min={0} |
| 607 | max={100} |
| 608 | step={5} |
| 609 | value={[Math.round(innerRadiusRatio * 100)]} |
| 610 | onValueChange={(values) => { |
| 611 | const value = values[0]; |
| 612 | if (value !== undefined) { |
| 613 | handleNodePropertyUpdate("innerRadiusRatio", value / 100); |
| 614 | } |
| 615 | }} |
| 616 | /> |
| 617 | </div> |
| 618 | )} |
| 619 | </div> |
| 620 | </PopoverContent> |
| 621 | </Popover> |
| 622 | </ToolbarGroup> |
| 623 | ); |
| 624 | } |
| 625 |