@x-plat/design-system 0.5.32 → 0.5.33

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.
@@ -2000,9 +2000,9 @@
2000
2000
  opacity: 1;
2001
2001
  }
2002
2002
  .lib-xplat-chart .chart-crosshair {
2003
- stroke: var(--semantic-border-subtle);
2003
+ stroke: var(--semantic-border-strong);
2004
2004
  stroke-width: 1;
2005
- stroke-dasharray: 4 2;
2005
+ stroke-dasharray: 4 3;
2006
2006
  pointer-events: none;
2007
2007
  }
2008
2008
  .lib-xplat-chart .chart-point-active {
@@ -2018,18 +2018,24 @@
2018
2018
  overflow: visible;
2019
2019
  }
2020
2020
  .lib-xplat-chart .chart-tooltip {
2021
- position: absolute;
2022
- transform: translate(-50%, -100%);
2023
- padding: var(--spacing-space-2) var(--spacing-space-3);
2021
+ padding: var(--spacing-space-3);
2024
2022
  background-color: var(--semantic-surface-neutral-strong);
2025
2023
  color: var(--semantic-text-inverse);
2026
2024
  font-size: 12px;
2025
+ line-height: 18px;
2027
2026
  font-weight: 500;
2028
2027
  border-radius: var(--spacing-radius-md);
2029
- white-space: nowrap;
2028
+ max-width: 240px;
2030
2029
  pointer-events: none;
2031
- z-index: 10;
2032
- animation: chart-tooltip-in 150ms ease-out;
2030
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
2031
+ }
2032
+ .lib-xplat-chart .chart-tooltip.chart-tooltip-show {
2033
+ opacity: 1;
2034
+ animation: chart-tooltip-in 120ms ease-out;
2035
+ }
2036
+ .lib-xplat-chart .chart-tooltip.chart-tooltip-hide {
2037
+ opacity: 0;
2038
+ animation: chart-tooltip-out 80ms ease-in;
2033
2039
  }
