@siyahi/ui 0.1.9-next.17 → 0.1.9-next.19

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.
Files changed (93) hide show
  1. package/README.md +77 -207
  2. package/agents/examples/accordion.tsx +39 -0
  3. package/agents/examples/alert.tsx +25 -0
  4. package/agents/examples/app-sidebar.tsx +38 -0
  5. package/agents/examples/avatar.tsx +14 -0
  6. package/agents/examples/badge.tsx +18 -0
  7. package/agents/examples/bar-list-labels.tsx +22 -0
  8. package/agents/examples/bar-list-sides.tsx +30 -0
  9. package/agents/examples/bar-list.tsx +24 -0
  10. package/agents/examples/breadcrumb.tsx +28 -0
  11. package/agents/examples/button.tsx +21 -0
  12. package/agents/examples/calendar.tsx +15 -0
  13. package/agents/examples/card.tsx +34 -0
  14. package/agents/examples/chart-area.tsx +38 -0
  15. package/agents/examples/chart-bar-flows.tsx +31 -0
  16. package/agents/examples/chart-bar-hourly.tsx +28 -0
  17. package/agents/examples/chart-bar-losses.tsx +29 -0
  18. package/agents/examples/chart-bar-sectors.tsx +70 -0
  19. package/agents/examples/chart-bar-years.tsx +40 -0
  20. package/agents/examples/chart-bar.tsx +56 -0
  21. package/agents/examples/chart-category-ticks.tsx +50 -0
  22. package/agents/examples/chart-line-events.tsx +47 -0
  23. package/agents/examples/chart-line-timeline.tsx +41 -0
  24. package/agents/examples/chart-line.tsx +57 -0
  25. package/agents/examples/code-block.tsx +38 -0
  26. package/agents/examples/collapsible.tsx +16 -0
  27. package/agents/examples/command.tsx +27 -0
  28. package/agents/examples/context-menu.tsx +15 -0
  29. package/agents/examples/copy-button.tsx +8 -0
  30. package/agents/examples/data-bar.tsx +121 -0
  31. package/agents/examples/data-table-filters.tsx +103 -0
  32. package/agents/examples/data-table.tsx +98 -0
  33. package/agents/examples/delta.tsx +13 -0
  34. package/agents/examples/dialog.tsx +28 -0
  35. package/agents/examples/distribution-bar-budget.tsx +30 -0
  36. package/agents/examples/distribution-bar-tug.tsx +72 -0
  37. package/agents/examples/dropdown-menu.tsx +31 -0
  38. package/agents/examples/formatters.tsx +42 -0
  39. package/agents/examples/heatmap-calendar.tsx +50 -0
  40. package/agents/examples/heatmap-status.tsx +51 -0
  41. package/agents/examples/heatmap-weekday.tsx +53 -0
  42. package/agents/examples/input.tsx +17 -0
  43. package/agents/examples/local-time.tsx +14 -0
  44. package/agents/examples/metric-strip.tsx +40 -0
  45. package/agents/examples/page-section.tsx +52 -0
  46. package/agents/examples/popover.tsx +23 -0
  47. package/agents/examples/price-chart.tsx +32 -0
  48. package/agents/examples/progress-tone.tsx +28 -0
  49. package/agents/examples/progress.tsx +10 -0
  50. package/agents/examples/scroll-area.tsx +20 -0
  51. package/agents/examples/section-cards-select.tsx +27 -0
  52. package/agents/examples/section-cards-trend.tsx +30 -0
  53. package/agents/examples/section-cards.tsx +48 -0
  54. package/agents/examples/select.tsx +27 -0
  55. package/agents/examples/separator.tsx +13 -0
  56. package/agents/examples/sheet.tsx +15 -0
  57. package/agents/examples/skeleton.tsx +11 -0
  58. package/agents/examples/slider.tsx +5 -0
  59. package/agents/examples/surfaces.tsx +115 -0
  60. package/agents/examples/table.tsx +50 -0
  61. package/agents/examples/tabs.tsx +16 -0
  62. package/agents/examples/toggle-group.tsx +12 -0
  63. package/agents/examples/toggle.tsx +11 -0
  64. package/agents/examples/tooltip.tsx +13 -0
  65. package/agents/examples/treemap.tsx +87 -0
  66. package/agents/manifest.json +931 -0
  67. package/agents/report-rules.json +7 -0
  68. package/dist/components/registry/charts/bar-chart-card.js +1 -0
  69. package/dist/components/registry/dashboardcn/bar-list.js +8 -7
  70. package/dist/components/registry/heatmap/calendar-heatmap.js +8 -1
  71. package/dist/components/registry/heatmap/status-heatmap.js +8 -1
  72. package/dist/components/registry/heatmap/weekday-heatmap.d.ts +6 -2
  73. package/dist/components/registry/heatmap/weekday-heatmap.js +56 -37
  74. package/dist/components/registry/niko-table/core/data-table-context.d.ts +2 -0
  75. package/dist/components/registry/niko-table/core/data-table-context.js +21 -0
  76. package/dist/components/registry/niko-table/core/data-table-grouped-row.js +3 -1
  77. package/dist/components/registry/niko-table/core/data-table-structure.js +15 -12
  78. package/dist/components/registry/niko-table/core/data-table.d.ts +2 -2
  79. package/dist/components/registry/niko-table/core/data-table.js +5 -1
  80. package/dist/components/registry/niko-table/filters/table-search-filter.js +1 -1
  81. package/dist/components/registry/niko-table/lib/flex-columns.d.ts +1 -1
  82. package/dist/components/registry/niko-table/lib/flex-columns.js +3 -3
  83. package/dist/components/registry/niko-table/lib/styles.d.ts +1 -1
  84. package/dist/components/registry/niko-table/lib/styles.js +2 -2
  85. package/dist/components/registry/sidebar-07/nav-main.js +7 -1
  86. package/dist/components/siyahi/treemap.js +46 -4
  87. package/dist/lib/chart-frame.js +180 -124
  88. package/dist/lib/chart-series.d.ts +1 -1
  89. package/dist/lib/format.js +32 -13
  90. package/dist/lib/heatmap-navigation.d.ts +8 -0
  91. package/dist/lib/heatmap-navigation.js +49 -0
  92. package/package.json +3 -1
  93. package/styles/siyahi.css +24 -0
@@ -24,19 +24,19 @@
24
24
  * Returns the set of column ids to render widthless (empty when fill is off or
25
25
  * no eligible column exists — the table then sizes to its columns as before).
26
26
  */
27
- function resolveFlexColumnIds(table) {
27
+ function resolveFlexColumnIds(table, pinningEnabled = true) {
28
28
  const ids = /* @__PURE__ */ new Set();
29
29
  if (!table.options.enableColumnResizing) return ids;
30
30
  if (table.options.meta?.disableFlexFill) return ids;
31
31
  const leafColumns = table.getVisibleLeafColumns();
32
32
  const columnSizing = table.state.columnSizing;
33
33
  let hasExplicit = false;
34
- for (const column of leafColumns) if (column.columnDef.meta?.flex === true && !column.getIsPinned() && columnSizing[column.id] == null) {
34
+ for (const column of leafColumns) if (column.columnDef.meta?.flex === true && !(pinningEnabled && column.getIsPinned()) && columnSizing[column.id] == null) {
35
35
  ids.add(column.id);
36
36
  hasExplicit = true;
37
37
  }
38
38
  if (hasExplicit) return ids;
39
- const auto = leafColumns.find((column) => column.getCanResize() && !column.getIsPinned() && column.columnDef.meta?.flex !== false && columnSizing[column.id] == null);
39
+ const auto = leafColumns.find((column) => column.getCanResize() && !(pinningEnabled && column.getIsPinned()) && column.columnDef.meta?.flex !== false && columnSizing[column.id] == null);
40
40
  if (auto) ids.add(auto.id);
41
41
  return ids;
42
42
  }
@@ -2,5 +2,5 @@ import { DataTableFeatures } from "./data-table-features.js";
2
2
  import React from "react";
3
3
  import { Column, RowData } from "@tanstack/react-table";
4
4
  //#region src/components/registry/niko-table/lib/styles.d.ts
5
- export declare const getCommonPinningStyles: <TData extends RowData>(column: Column<DataTableFeatures, TData>, isHeader?: boolean) => React.CSSProperties;
5
+ export declare const getCommonPinningStyles: <TData extends RowData>(column: Column<DataTableFeatures, TData>, isHeader?: boolean, pinningEnabled?: boolean) => React.CSSProperties;
6
6
  //#endregion
@@ -1,8 +1,8 @@
1
1
  import "@tanstack/react-table";
2
2
  //#region src/components/registry/niko-table/lib/styles.ts
3
- const getCommonPinningStyles = (column, isHeader = false) => {
3
+ const getCommonPinningStyles = (column, isHeader = false, pinningEnabled = true) => {
4
4
  const isPinned = column.getIsPinned();
5
- if (!isPinned) return {};
5
+ if (!isPinned || !pinningEnabled) return {};
6
6
  const isLeft = isPinned === "start";
7
7
  const columnSize = column.getSize();
8
8
  return {
@@ -2,9 +2,11 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../ui/co
2
2
  import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, useSidebar } from "../../ui/sidebar.js";
3
3
  import { ChevronRightIcon } from "lucide-react";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { useState } from "react";
5
6
  //#region src/components/registry/sidebar-07/nav-main.tsx
6
7
  function NavMain({ items, label = "Navigation" }) {
7
8
  const { isMobile, setOpenMobile } = useSidebar();
9
+ const [expanded, setExpanded] = useState({});
8
10
  const closeDrawer = () => {
9
11
  if (isMobile) setOpenMobile(false);
10
12
  };
@@ -19,7 +21,11 @@ function NavMain({ items, label = "Navigation" }) {
19
21
  onClick: closeDrawer,
20
22
  children: [item.icon, /* @__PURE__ */ jsx("span", { children: item.title })]
21
23
  }) }, item.title) : /* @__PURE__ */ jsxs(Collapsible, {
22
- defaultOpen: item.isActive,
24
+ open: expanded[item.title] ?? !!item.isActive,
25
+ onOpenChange: (open) => setExpanded((previous) => ({
26
+ ...previous,
27
+ [item.title]: open
28
+ })),
23
29
  className: "group/collapsible",
24
30
  render: /* @__PURE__ */ jsx(SidebarMenuItem, {}),
25
31
  children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, {
@@ -2,12 +2,13 @@ import { useFormat } from "../../lib/format.js";
2
2
  import { heat, heatFill, heatRange, layout } from "../../lib/treemap.js";
3
3
  import { cn } from "cn";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
- import { useEffect, useEffectEvent, useLayoutEffect, useRef, useState } from "react";
5
+ import { useEffect, useEffectEvent, useLayoutEffect, useMemo, useRef, useState } from "react";
6
6
  //#region src/components/siyahi/treemap.tsx
7
7
  /** Holdings heat map: area by weight, colour by signed change, optionally grouped; hand-drawn SVG. */
8
8
  function Treemap({ items, height, range, onHover, selected, onSelect }) {
9
9
  const ref = useRef(null);
10
10
  const [width, setWidth] = useState(0);
11
+ const [focusedKey, setFocusedKey] = useState(null);
11
12
  const selectedKey = selected ? selected.key ?? selected.name : null;
12
13
  const { pct } = useFormat();
13
14
  useLayoutEffect(() => {
@@ -25,7 +26,12 @@ function Treemap({ items, height, range, onHover, selected, onSelect }) {
25
26
  document.addEventListener("pointerdown", onPointerDown);
26
27
  return () => document.removeEventListener("pointerdown", onPointerDown);
27
28
  }, [selectedKey]);
28
- const { groups, tiles } = layout(items, width, height);
29
+ const { groups, tiles } = useMemo(() => layout(items, width, height), [
30
+ items,
31
+ width,
32
+ height
33
+ ]);
34
+ const focusKey = tiles.some(({ item }) => (item.key ?? item.name) === focusedKey) ? focusedKey : tiles[0]?.item.key ?? tiles[0]?.item.name;
29
35
  const change = (value) => value === null ? "—" : `${value > 0 ? "▲ " : value < 0 ? "▼ " : ""}${pct(Math.abs(value))}`;
30
36
  return /* @__PURE__ */ jsx("div", {
31
37
  ref,
@@ -34,7 +40,7 @@ function Treemap({ items, height, range, onHover, selected, onSelect }) {
34
40
  children: /* @__PURE__ */ jsxs("svg", {
35
41
  width,
36
42
  height,
37
- role: "img",
43
+ role: onSelect ? "group" : "img",
38
44
  "aria-label": "Treemap of holdings by weight and change",
39
45
  className: "text-xs",
40
46
  onPointerLeave: () => onHover?.(null),
@@ -53,12 +59,48 @@ function Treemap({ items, height, range, onHover, selected, onSelect }) {
53
59
  className: "fill-muted-foreground font-medium",
54
60
  children: group
55
61
  })]
56
- }, group)), tiles.map(({ item, ...box }) => {
62
+ }, group)), tiles.map(({ item, ...box }, index) => {
57
63
  const key = item.key ?? item.name;
58
64
  return /* @__PURE__ */ jsxs("svg", {
59
65
  ...box,
60
66
  "data-slot": "treemap-tile",
61
67
  "data-selected": key === selectedKey || void 0,
68
+ role: onSelect ? "button" : void 0,
69
+ tabIndex: onSelect ? key === focusKey ? 0 : -1 : void 0,
70
+ "aria-label": `${item.name}${item.group ? `, ${item.group}` : ""}, ${change(item.change)}`,
71
+ "aria-pressed": onSelect ? key === selectedKey : void 0,
72
+ onFocus: () => setFocusedKey(key),
73
+ onKeyDown: (event) => {
74
+ if (!onSelect) return;
75
+ if (event.key === "Enter" || event.key === " ") {
76
+ event.preventDefault();
77
+ onSelect(key === selectedKey ? null : item);
78
+ } else if (event.key === "Escape") onSelect(null);
79
+ else {
80
+ let next;
81
+ switch (event.key) {
82
+ case "Home":
83
+ next = 0;
84
+ break;
85
+ case "End":
86
+ next = tiles.length - 1;
87
+ break;
88
+ case "ArrowRight":
89
+ case "ArrowDown":
90
+ next = (index + 1) % tiles.length;
91
+ break;
92
+ case "ArrowLeft":
93
+ case "ArrowUp":
94
+ next = (index + tiles.length - 1) % tiles.length;
95
+ break;
96
+ default: return;
97
+ }
98
+ event.preventDefault();
99
+ const target = tiles[next]?.item;
100
+ if (target) setFocusedKey(target.key ?? target.name);
101
+ ref.current?.querySelectorAll("[data-slot=\"treemap-tile\"]")[next]?.focus();
102
+ }
103
+ },
62
104
  onPointerEnter: () => onHover?.(item),
63
105
  onClick: () => onSelect?.(key === selectedKey ? null : item),
64
106
  children: [
@@ -3,7 +3,8 @@ import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartToo
3
3
  import { cn } from "./utils.js";
4
4
  import { useLocale } from "../components/siyahi/siyahi-provider.js";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
- import { CartesianGrid, ReferenceLine, XAxis, YAxis } from "recharts";
6
+ import { useMemo } from "react";
7
+ import { CartesianGrid, ReferenceLine, Text, XAxis, YAxis, useChartWidth } from "recharts";
7
8
  //#region src/lib/chart-frame.tsx
8
9
  const AXIS_TITLE = "fill-muted-foreground text-xs";
9
10
  const TICK = { className: "fill-muted-foreground font-mono" };
@@ -47,26 +48,51 @@ function endRoom(xFormatter, xTicks) {
47
48
  const last = xTicks.at(-1);
48
49
  return Math.max(24, Math.ceil((last === void 0 ? 0 : xFormatter(last).length * MONO_WIDTH) / 2) + 4);
49
50
  }
50
- /** Squeezes a line into its column with SVG's own textLength when it would run into the next one. */
51
- const fitTo = (text, room, charWidth) => room > 0 && text.length * charWidth > room ? {
52
- textLength: room,
53
- lengthAdjust: "spacingAndGlyphs"
54
- } : {};
55
51
  /**
56
- * An x tick fitted to its column, drawn whenever a page names its ticks or gives each a second line: Recharts'
52
+ * An x tick with a second line or explicit position: Recharts'
57
53
  * own ticks would rather drop a label than let it run into the next. With a detail, the second line comes from the
58
54
  * tick's row; under a column the lines hang below the axis, and beside a horizontal bar they straddle its middle.
59
55
  * A two-line tick is set in Geist, not Mono: a name with a figure under it reads as a label, as in BarList, and
60
56
  * sector names in Mono would take most of a phone's width.
61
57
  */
62
- function fittedTick(data, xKey, xFormatter, detailOf, beside) {
63
- const charWidth = detailOf ? GEIST_WIDTH : MONO_WIDTH;
64
- return ({ x, y, width, visibleTicksCount, payload }) => {
58
+ function fittedTick(data, xKey, xFormatter, detailOf, beside, labelWidth) {
59
+ const rows = /* @__PURE__ */ new Map();
60
+ for (const row of data) if (!rows.has(row[xKey])) rows.set(row[xKey], row);
61
+ return ({ x, y, payload }) => {
65
62
  const value = payload?.value;
66
- const row = data.find((r) => r[xKey] === value);
63
+ const row = rows.get(value);
67
64
  const name = xFormatter(value);
68
65
  const detail = row && detailOf ? detailOf(row) : null;
69
- const room = beside || !visibleTicksCount ? 0 : Number(width) / visibleTicksCount - 6;
66
+ if (beside) return /* @__PURE__ */ jsxs("g", {
67
+ className: "fill-muted-foreground",
68
+ children: [/* @__PURE__ */ jsx(Text, {
69
+ x,
70
+ y: Number(y) - (detail ? 4 : 0),
71
+ width: labelWidth,
72
+ maxLines: 2,
73
+ textAnchor: "end",
74
+ verticalAnchor: detail ? "end" : "middle",
75
+ lineHeight: 14,
76
+ style: { fontSize: "var(--text-xs)" },
77
+ "aria-label": name,
78
+ className: "recharts-cartesian-axis-tick-value fill-muted-foreground",
79
+ children: name
80
+ }), detail !== null && /* @__PURE__ */ jsx(Text, {
81
+ x,
82
+ y: Number(y) + 4,
83
+ width: labelWidth,
84
+ textAnchor: "end",
85
+ verticalAnchor: "start",
86
+ lineHeight: 14,
87
+ style: {
88
+ fontSize: "var(--text-xs)",
89
+ fontVariantNumeric: "tabular-nums"
90
+ },
91
+ "aria-label": detail,
92
+ className: "fill-muted-foreground tabular-nums",
93
+ children: detail
94
+ })]
95
+ });
70
96
  return /* @__PURE__ */ jsxs("text", {
71
97
  x,
72
98
  y,
@@ -74,19 +100,32 @@ function fittedTick(data, xKey, xFormatter, detailOf, beside) {
74
100
  className: cn("recharts-cartesian-axis-tick-value fill-muted-foreground", !detailOf && "font-mono"),
75
101
  children: [/* @__PURE__ */ jsx("tspan", {
76
102
  x,
77
- dy: beside ? -4 : 12,
78
- ...fitTo(name, room, charWidth),
103
+ dy: 12,
79
104
  children: name
80
105
  }), detail !== null && /* @__PURE__ */ jsx("tspan", {
81
106
  x,
82
107
  dy: 15,
83
108
  className: "tabular-nums",
84
- ...fitTo(detail, room, charWidth),
85
109
  children: detail
86
110
  })]
87
111
  });
88
112
  };
89
113
  }
114
+ function HorizontalAxis({ data, xKey, xFormatter, xTickDetail, xTicks }) {
115
+ const width = Math.min(240, Math.floor((useChartWidth() ?? 320) * .45));
116
+ return /* @__PURE__ */ jsx(YAxis, {
117
+ dataKey: xKey,
118
+ type: "category",
119
+ width,
120
+ tickLine: false,
121
+ axisLine: false,
122
+ tickMargin: 8,
123
+ tickFormatter: xFormatter,
124
+ interval: 0,
125
+ ticks: xTicks,
126
+ tick: fittedTick(data, xKey, xFormatter, xTickDetail, true, width - 16)
127
+ });
128
+ }
90
129
  /**
91
130
  * The card, axes, grid, tooltip and legend the three chart cards share. Both
92
131
  * axes keep Recharts' default ids: marks find their axes by id, and a page
@@ -95,127 +134,144 @@ function fittedTick(data, xKey, xFormatter, detailOf, beside) {
95
134
  */
96
135
  function ChartFrame({ chart: Chart, children, horizontal = false, stackOffset, onClick, title, description, action, data, xKey, series, xFormatter = String, xTooltipFormatter = xFormatter, valueFormatter, yTickFormatter, yDomain, yTicks, height, xTickDetail, xLabel, yLabel, referenceLines = NO_LINES, placeholder, footer, xDomain, xTicks }) {
97
136
  const { numberLocale } = useLocale();
98
- const formatValue = valueFormatter ?? new Intl.NumberFormat(numberLocale).format;
137
+ const formatValue = useMemo(() => valueFormatter ?? new Intl.NumberFormat(numberLocale).format, [valueFormatter, numberLocale]);
99
138
  const config = Object.fromEntries(series.map(({ key, label, color }) => [key, {
100
139
  label,
101
140
  color
102
141
  }]));
103
142
  const crossesZero = data.some((row) => series.some(({ key }) => Number(row[key]) < 0));
143
+ const detailedRows = horizontal && xTickDetail !== void 0;
144
+ const rowCount = (xTicks ?? data).length;
145
+ const plotClass = detailedRows ? "min-h-(--wrapped-chart-height) @min-[24rem]/chart:min-h-0" : void 0;
146
+ let minWidth;
147
+ if (!horizontal && (xTicks || xTickDetail)) {
148
+ const labelledValues = xTicks ?? data.map((row) => row[xKey]);
149
+ let tickWidth = 0;
150
+ for (const value of labelledValues) tickWidth = Math.max(tickWidth, xFormatter(value).length * MONO_WIDTH);
151
+ if (xTickDetail) for (const row of data) tickWidth = Math.max(tickWidth, xTickDetail(row).length * GEIST_WIDTH);
152
+ minWidth = Math.ceil((tickWidth + 12) * labelledValues.length + 64);
153
+ }
154
+ const plotStyle = {
155
+ height: detailedRows ? Math.max(height, rowCount * 72 + 48) : height,
156
+ minWidth,
157
+ ...detailedRows ? { "--wrapped-chart-height": `${rowCount * 104 + 48}px` } : {}
158
+ };
104
159
  return /* @__PURE__ */ jsxs(Card, { children: [
105
160
  /* @__PURE__ */ jsxs(CardHeader, { children: [
106
161
  /* @__PURE__ */ jsx(CardTitle, { children: title }),
107
162
  description && /* @__PURE__ */ jsx(CardDescription, { children: description }),
108
163
  action && /* @__PURE__ */ jsx(CardAction, { children: action })
109
164
  ] }),
110
- /* @__PURE__ */ jsx(CardContent, { children: placeholder ? /* @__PURE__ */ jsx("div", {
111
- "data-slot": "chart-placeholder",
112
- className: "flex items-center justify-center",
113
- style: { height },
114
- children: placeholder
115
- }) : /* @__PURE__ */ jsx(ChartContainer, {
116
- config,
117
- className: cn("aspect-auto w-full", onClick && "cursor-pointer"),
118
- style: { height },
119
- children: /* @__PURE__ */ jsxs(Chart, {
120
- data,
121
- layout: horizontal ? "vertical" : "horizontal",
122
- stackOffset,
123
- onClick,
124
- throttledEvents: onClick ? [] : void 0,
125
- margin: {
126
- left: 0,
127
- right: !horizontal && xTicks ? endRoom(xFormatter, xTicks) : 8
128
- },
129
- children: [
130
- /* @__PURE__ */ jsx(CartesianGrid, {
131
- vertical: horizontal,
132
- horizontal: !horizontal,
133
- strokeDasharray: "3 3"
134
- }),
135
- horizontal ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(XAxis, {
136
- type: "number",
137
- hide: true,
138
- domain: yDomain,
139
- ticks: yTicks
140
- }), /* @__PURE__ */ jsx(YAxis, {
141
- dataKey: xKey,
142
- type: "category",
143
- width: "auto",
144
- tickLine: false,
145
- axisLine: false,
146
- tickMargin: 8,
147
- tickFormatter: (value) => xFormatter(value),
148
- tick: xTickDetail || xTicks ? fittedTick(data, xKey, xFormatter, xTickDetail, horizontal) : TICK,
149
- ticks: xTicks,
150
- interval: 0
151
- })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(XAxis, {
152
- dataKey: xKey,
153
- type: xDomain ? "number" : "category",
154
- domain: xDomain,
155
- ticks: xTicks,
156
- height: (xTickDetail ? 40 : 30) + (xLabel ? 18 : 0),
157
- label: xLabel ? {
158
- value: xLabel,
159
- position: "insideBottom",
160
- className: AXIS_TITLE
161
- } : void 0,
162
- tickLine: false,
163
- axisLine: false,
164
- tickMargin: 8,
165
- tickFormatter: (value) => xFormatter(value),
166
- tick: xTickDetail || xTicks ? fittedTick(data, xKey, xFormatter, xTickDetail, false) : TICK,
167
- interval: xTickDetail || xTicks ? 0 : void 0
168
- }), /* @__PURE__ */ jsx(YAxis, {
169
- tickLine: false,
170
- axisLine: false,
171
- width: "auto",
172
- domain: yDomain,
173
- ticks: yTicks,
174
- label: yLabel ? {
175
- value: yLabel,
176
- angle: -90,
177
- position: "insideLeft",
178
- style: { textAnchor: "middle" },
179
- className: AXIS_TITLE
180
- } : void 0,
181
- tickFormatter: (value) => (yTickFormatter ?? formatValue)(value),
182
- tick: TICK
183
- })] }),
184
- crossesZero && (horizontal ? /* @__PURE__ */ jsx(ReferenceLine, {
185
- x: 0,
186
- stroke: "var(--border)"
187
- }) : /* @__PURE__ */ jsx(ReferenceLine, {
188
- y: 0,
189
- stroke: "var(--border)"
190
- })),
191
- referenceLines.map(({ label, emphasis = "muted", labelPosition = "insideTopLeft", ...at }) => /* @__PURE__ */ jsx(ReferenceLine, {
192
- ...horizontal ? {
193
- x: at.y,
194
- y: at.x
195
- } : at,
196
- stroke: emphasis === "strong" ? "var(--foreground)" : "var(--muted-foreground)",
197
- strokeDasharray: at.y === void 0 ? void 0 : "4 4",
198
- label: label === void 0 ? void 0 : {
199
- value: label,
200
- position: labelPosition,
201
- className: "fill-muted-foreground text-xs"
202
- }
203
- }, `${at.x ?? ""}:${at.y ?? ""}:${label ?? ""}`)),
204
- /* @__PURE__ */ jsx(ChartTooltip, { content: /* @__PURE__ */ jsx(ChartTooltipContent, {
205
- labelFormatter: (_, payload) => {
206
- const row = payload[0]?.payload;
207
- return row ? xTooltipFormatter(row[xKey]) : null;
208
- },
209
- formatter: seriesRow(series, formatValue)
210
- }) }),
211
- children,
212
- /* @__PURE__ */ jsx(ChartLegend, {
213
- content: /* @__PURE__ */ jsx(ChartLegendContent, {}),
214
- itemSorter: null
215
- })
216
- ]
165
+ /* @__PURE__ */ jsx(CardContent, {
166
+ className: "@container/chart overflow-x-auto",
167
+ role: minWidth ? "region" : void 0,
168
+ "aria-label": minWidth ? "Chart, scroll horizontally for all labelled values" : void 0,
169
+ tabIndex: minWidth ? 0 : void 0,
170
+ children: placeholder ? /* @__PURE__ */ jsx("div", {
171
+ "data-slot": "chart-placeholder",
172
+ className: cn("flex items-center justify-center", plotClass),
173
+ style: plotStyle,
174
+ children: placeholder
175
+ }) : /* @__PURE__ */ jsx(ChartContainer, {
176
+ config,
177
+ className: cn("aspect-auto w-full", plotClass, onClick && "cursor-pointer"),
178
+ style: plotStyle,
179
+ children: /* @__PURE__ */ jsxs(Chart, {
180
+ data,
181
+ layout: horizontal ? "vertical" : "horizontal",
182
+ stackOffset,
183
+ onClick,
184
+ throttledEvents: onClick ? [] : void 0,
185
+ margin: {
186
+ left: 0,
187
+ right: !horizontal && xTicks ? endRoom(xFormatter, xTicks) : 8
188
+ },
189
+ children: [
190
+ /* @__PURE__ */ jsx(CartesianGrid, {
191
+ vertical: horizontal,
192
+ horizontal: !horizontal,
193
+ strokeDasharray: "3 3"
194
+ }),
195
+ horizontal ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(XAxis, {
196
+ type: "number",
197
+ hide: true,
198
+ domain: yDomain,
199
+ ticks: yTicks
200
+ }), /* @__PURE__ */ jsx(HorizontalAxis, {
201
+ data,
202
+ xKey,
203
+ xFormatter,
204
+ xTickDetail,
205
+ xTicks
206
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(XAxis, {
207
+ dataKey: xKey,
208
+ type: xDomain ? "number" : "category",
209
+ domain: xDomain,
210
+ ticks: xTicks,
211
+ height: (xTickDetail ? 40 : 30) + (xLabel ? 18 : 0),
212
+ label: xLabel ? {
213
+ value: xLabel,
214
+ position: "insideBottom",
215
+ className: AXIS_TITLE
216
+ } : void 0,
217
+ tickLine: false,
218
+ axisLine: false,
219
+ tickMargin: 8,
220
+ tickFormatter: (value) => xFormatter(value),
221
+ tick: xTickDetail || xTicks ? fittedTick(data, xKey, xFormatter, xTickDetail, false) : TICK,
222
+ interval: xTickDetail || xTicks ? 0 : void 0
223
+ }), /* @__PURE__ */ jsx(YAxis, {
224
+ tickLine: false,
225
+ axisLine: false,
226
+ width: "auto",
227
+ domain: yDomain,
228
+ ticks: yTicks,
229
+ label: yLabel ? {
230
+ value: yLabel,
231
+ angle: -90,
232
+ position: "insideLeft",
233
+ style: { textAnchor: "middle" },
234
+ className: AXIS_TITLE
235
+ } : void 0,
236
+ tickFormatter: (value) => (yTickFormatter ?? formatValue)(value),
237
+ tick: TICK
238
+ })] }),
239
+ crossesZero && (horizontal ? /* @__PURE__ */ jsx(ReferenceLine, {
240
+ x: 0,
241
+ stroke: "var(--border)"
242
+ }) : /* @__PURE__ */ jsx(ReferenceLine, {
243
+ y: 0,
244
+ stroke: "var(--border)"
245
+ })),
246
+ referenceLines.map(({ label, emphasis = "muted", labelPosition = "insideTopLeft", ...at }) => /* @__PURE__ */ jsx(ReferenceLine, {
247
+ ...horizontal ? {
248
+ x: at.y,
249
+ y: at.x
250
+ } : at,
251
+ stroke: emphasis === "strong" ? "var(--foreground)" : "var(--muted-foreground)",
252
+ strokeDasharray: at.y === void 0 ? void 0 : "4 4",
253
+ label: label === void 0 ? void 0 : {
254
+ value: label,
255
+ position: labelPosition,
256
+ className: "fill-muted-foreground text-xs"
257
+ }
258
+ }, `${at.x ?? ""}:${at.y ?? ""}:${label ?? ""}`)),
259
+ /* @__PURE__ */ jsx(ChartTooltip, { content: /* @__PURE__ */ jsx(ChartTooltipContent, {
260
+ labelFormatter: (_, payload) => {
261
+ const row = payload[0]?.payload;
262
+ return row ? xTooltipFormatter(row[xKey]) : null;
263
+ },
264
+ formatter: seriesRow(series, formatValue)
265
+ }) }),
266
+ children,
267
+ /* @__PURE__ */ jsx(ChartLegend, {
268
+ content: /* @__PURE__ */ jsx(ChartLegendContent, {}),
269
+ itemSorter: null
270
+ })
271
+ ]
272
+ })
217
273
  })
218
- }) }),
274
+ }),
219
275
  footer && /* @__PURE__ */ jsx(CardFooter, {
220
276
  className: "flex-col items-start gap-2 text-sm",
221
277
  children: footer
@@ -66,7 +66,7 @@ export interface SeriesChartProps<T extends object, K extends keyof T> {
66
66
  yDomain?: [number, number];
67
67
  /** Where the value axis puts its ticks, such as [0, 25, 50, 75, 100]. */
68
68
  yTicks?: readonly number[];
69
- /** Height of the plot and legend, in pixels. */
69
+ /** Height of the plot and legend, in pixels. Sideways bars grow when wrapped tick details need more room. */
70
70
  height: number;
71
71
  /** A second line under each x tick, so a row's figure reads without a hover: "FY24" then "▲₹1.2L". */
72
72
  xTickDetail?: (row: T) => string;
@@ -2,9 +2,21 @@ import { defaultLocale } from "./locale.js";
2
2
  import { useLocale } from "../components/siyahi/siyahi-provider.js";
3
3
  import { useMemo } from "react";
4
4
  //#region src/lib/format.ts
5
+ const numbers = /* @__PURE__ */ new Map();
6
+ const clocks = /* @__PURE__ */ new Map();
7
+ const regions = /* @__PURE__ */ new Map();
8
+ function numberFormat(locale, options) {
9
+ const key = JSON.stringify([locale, options]);
10
+ let formatter = numbers.get(key);
11
+ if (!formatter) {
12
+ formatter = new Intl.NumberFormat(locale, options);
13
+ numbers.set(key, formatter);
14
+ }
15
+ return formatter;
16
+ }
5
17
  /** An amount in the locale's currency, whole units by default: ₹1,53,110. Signed figures belong in `Delta`. */
6
18
  function money(value, locale = defaultLocale, fractionDigits = 0) {
7
- return new Intl.NumberFormat(locale.numberLocale, {
19
+ return numberFormat(locale.numberLocale, {
8
20
  style: "currency",
9
21
  currency: locale.currency,
10
22
  minimumFractionDigits: fractionDigits,
@@ -18,8 +30,9 @@ function money(value, locale = defaultLocale, fractionDigits = 0) {
18
30
  * `Delta`, which adds the arrow.
19
31
  */
20
32
  function moneyCompact(value, locale = defaultLocale) {
21
- if (new Intl.Locale(locale.numberLocale).region === "IN" && Math.abs(value) < 1e5) return money(value, locale);
22
- return new Intl.NumberFormat(locale.numberLocale, {
33
+ if (!regions.has(locale.numberLocale)) regions.set(locale.numberLocale, new Intl.Locale(locale.numberLocale).region);
34
+ if (regions.get(locale.numberLocale) === "IN" && Math.abs(value) < 1e5) return money(value, locale);
35
+ return numberFormat(locale.numberLocale, {
23
36
  style: "currency",
24
37
  currency: locale.currency,
25
38
  notation: "compact",
@@ -28,11 +41,11 @@ function moneyCompact(value, locale = defaultLocale) {
28
41
  }
29
42
  /** A whole count with the locale's grouping: 1,53,110 in en-IN. */
30
43
  function count(value, locale = defaultLocale) {
31
- return new Intl.NumberFormat(locale.numberLocale, { maximumFractionDigits: 0 }).format(value);
44
+ return numberFormat(locale.numberLocale, { maximumFractionDigits: 0 }).format(value);
32
45
  }
33
46
  /** A fraction as a percentage: 0.1234 is 12.3%. */
34
47
  function pct(value, locale = defaultLocale, fractionDigits = 1) {
35
- return new Intl.NumberFormat(locale.numberLocale, {
48
+ return numberFormat(locale.numberLocale, {
36
49
  style: "percent",
37
50
  minimumFractionDigits: fractionDigits,
38
51
  maximumFractionDigits: fractionDigits
@@ -40,19 +53,25 @@ function pct(value, locale = defaultLocale, fractionDigits = 1) {
40
53
  }
41
54
  /** A 24-hour time in the locale's zone, with the zone written: 14:30 IST. */
42
55
  function time(value, locale = defaultLocale, { withDate = false } = {}) {
43
- const date = withDate ? {
44
- day: "numeric",
45
- month: "short",
46
- year: "numeric"
47
- } : {};
48
- return new Intl.DateTimeFormat(locale.numberLocale, {
49
- ...date,
56
+ const options = {
57
+ ...withDate ? {
58
+ day: "numeric",
59
+ month: "short",
60
+ year: "numeric"
61
+ } : {},
50
62
  hour: "2-digit",
51
63
  minute: "2-digit",
52
64
  hourCycle: "h23",
53
65
  timeZone: locale.timeZone,
54
66
  timeZoneName: "short"
55
- }).format(new Date(value));
67
+ };
68
+ const key = JSON.stringify([locale.numberLocale, options]);
69
+ let formatter = clocks.get(key);
70
+ if (!formatter) {
71
+ formatter = new Intl.DateTimeFormat(locale.numberLocale, options);
72
+ clocks.set(key, formatter);
73
+ }
74
+ return formatter.format(new Date(value));
56
75
  }
57
76
  /** The formatters above, bound to the locale of the nearest `SiyahiProvider`. */
58
77
  function useFormat() {
@@ -0,0 +1,8 @@
1
+ import { KeyboardEventHandler, RefObject } from "react";
2
+ //#region src/lib/heatmap-navigation.d.ts
3
+ /** One tab stop per heatmap; arrows move between its actual drawn cells. */
4
+ export declare function useHeatmapNavigation(): {
5
+ ref: RefObject<HTMLDivElement | null>;
6
+ onKeyDown: KeyboardEventHandler<HTMLDivElement>;
7
+ };
8
+ //#endregion