2034
2040
  .lib-xplat-chart .chart-bar-animate {
2035
2041
  animation: chart-bar-grow 800ms ease-out both;
@@ -2041,6 +2047,38 @@
2041
2047
  .lib-xplat-chart .chart-area[style*=animationDelay] {
2042
2048
  animation: chart-fade-in 800ms ease-out both;
2043
2049
  }
2050
+ .lib-xplat-chart .chart-legend {
2051
+ display: flex;
2052
+ flex-wrap: wrap;
2053
+ justify-content: center;
2054
+ gap: var(--spacing-space-4);
2055
+ padding: var(--spacing-space-3) 0;
2056
+ }
2057
+ .lib-xplat-chart .chart-legend-item {
2058
+ display: flex;
2059
+ align-items: flex-start;
2060
+ gap: var(--spacing-space-2);
2061
+ }
2062
+ .lib-xplat-chart .chart-legend-dot {
2063
+ flex-shrink: 0;
2064
+ width: 10px;
2065
+ height: 10px;
2066
+ border-radius: 50%;
2067
+ margin-top: 3px;
2068
+ }
2069
+ .lib-xplat-chart .chart-legend-text {
2070
+ display: flex;
2071
+ flex-direction: column;
2072
+ }
2073
+ .lib-xplat-chart .chart-legend-label {
2074
+ font-size: 12px;
2075
+ color: var(--semantic-text-muted);
2076
+ }
2077
+ .lib-xplat-chart .chart-legend-value {
2078
+ font-size: 14px;
2079
+ font-weight: 600;
2080
+ color: var(--semantic-text-strong);
2081
+ }
2044
2082
  @keyframes chart-bar-grow {
2045
2083
  from {
2046
2084
  transform: scaleY(0);
@@ -2060,11 +2098,17 @@
2060
2098
  @keyframes chart-tooltip-in {
2061
2099
  from {
2062
2100
  opacity: 0;
2063
- transform: translate(-50%, -90%);
2064
2101
  }
2065
2102
  to {
2066
2103
  opacity: 1;
2067
- transform: translate(-50%, -100%);
2104
+ }
2105
+ }
2106
+ @keyframes chart-tooltip-out {
2107
+ from {
2108
+ opacity: 1;
2109
+ }
2110
+ to {
2111
+ opacity: 0;
2068
2112
  }
2069
2113
  }
2070
2114
  @media (prefers-reduced-motion: reduce) {
@@ -2261,40 +2261,28 @@ var useChartAnimation = (containerRef, dataKey) => {
2261
2261
  }, [dataKey]);
2262
2262
  return animate || prefersReducedMotion();
2263
2263
  };
2264
+ var TOOLTIP_OFFSET = 12;
2264
2265
  var useChartTooltip = (enabled) => {
2265
2266
  const [tooltip, setTooltip] = React6.useState({
2266
2267
  visible: false,
2267
- x: 0,
2268
- y: 0,
2268
+ clientX: 0,
2269
+ clientY: 0,
2269
2270
  content: ""
2270
2271
  });
2271
2272
  const containerRef = React6.useRef(null);
2272
2273
  const rafRef = React6.useRef(0);
2273
2274
  const move = React6.useCallback((e) => {
2274
2275
  if (!enabled) return;
2275
- const clientX = e.clientX;
2276
- const clientY = e.clientY;
2276
+ const cx = e.clientX;
2277
+ const cy = e.clientY;
2277
2278
  cancelAnimationFrame(rafRef.current);
2278
2279
  rafRef.current = requestAnimationFrame(() => {
2279
- const rect = containerRef.current?.getBoundingClientRect();
2280
- if (!rect) return;
2281
- setTooltip((prev) => ({
2282
- ...prev,
2283
- x: clientX - rect.left,
2284
- y: clientY - rect.top - 12
2285
- }));
2280
+ setTooltip((prev) => ({ ...prev, clientX: cx, clientY: cy }));
2286
2281
  });
2287
2282
  }, [enabled]);
2288
2283
  const show = React6.useCallback((e, content) => {
2289
2284
  if (!enabled) return;
2290
- const rect = containerRef.current?.getBoundingClientRect();
2291
- if (!rect) return;
2292
- setTooltip({
2293
- visible: true,
2294
- x: e.clientX - rect.left,
2295
- y: e.clientY - rect.top - 12,
2296
- content
2297
- });
2285
+ setTooltip({ visible: true, clientX: e.clientX, clientY: e.clientY, content });
2298
2286
  }, [enabled]);
2299
2287
  const hide = React6.useCallback(() => {
2300
2288
  cancelAnimationFrame(rafRef.current);
@@ -2328,14 +2316,14 @@ var AxisLabels = React6.memo(({ labels, count, chartW, height }) => {
2328
2316
  AxisLabels.displayName = "AxisLabels";
2329
2317
  var useCrosshair = (seriesPoints, entries, labels, chartH) => {
2330
2318
  const [activeIndex, setActiveIndex] = React6.useState(null);
2331
- const [mouseX, setMouseX] = React6.useState(null);
2332
2319
  const handleMouseMove = React6.useCallback((e) => {
2333
2320
  const svg = e.currentTarget;
2334
2321
  const rect = svg.getBoundingClientRect();
2335
2322
  const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
2336
- setMouseX(mx);
2337
2323
  if (seriesPoints.length === 0 || seriesPoints[0].length === 0) return;
2338
2324
  const points = seriesPoints[0];
2325
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
2326
+ const threshold = step / 2;
2339
2327
  let closest = 0;
2340
2328
  let minDist = Math.abs(points[0].x - mx);
2341
2329
  for (let i = 1; i < points.length; i++) {
@@ -2345,11 +2333,10 @@ var useCrosshair = (seriesPoints, entries, labels, chartH) => {
2345
2333
  closest = i;
2346
2334
  }
2347
2335
  }
2348
- setActiveIndex(closest);
2336
+ setActiveIndex(minDist <= threshold ? closest : null);
2349
2337
  }, [seriesPoints]);
2350
2338
  const handleMouseLeave = React6.useCallback(() => {
2351
2339
  setActiveIndex(null);
2352
- setMouseX(null);
2353
2340
  }, []);
2354
2341
  const tooltipContent = React6.useMemo(() => {
2355
2342
  if (activeIndex === null) return "";
@@ -2358,7 +2345,13 @@ var useCrosshair = (seriesPoints, entries, labels, chartH) => {
2358
2345
  return p ? `${key}: ${p.v}` : "";
2359
2346
  }).filter(Boolean).join(" / ");
2360
2347
  }, [activeIndex, entries, seriesPoints]);
2361
- return { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent };
2348
+ const getTooltipAt = React6.useCallback((idx) => {
2349
+ return entries.map(([key], di) => {
2350
+ const p = seriesPoints[di]?.[idx];
2351
+ return p ? `${key}: ${p.v}` : "";
2352
+ }).filter(Boolean).join(" / ");
2353
+ }, [entries, seriesPoints]);
2354
+ return { activeIndex, handleMouseMove, handleMouseLeave, tooltipContent, getTooltipAt };
2362
2355
  };
2363
2356
  var LineChart = React6.memo(({ data, labels, width, height, animate, onHover, onMove, onLeave }) => {
2364
2357
  const entries = React6.useMemo(() => Object.entries(data), [data]);
@@ -2381,7 +2374,7 @@ var LineChart = React6.memo(({ data, labels, width, height, animate, onHover, on
2381
2374
  );
2382
2375
  const lineRefs = React6.useRef([]);
2383
2376
  const clipRef = React6.useRef(null);
2384
- const { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent } = useCrosshair(seriesPoints, entries, labels, chartH);
2377
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
2385
2378
  React6.useEffect(() => {
2386
2379
  if (!animate) return;
2387
2380
  lineRefs.current.forEach((el) => {
@@ -2404,8 +2397,7 @@ var LineChart = React6.memo(({ data, labels, width, height, animate, onHover, on
2404
2397
  });
2405
2398
  }
2406
2399
  }, [animate, seriesPoints, width]);
2407
- const guideX = mouseX != null && mouseX >= PADDING.left && mouseX <= width - PADDING.right ? mouseX : null;
2408
- const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x : null;
2400
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
2409
2401
  const lineClipId = "line-area-clip";
2410
2402
  return /* @__PURE__ */ jsxs197(
2411
2403
  "svg",
@@ -2414,7 +2406,26 @@ var LineChart = React6.memo(({ data, labels, width, height, animate, onHover, on
2414
2406
  className: "chart-svg",
2415
2407
  onMouseMove: (e) => {
2416
2408
  handleMouseMove(e);
2417
- onMove(e);
2409
+ const svg = e.currentTarget;
2410
+ const rect = svg.getBoundingClientRect();
2411
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
2412
+ const points = seriesPoints[0];
2413
+ if (!points || points.length === 0) return;
2414
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
2415
+ let closest = 0;
2416
+ let minDist = Math.abs(points[0].x - mx);
2417
+ for (let i = 1; i < points.length; i++) {
2418
+ const dist = Math.abs(points[i].x - mx);
2419
+ if (dist < minDist) {
2420
+ minDist = dist;
2421
+ closest = i;
2422
+ }
2423
+ }
2424
+ if (minDist <= step / 2) {
2425
+ onHover(e, `${labels[closest]} \u2014 ${getTooltipAt(closest)}`);
2426
+ } else {
2427
+ onLeave();
2428
+ }
2418
2429
  },
2419
2430
  onMouseLeave: () => {
2420
2431
  handleMouseLeave();
@@ -2469,21 +2480,16 @@ var LineChart = React6.memo(({ data, labels, width, height, animate, onHover, on
2469
2480
  )
2470
2481
  ] }, di);
2471
2482
  }),
2472
- guideX !== null && /* @__PURE__ */ jsx307(
2483
+ activeX !== null && /* @__PURE__ */ jsx307(
2473
2484
  "line",
2474
2485
  {
2475
- x1: guideX,
2486
+ x1: activeX,
2476
2487
  y1: PADDING.top,
2477
- x2: guideX,
2488
+ x2: activeX,
2478
2489
  y2: PADDING.top + chartH,
2479
2490
  className: "chart-crosshair"
2480
2491
  }
2481
2492
  ),
2482
- activeIndex !== null && activeX !== null && /* @__PURE__ */ jsx307("foreignObject", { x: activeX - 100, y: 0, width: "200", height: PADDING.top, children: /* @__PURE__ */ jsxs197("div", { className: "chart-crosshair-label", children: [
2483
- labels[activeIndex],
2484
- " \u2014 ",
2485
- tooltipContent
2486
- ] }) }),
2487
2493
  /* @__PURE__ */ jsx307(
2488
2494
  "rect",
2489
2495
  {
@@ -2521,7 +2527,7 @@ var CurveChart = React6.memo(({ data, labels, width, height, animate, onHover, o
2521
2527
  );
2522
2528
  const lineRefs = React6.useRef([]);
2523
2529
  const curveClipRef = React6.useRef(null);
2524
- const { activeIndex, mouseX, handleMouseMove, handleMouseLeave, tooltipContent } = useCrosshair(seriesPoints, entries, labels, chartH);
2530
+ const { activeIndex, handleMouseMove, handleMouseLeave, getTooltipAt } = useCrosshair(seriesPoints, entries, labels, chartH);
2525
2531
  React6.useEffect(() => {
2526
2532
  if (!animate) return;
2527
2533
  lineRefs.current.forEach((el) => {
@@ -2544,8 +2550,7 @@ var CurveChart = React6.memo(({ data, labels, width, height, animate, onHover, o
2544
2550
  });
2545
2551
  }
2546
2552
  }, [animate, seriesPoints, width]);
2547
- const guideX = mouseX != null && mouseX >= PADDING.left && mouseX <= width - PADDING.right ? mouseX : null;
2548
- const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x : null;
2553
+ const activeX = activeIndex !== null ? seriesPoints[0]?.[activeIndex]?.x ?? null : null;
2549
2554
  const curveClipId = "curve-area-clip";
2550
2555
  return /* @__PURE__ */ jsxs197(
2551
2556
  "svg",
@@ -2554,7 +2559,26 @@ var CurveChart = React6.memo(({ data, labels, width, height, animate, onHover, o
2554
2559
  className: "chart-svg",
2555
2560
  onMouseMove: (e) => {
2556
2561
  handleMouseMove(e);
2557
- onMove(e);
2562
+ const svg = e.currentTarget;
2563
+ const rect = svg.getBoundingClientRect();
2564
+ const mx = (e.clientX - rect.left) / rect.width * svg.viewBox.baseVal.width;
2565
+ const points = seriesPoints[0];
2566
+ if (!points || points.length === 0) return;
2567
+ const step = points.length > 1 ? Math.abs(points[1].x - points[0].x) : 20;
2568
+ let closest = 0;
2569
+ let minDist = Math.abs(points[0].x - mx);
2570
+ for (let i = 1; i < points.length; i++) {
2571
+ const dist = Math.abs(points[i].x - mx);
2572
+ if (dist < minDist) {
2573
+ minDist = dist;
2574
+ closest = i;
2575
+ }
2576
+ }
2577
+ if (minDist <= step / 2) {
2578
+ onHover(e, `${labels[closest]} \u2014 ${getTooltipAt(closest)}`);
2579
+ } else {
2580
+ onLeave();
2581
+ }
2558
2582
  },
2559
2583
  onMouseLeave: () => {
2560
2584
  handleMouseLeave();
@@ -2609,21 +2633,16 @@ var CurveChart = React6.memo(({ data, labels, width, height, animate, onHover, o
2609
2633
  )
2610
2634
  ] }, di);
2611
2635
  }),
2612
- guideX !== null && /* @__PURE__ */ jsx307(
2636
+ activeX !== null && /* @__PURE__ */ jsx307(
2613
2637
  "line",
2614
2638
  {
2615
- x1: guideX,
2639
+ x1: activeX,
2616
2640
  y1: PADDING.top,
2617
- x2: guideX,
2641
+ x2: activeX,
2618
2642
  y2: PADDING.top + chartH,
2619
2643
  className: "chart-crosshair"
2620
2644
  }
2621
2645
  ),
2622
- activeIndex !== null && activeX !== null && /* @__PURE__ */ jsx307("foreignObject", { x: activeX - 100, y: 0, width: "200", height: PADDING.top, children: /* @__PURE__ */ jsxs197("div", { className: "chart-crosshair-label", children: [
2623
- labels[activeIndex],
2624
- " \u2014 ",
2625
- tooltipContent
2626
- ] }) }),
2627
2646
  /* @__PURE__ */ jsx307(
2628
2647
  "rect",
2629
2648
  {
@@ -2802,30 +2821,70 @@ var PieDonutChart = React6.memo(
2802
2821
  }
2803
2822
  );
2804
2823
  PieDonutChart.displayName = "PieDonutChart";
2805
- var TooltipBubble = ({ x, y, containerWidth, children }) => {
2824
+ var ChartTooltipPortal = ({ clientX, clientY, visible, children }) => {
2806
2825
  const ref = React6.useRef(null);
2807
- const [adjustedX, setAdjustedX] = React6.useState(x);
2808
- React6.useEffect(() => {
2826
+ const [pos, setPos] = React6.useState({ left: 0, top: 0 });
2827
+ React6.useLayoutEffect(() => {
2809
2828
  const el = ref.current;
2810
2829
  if (!el) return;
2811
2830
  const w = el.offsetWidth;
2812
- const half = w / 2;
2813
- const margin = 8;
2814
- let nx = x;
2815
- if (x - half < margin) nx = half + margin;
2816
- else if (x + half > containerWidth - margin) nx = containerWidth - half - margin;
2817
- setAdjustedX(nx);
2818
- }, [x, containerWidth]);
2831
+ const h = el.offsetHeight;
2832
+ const vw = window.innerWidth;
2833
+ let left = clientX + TOOLTIP_OFFSET;
2834
+ let top = clientY - h - TOOLTIP_OFFSET;
2835
+ if (left + w > vw - 8) left = clientX - w - TOOLTIP_OFFSET;
2836
+ if (top < 8) top = clientY + TOOLTIP_OFFSET;
2837
+ if (left < 8) left = 8;
2838
+ setPos({ left, top });
2839
+ }, [clientX, clientY]);
2819
2840
  return /* @__PURE__ */ jsx307(
2820
2841
  "div",
2821
2842
  {
2822
2843
  ref,
2823
- className: "chart-tooltip",
2824
- style: { left: adjustedX, top: y },
2844
+ className: `chart-tooltip ${visible ? "chart-tooltip-show" : "chart-tooltip-hide"}`,
2845
+ style: { position: "fixed", left: pos.left, top: pos.top, zIndex: 1100 },
2825
2846
  children
2826
2847
  }
2827
2848
  );
2828
2849
  };
2850
+ var ChartLegend = ({ data, labels, type }) => {
2851
+ const entries = Object.entries(data);
2852
+ if (type === "pie" || type === "doughnut") {
2853
+ const values = entries.flatMap(([, v]) => v);
2854
+ const total = values.reduce((a, b) => a + b, 0) || 1;
2855
+ const firstKey = entries[0]?.[0] ?? "";
2856
+ const colorOffset = hashString(firstKey);
2857
+ return /* @__PURE__ */ jsx307("div", { className: "chart-legend", children: values.map((v, i) => {
2858
+ const pct = Math.round(v / total * 100);
2859
+ const color = PIE_COLORS[(i + colorOffset) % PIE_COLORS.length];
2860
+ return /* @__PURE__ */ jsxs197("div", { className: "chart-legend-item", children: [
2861
+ /* @__PURE__ */ jsx307("span", { className: "chart-legend-dot", style: { backgroundColor: color } }),
2862
+ /* @__PURE__ */ jsxs197("div", { className: "chart-legend-text", children: [
2863
+ /* @__PURE__ */ jsx307("span", { className: "chart-legend-label", children: labels[i] || `${i + 1}` }),
2864
+ /* @__PURE__ */ jsxs197("span", { className: "chart-legend-value", children: [
2865
+ v.toLocaleString(),
2866
+ "(",
2867
+ pct,
2868
+ "%)"
2869
+ ] })
2870
+ ] })
2871
+ ] }, i);
2872
+ }) });
2873
+ }
2874
+ return /* @__PURE__ */ jsx307("div", { className: "chart-legend", children: entries.map(([key], di) => {
2875
+ const palette = getPalette(LINE_BAR_PALETTES, di, key);
2876
+ const color = palette[2];
2877
+ const values = entries[di][1];
2878
+ const sum = values.reduce((a, b) => a + b, 0);
2879
+ return /* @__PURE__ */ jsxs197("div", { className: "chart-legend-item", children: [
2880
+ /* @__PURE__ */ jsx307("span", { className: "chart-legend-dot", style: { backgroundColor: color } }),
2881
+ /* @__PURE__ */ jsxs197("div", { className: "chart-legend-text", children: [
2882
+ /* @__PURE__ */ jsx307("span", { className: "chart-legend-label", children: key }),
2883
+ /* @__PURE__ */ jsx307("span", { className: "chart-legend-value", children: sum.toLocaleString() })
2884
+ ] })
2885
+ ] }, di);
2886
+ }) });
2887
+ };
2829
2888
  var Chart = React6.memo((props) => {
2830
2889
  const { type, data, labels, tooltip: showTooltip = true } = props;
2831
2890
  const { tooltip, show, hide, move, containerRef } = useChartTooltip(showTooltip);
@@ -2841,7 +2900,8 @@ var Chart = React6.memo((props) => {
2841
2900
  ready && type === "bar" && /* @__PURE__ */ jsx307(BarChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
2842
2901
  ready && type === "pie" && /* @__PURE__ */ jsx307(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, onHover: show, onMove: move, onLeave: hide }),
2843
2902
  ready && type === "doughnut" && /* @__PURE__ */ jsx307(PieDonutChart, { data: stableData, labels: stableLabels, width, height, animate, isDoughnut: true, onHover: show, onMove: move, onLeave: hide }),
2844
- tooltip.visible && /* @__PURE__ */ jsx307(TooltipBubble, { x: tooltip.x, y: tooltip.y, containerWidth: width, children: tooltip.content })
2903
+ ready && (type === "bar" || type === "pie" || type === "doughnut") && /* @__PURE__ */ jsx307(ChartLegend, { data: stableData, labels: stableLabels, type }),
2904
+ tooltip.content && /* @__PURE__ */ jsx307(ChartTooltipPortal, { clientX: tooltip.clientX, clientY: tooltip.clientY, visible: tooltip.visible, children: tooltip.content })
2845
2905
  ] });
2846
2906
  });
2847
2907
  Chart.displayName = "Chart";