@siyahi/ui 0.1.9-next.2 → 0.1.9-next.20

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 (149) hide show
  1. package/README.md +82 -176
  2. package/THIRD_PARTY_NOTICES.md +11 -11
  3. package/agents/examples/accordion.tsx +39 -0
  4. package/agents/examples/alert.tsx +25 -0
  5. package/agents/examples/app-sidebar.tsx +38 -0
  6. package/agents/examples/avatar.tsx +14 -0
  7. package/agents/examples/badge.tsx +18 -0
  8. package/agents/examples/bar-list-labels.tsx +22 -0
  9. package/agents/examples/bar-list-sides.tsx +30 -0
  10. package/agents/examples/bar-list.tsx +24 -0
  11. package/agents/examples/breadcrumb.tsx +28 -0
  12. package/agents/examples/button.tsx +21 -0
  13. package/agents/examples/calendar.tsx +15 -0
  14. package/agents/examples/card.tsx +34 -0
  15. package/agents/examples/chart-area.tsx +38 -0
  16. package/agents/examples/chart-bar-flows.tsx +31 -0
  17. package/agents/examples/chart-bar-hourly.tsx +28 -0
  18. package/agents/examples/chart-bar-losses.tsx +29 -0
  19. package/agents/examples/chart-bar-sectors.tsx +70 -0
  20. package/agents/examples/chart-bar-years.tsx +40 -0
  21. package/agents/examples/chart-bar.tsx +56 -0
  22. package/agents/examples/chart-category-ticks.tsx +50 -0
  23. package/agents/examples/chart-line-events.tsx +47 -0
  24. package/agents/examples/chart-line-timeline.tsx +41 -0
  25. package/agents/examples/chart-line.tsx +57 -0
  26. package/agents/examples/code-block.tsx +38 -0
  27. package/agents/examples/collapsible.tsx +16 -0
  28. package/agents/examples/command.tsx +27 -0
  29. package/agents/examples/context-menu.tsx +15 -0
  30. package/agents/examples/copy-button.tsx +8 -0
  31. package/agents/examples/data-bar.tsx +121 -0
  32. package/agents/examples/data-table-filters.tsx +103 -0
  33. package/agents/examples/data-table.tsx +98 -0
  34. package/agents/examples/delta.tsx +13 -0
  35. package/agents/examples/dialog.tsx +28 -0
  36. package/agents/examples/distribution-bar-budget.tsx +30 -0
  37. package/agents/examples/distribution-bar-tug.tsx +72 -0
  38. package/agents/examples/dropdown-menu.tsx +31 -0
  39. package/agents/examples/formatters.tsx +42 -0
  40. package/agents/examples/heatmap-calendar.tsx +50 -0
  41. package/agents/examples/heatmap-status.tsx +51 -0
  42. package/agents/examples/heatmap-weekday.tsx +53 -0
  43. package/agents/examples/input.tsx +17 -0
  44. package/agents/examples/local-time.tsx +14 -0
  45. package/agents/examples/metric-strip.tsx +40 -0
  46. package/agents/examples/page-section.tsx +52 -0
  47. package/agents/examples/popover.tsx +23 -0
  48. package/agents/examples/price-chart.tsx +32 -0
  49. package/agents/examples/progress-tone.tsx +28 -0
  50. package/agents/examples/progress.tsx +10 -0
  51. package/agents/examples/scroll-area.tsx +20 -0
  52. package/agents/examples/section-cards-select.tsx +27 -0
  53. package/agents/examples/section-cards-trend.tsx +30 -0
  54. package/agents/examples/section-cards.tsx +48 -0
  55. package/agents/examples/select.tsx +27 -0
  56. package/agents/examples/separator.tsx +13 -0
  57. package/agents/examples/sheet.tsx +15 -0
  58. package/agents/examples/skeleton.tsx +11 -0
  59. package/agents/examples/slider.tsx +5 -0
  60. package/agents/examples/surfaces.tsx +115 -0
  61. package/agents/examples/table.tsx +50 -0
  62. package/agents/examples/tabs.tsx +16 -0
  63. package/agents/examples/toggle-group.tsx +12 -0
  64. package/agents/examples/toggle.tsx +11 -0
  65. package/agents/examples/tooltip.tsx +13 -0
  66. package/agents/examples/treemap.tsx +87 -0
  67. package/agents/manifest.json +931 -0
  68. package/agents/report-rules.json +7 -0
  69. package/dist/components/registry/charts/bar-chart-card.js +33 -26
  70. package/dist/components/registry/dashboard-01/section-cards.d.ts +2 -0
  71. package/dist/components/registry/dashboard-01/section-cards.js +42 -35
  72. package/dist/components/registry/dashboardcn/bar-list.js +9 -8
  73. package/dist/components/registry/dashboardcn/distribution-bar.js +1 -1
  74. package/dist/components/registry/dashboardcn/kpi-row-card.d.ts +29 -0
  75. package/dist/components/registry/dashboardcn/kpi-row-card.js +81 -0
  76. package/dist/components/registry/dashboardcn/sparkline.d.ts +18 -0
  77. package/dist/components/registry/dashboardcn/sparkline.js +95 -0
  78. package/dist/components/registry/heatmap/calendar-heatmap.js +8 -1
  79. package/dist/components/registry/heatmap/status-heatmap.js +8 -1
  80. package/dist/components/registry/heatmap/weekday-heatmap.d.ts +6 -2
  81. package/dist/components/registry/heatmap/weekday-heatmap.js +56 -37
  82. package/dist/components/registry/niko-table/core/data-table-context.d.ts +2 -0
  83. package/dist/components/registry/niko-table/core/data-table-context.js +21 -0
  84. package/dist/components/registry/niko-table/core/data-table-grouped-row.js +3 -1
  85. package/dist/components/registry/niko-table/core/data-table-structure.js +15 -12
  86. package/dist/components/registry/niko-table/core/data-table.d.ts +2 -2
  87. package/dist/components/registry/niko-table/core/data-table.js +5 -1
  88. package/dist/components/registry/niko-table/filters/table-faceted-filter.js +4 -4
  89. package/dist/components/registry/niko-table/filters/table-search-filter.js +1 -1
  90. package/dist/components/registry/niko-table/lib/flex-columns.d.ts +1 -1
  91. package/dist/components/registry/niko-table/lib/flex-columns.js +3 -3
  92. package/dist/components/registry/niko-table/lib/styles.d.ts +1 -1
  93. package/dist/components/registry/niko-table/lib/styles.js +2 -2
  94. package/dist/components/registry/niko-table/ui/table.js +2 -2
  95. package/dist/components/registry/sidebar-07/app-sidebar.d.ts +1 -1
  96. package/dist/components/registry/sidebar-07/app-sidebar.js +3 -1
  97. package/dist/components/registry/sidebar-07/nav-main.d.ts +1 -1
  98. package/dist/components/registry/sidebar-07/nav-main.js +7 -1
  99. package/dist/components/siyahi/data-bar.d.ts +19 -0
  100. package/dist/components/siyahi/data-bar.js +29 -0
  101. package/dist/components/siyahi/page-section.d.ts +4 -1
  102. package/dist/components/siyahi/page-section.js +6 -3
  103. package/dist/components/siyahi/panel-grid.d.ts +22 -0
  104. package/dist/components/siyahi/panel-grid.js +34 -0
  105. package/dist/components/siyahi/siyahi-provider.d.ts +13 -2
  106. package/dist/components/siyahi/siyahi-provider.js +17 -4
  107. package/dist/components/siyahi/treemap.js +46 -4
  108. package/dist/components/ui/accordion.js +5 -5
  109. package/dist/components/ui/alert.js +2 -2
  110. package/dist/components/ui/badge.js +1 -1
  111. package/dist/components/ui/breadcrumb.js +2 -2
  112. package/dist/components/ui/button.js +10 -10
  113. package/dist/components/ui/calendar.js +1 -1
  114. package/dist/components/ui/card.js +4 -4
  115. package/dist/components/ui/chart.js +1 -1
  116. package/dist/components/ui/code-block.js +13 -12
  117. package/dist/components/ui/command.js +7 -6
  118. package/dist/components/ui/context-menu.js +7 -7
  119. package/dist/components/ui/dialog.js +5 -5
  120. package/dist/components/ui/dropdown-menu.js +8 -8
  121. package/dist/components/ui/input-group.js +11 -11
  122. package/dist/components/ui/input.js +1 -1
  123. package/dist/components/ui/popover.js +3 -3
  124. package/dist/components/ui/progress.js +1 -1
  125. package/dist/components/ui/select.js +6 -6
  126. package/dist/components/ui/sheet.js +5 -5
  127. package/dist/components/ui/sidebar.js +17 -18
  128. package/dist/components/ui/skeleton.js +1 -1
  129. package/dist/components/ui/slider.js +2 -2
  130. package/dist/components/ui/table.js +2 -2
  131. package/dist/components/ui/tabs.js +2 -2
  132. package/dist/components/ui/textarea.js +1 -1
  133. package/dist/components/ui/toggle-group.js +2 -2
  134. package/dist/components/ui/toggle.js +4 -4
  135. package/dist/components/ui/tooltip.js +2 -2
  136. package/dist/index.d.ts +7 -4
  137. package/dist/index.js +7 -4
  138. package/dist/lib/chart-frame.d.ts +13 -5
  139. package/dist/lib/chart-frame.js +207 -127
  140. package/dist/lib/chart-series.d.ts +7 -4
  141. package/dist/lib/format.js +32 -13
  142. package/dist/lib/heatmap-navigation.d.ts +8 -0
  143. package/dist/lib/heatmap-navigation.js +49 -0
  144. package/package.json +3 -1
  145. package/registry.lock.json +16 -0
  146. package/report.css +11 -11
  147. package/styles/siyahi.css +77 -11
  148. package/dist/lib/recharts.d.ts +0 -2
  149. package/dist/lib/recharts.js +0 -1
@@ -1,12 +1,22 @@
1
1
  import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../components/ui/card.js";
2
2
  import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent } from "../components/ui/chart.js";
3
+ import { cn } from "./utils.js";
3
4
  import { useLocale } from "../components/siyahi/siyahi-provider.js";
4
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
- import { CartesianGrid, ReferenceLine, XAxis, YAxis } from "recharts";
6
+ import { useMemo } from "react";
7
+ import { CartesianGrid, ReferenceLine, Text, XAxis, YAxis, useChartWidth } from "recharts";
6
8
  //#region src/lib/chart-frame.tsx
7
9
  const AXIS_TITLE = "fill-muted-foreground text-xs";
8
10
  const TICK = { className: "fill-muted-foreground font-mono" };
9
11
  const NO_LINES = [];
12
+ /**
13
+ * The colour a series' Bar takes, which its legend chip shows: the negative colour when every value drawn is below
14
+ * zero, since each bar's own Cell follows its sign but the chip follows the Bar.
15
+ */
16
+ function seriesFill(item, data) {
17
+ const values = data.map((row) => row[item.key]).filter((value) => Number.isFinite(value)).map(Number);
18
+ return item.negativeColor && values.length > 0 && values.every((value) => value < 0) ? item.negativeColor : item.color;
19
+ }
10
20
  /** A tooltip row: the series' colour and label, then its value as the page formats it, or — when the row has none. */
11
21
  function seriesRow(series, formatValue) {
12
22
  return (value, _name, item) => {
@@ -28,170 +38,240 @@ function seriesRow(series, formatValue) {
28
38
  ] });
29
39
  };
30
40
  }
31
- const CHAR_WIDTH = 6.6;
32
- /** Squeezes a line into its column with SVG's own textLength when it would run into the next one. */
33
- const fitTo = (text, room) => room > 0 && text.length * CHAR_WIDTH > room ? {
34
- textLength: room,
35
- lengthAdjust: "spacingAndGlyphs"
36
- } : {};
41
+ const GEIST_WIDTH = 6.6;
42
+ const MONO_WIDTH = 7.2;
43
+ /**
44
+ * Room to keep right of the plot when every named tick is drawn: half the last label, which centres on the
45
+ * domain's right end, and at least 24px.
46
+ */
47
+ function endRoom(xFormatter, xTicks) {
48
+ const last = xTicks.at(-1);
49
+ return Math.max(24, Math.ceil((last === void 0 ? 0 : xFormatter(last).length * MONO_WIDTH) / 2) + 4);
50
+ }
37
51
  /**
38
- * An x tick with a second line from its row. Under a column the lines hang below the axis; beside a horizontal
39
- * bar they straddle its middle. Both lines are set in Geist, not Mono: a name with a figure under it reads as a
40
- * label, as in BarList, and sector names in Mono would take most of a phone's width.
52
+ * An x tick with a second line or explicit position: Recharts'
53
+ * own ticks would rather drop a label than let it run into the next. With a detail, the second line comes from the
54
+ * tick's row; under a column the lines hang below the axis, and beside a horizontal bar they straddle its middle.
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
56
+ * sector names in Mono would take most of a phone's width.
41
57
  */
42
- function twoLineTick(data, xKey, xFormatter, detailOf, beside) {
43
- 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 }) => {
44
62
  const value = payload?.value;
45
- const row = data.find((r) => r[xKey] === value);
63
+ const row = rows.get(value);
46
64
  const name = xFormatter(value);
47
- const detail = row ? detailOf(row) : null;
48
- const room = beside || !visibleTicksCount ? 0 : Number(width) / visibleTicksCount - 6;
65
+ const detail = row && detailOf ? detailOf(row) : null;
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
+ });
49
96
  return /* @__PURE__ */ jsxs("text", {
50
97
  x,
51
98
  y,
52
99
  textAnchor: beside ? "end" : "middle",
53
- className: "recharts-cartesian-axis-tick-value fill-muted-foreground",
100
+ className: cn("recharts-cartesian-axis-tick-value fill-muted-foreground", !detailOf && "font-mono"),
54
101
  children: [/* @__PURE__ */ jsx("tspan", {
55
102
  x,
56
- dy: beside ? -4 : 12,
57
- ...fitTo(name, room),
103
+ dy: 12,
58
104
  children: name
59
105
  }), detail !== null && /* @__PURE__ */ jsx("tspan", {
60
106
  x,
61
107
  dy: 15,
62
108
  className: "tabular-nums",
63
- ...fitTo(detail, room),
64
109
  children: detail
65
110
  })]
66
111
  });
67
112
  };
68
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
+ }
69
129
  /**
70
130
  * The card, axes, grid, tooltip and legend the three chart cards share. Both
71
131
  * axes keep Recharts' default ids: marks find their axes by id, and a page
72
132
  * that gave its category axis its own id (dash#88) got a plot split in two
73
133
  * and every tooltip headed with the series name instead of the category.
74
134
  */
75
- function ChartFrame({ chart: Chart, children, horizontal = false, stackOffset, 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 }) {
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 }) {
76
136
  const { numberLocale } = useLocale();
77
- const formatValue = valueFormatter ?? new Intl.NumberFormat(numberLocale).format;
137
+ const formatValue = useMemo(() => valueFormatter ?? new Intl.NumberFormat(numberLocale).format, [valueFormatter, numberLocale]);
78
138
  const config = Object.fromEntries(series.map(({ key, label, color }) => [key, {
79
139
  label,
80
140
  color
81
141
  }]));
82
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
+ };
83
159
  return /* @__PURE__ */ jsxs(Card, { children: [
84
160
  /* @__PURE__ */ jsxs(CardHeader, { children: [
85
161
  /* @__PURE__ */ jsx(CardTitle, { children: title }),
86
162
  description && /* @__PURE__ */ jsx(CardDescription, { children: description }),
87
163
  action && /* @__PURE__ */ jsx(CardAction, { children: action })
88
164
  ] }),
89
- /* @__PURE__ */ jsx(CardContent, { children: placeholder ? /* @__PURE__ */ jsx("div", {
90
- "data-slot": "chart-placeholder",
91
- className: "flex items-center justify-center",
92
- style: { height },
93
- children: placeholder
94
- }) : /* @__PURE__ */ jsx(ChartContainer, {
95
- config,
96
- className: "aspect-auto w-full",
97
- style: { height },
98
- children: /* @__PURE__ */ jsxs(Chart, {
99
- data,
100
- layout: horizontal ? "vertical" : "horizontal",
101
- stackOffset,
102
- margin: {
103
- left: 0,
104
- right: 8
105
- },
106
- children: [
107
- /* @__PURE__ */ jsx(CartesianGrid, {
108
- vertical: horizontal,
109
- horizontal: !horizontal,
110
- strokeDasharray: "3 3"
111
- }),
112
- horizontal ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(XAxis, {
113
- type: "number",
114
- hide: true,
115
- domain: yDomain,
116
- ticks: yTicks
117
- }), /* @__PURE__ */ jsx(YAxis, {
118
- dataKey: xKey,
119
- type: "category",
120
- width: "auto",
121
- tickLine: false,
122
- axisLine: false,
123
- tickMargin: 8,
124
- tickFormatter: (value) => xFormatter(value),
125
- tick: xTickDetail ? twoLineTick(data, xKey, xFormatter, xTickDetail, horizontal) : TICK,
126
- interval: 0
127
- })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(XAxis, {
128
- dataKey: xKey,
129
- type: xDomain ? "number" : "category",
130
- domain: xDomain,
131
- ticks: xTicks,
132
- height: (xTickDetail ? 40 : 30) + (xLabel ? 18 : 0),
133
- label: xLabel ? {
134
- value: xLabel,
135
- position: "insideBottom",
136
- className: AXIS_TITLE
137
- } : void 0,
138
- tickLine: false,
139
- axisLine: false,
140
- tickMargin: 8,
141
- tickFormatter: (value) => xFormatter(value),
142
- tick: xTickDetail ? twoLineTick(data, xKey, xFormatter, xTickDetail, false) : TICK,
143
- interval: xTickDetail ? 0 : void 0
144
- }), /* @__PURE__ */ jsx(YAxis, {
145
- tickLine: false,
146
- axisLine: false,
147
- width: "auto",
148
- domain: yDomain,
149
- ticks: yTicks,
150
- label: yLabel ? {
151
- value: yLabel,
152
- angle: -90,
153
- position: "insideLeft",
154
- style: { textAnchor: "middle" },
155
- className: AXIS_TITLE
156
- } : void 0,
157
- tickFormatter: (value) => (yTickFormatter ?? formatValue)(value),
158
- tick: TICK
159
- })] }),
160
- crossesZero && (horizontal ? /* @__PURE__ */ jsx(ReferenceLine, {
161
- x: 0,
162
- stroke: "var(--border)"
163
- }) : /* @__PURE__ */ jsx(ReferenceLine, {
164
- y: 0,
165
- stroke: "var(--border)"
166
- })),
167
- referenceLines.map(({ label, emphasis = "muted", labelPosition = "insideTopLeft", ...at }) => /* @__PURE__ */ jsx(ReferenceLine, {
168
- ...horizontal ? {
169
- x: at.y,
170
- y: at.x
171
- } : at,
172
- stroke: emphasis === "strong" ? "var(--foreground)" : "var(--muted-foreground)",
173
- strokeDasharray: at.y === void 0 ? void 0 : "4 4",
174
- label: label === void 0 ? void 0 : {
175
- value: label,
176
- position: labelPosition,
177
- className: "fill-muted-foreground text-xs"
178
- }
179
- }, `${at.x ?? ""}:${at.y ?? ""}:${label ?? ""}`)),
180
- /* @__PURE__ */ jsx(ChartTooltip, { content: /* @__PURE__ */ jsx(ChartTooltipContent, {
181
- labelFormatter: (_, payload) => {
182
- const row = payload[0]?.payload;
183
- return row ? xTooltipFormatter(row[xKey]) : null;
184
- },
185
- formatter: seriesRow(series, formatValue)
186
- }) }),
187
- children,
188
- /* @__PURE__ */ jsx(ChartLegend, {
189
- content: /* @__PURE__ */ jsx(ChartLegendContent, {}),
190
- itemSorter: null
191
- })
192
- ]
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
+ })
193
273
  })
194
- }) }),
274
+ }),
195
275
  footer && /* @__PURE__ */ jsx(CardFooter, {
196
276
  className: "flex-col items-start gap-2 text-sm",
197
277
  children: footer
@@ -199,4 +279,4 @@ function ChartFrame({ chart: Chart, children, horizontal = false, stackOffset, t
199
279
  ] });
200
280
  }
201
281
  //#endregion
202
- export { ChartFrame, seriesRow };
282
+ export { ChartFrame, seriesFill, seriesRow };
@@ -49,6 +49,8 @@ export interface SeriesChartProps<T extends object, K extends keyof T> {
49
49
  /** The field the x axis reads: a month, an hour of day, a price bucket. One row per value; a repeated value is hidden behind the first. */
50
50
  xKey: K;
51
51
  series: ChartSeries<NoInfer<T>>;
52
+ /** Categories to label, or tick positions with xDomain. Every named tick is drawn; other rows keep their marks and tooltips. */
53
+ xTicks?: readonly T[K][];
52
54
  /** The x axis ticks, and the tooltip's heading unless xTooltipFormatter is set. Defaults to the value as text. */
53
55
  xFormatter?: (value: T[K]) => string;
54
56
  /** The tooltip's heading, when it should say more than a tick: "19:34:13 IST, 10 s average" against "19:34". */
@@ -57,11 +59,14 @@ export interface SeriesChartProps<T extends object, K extends keyof T> {
57
59
  valueFormatter?: (value: number) => string;
58
60
  /** The y axis, when its ticks should be shorter than the tooltip's figures: moneyCompact against money. */
59
61
  yTickFormatter?: (value: number) => string;
60
- /** A fixed range for the value axis, such as [0, 100] for a percentage; by default it fits the data. */
62
+ /**
63
+ * A fixed range for the value axis, such as [0, 100] for a percentage. By default it fits the data; a bar
64
+ * chart's always reaches zero.
65
+ */
61
66
  yDomain?: [number, number];
62
67
  /** Where the value axis puts its ticks, such as [0, 25, 50, 75, 100]. */
63
68
  yTicks?: readonly number[];
64
- /** 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. */
65
70
  height: number;
66
71
  /** A second line under each x tick, so a row's figure reads without a hover: "FY24" then "▲₹1.2L". */
67
72
  xTickDetail?: (row: T) => string;
@@ -80,8 +85,6 @@ export interface SeriesChartProps<T extends object, K extends keyof T> {
80
85
  export interface NumericXProps {
81
86
  /** The x axis runs over this range of the xKey field, which must hold numbers; without it the axis is one slot per row. */
82
87
  xDomain?: [number, number];
83
- /** Where the x axis puts its ticks, in the xKey field's units. */
84
- xTicks?: readonly number[];
85
88
  }
86
89
  /**
87
90
  * How a line or an area's edge joins its points: "linear" draws straight segments, as measured samples should
@@ -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
@@ -0,0 +1,49 @@
1
+ import { useLayoutEffect, useRef } from "react";
2
+ //#region src/lib/heatmap-navigation.ts
3
+ /** One tab stop per heatmap; arrows move between its actual drawn cells. */
4
+ function useHeatmapNavigation() {
5
+ const ref = useRef(null);
6
+ const cells = () => [...ref.current?.querySelectorAll("rect[role=\"button\"]") ?? []];
7
+ useLayoutEffect(() => {
8
+ const buttons = cells();
9
+ const active = buttons.find((cell) => cell === document.activeElement) ?? buttons[0];
10
+ for (const cell of buttons) cell.tabIndex = cell === active ? 0 : -1;
11
+ });
12
+ const onKeyDown = (event) => {
13
+ if (!(event.target instanceof SVGElement) || event.target.getAttribute("role") !== "button") return;
14
+ const buttons = cells();
15
+ let next;
16
+ if (event.key === "Home") next = buttons[0];
17
+ else if (event.key === "End") next = buttons.at(-1);
18
+ else if ([
19
+ "ArrowLeft",
20
+ "ArrowRight",
21
+ "ArrowUp",
22
+ "ArrowDown"
23
+ ].includes(event.key)) {
24
+ const origin = event.target.getBoundingClientRect();
25
+ const horizontal = event.key === "ArrowLeft" || event.key === "ArrowRight";
26
+ const direction = event.key === "ArrowLeft" || event.key === "ArrowUp" ? -1 : 1;
27
+ let nearest = Infinity;
28
+ for (const cell of buttons) {
29
+ const box = cell.getBoundingClientRect();
30
+ const primary = (horizontal ? box.x - origin.x : box.y - origin.y) * direction;
31
+ const distance = primary + Math.abs(horizontal ? box.y - origin.y : box.x - origin.x) * 4;
32
+ if (primary > 1 && distance < nearest) {
33
+ nearest = distance;
34
+ next = cell;
35
+ }
36
+ }
37
+ } else return;
38
+ event.preventDefault();
39
+ if (!next) return;
40
+ for (const cell of buttons) cell.tabIndex = cell === next ? 0 : -1;
41
+ next.focus();
42
+ };
43
+ return {
44
+ ref,
45
+ onKeyDown
46
+ };
47
+ }
48
+ //#endregion
49
+ export { useHeatmapNavigation };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siyahi/ui",
3
- "version": "0.1.9-next.2",
3
+ "version": "0.1.9-next.20",
4
4
  "description": "Stock shadcn/ui on Base UI and Tailwind v4, a finance theme, a few compositions and formatters.",
5
5
  "keywords": [
6
6
  "base-ui",
@@ -19,6 +19,7 @@
19
19
  "directory": "packages/ui"
20
20
  },
21
21
  "files": [
22
+ "agents",
22
23
  "dist",
23
24
  "styles",
24
25
  "report.css",
@@ -32,6 +33,7 @@
32
33
  "**/*.css"
33
34
  ],
34
35
  "exports": {
36
+ "./agents/*": "./agents/*",
35
37
  ".": {
36
38
  "types": "./dist/index.d.ts",
37
39
  "default": "./dist/index.js"
@@ -49,6 +49,22 @@
49
49
  "copied": "2026-10-22",
50
50
  "sha256": "9028368f1363732cef765dabad2eca3ba98b608f1e237123b09df701c41c8b5a"
51
51
  },
52
+ {
53
+ "file": "src/components/registry/dashboardcn/kpi-row-card.tsx",
54
+ "registry": "@dashboardcn/kpi-row-card",
55
+ "origin": "https://dashboardcn.com/r/kpi-row-card.json",
56
+ "licence": "MIT, Copyright (c) 2026 dashboardcn contributors, https://github.com/NoahGdev/dashboardcn/blob/main/LICENSE",
57
+ "copied": "2026-10-26",
58
+ "sha256": "b49fa95b3efa19086b3df2e3a18f6a0d5a95204951448c5d93fc41c2bdb6326f"
59
+ },
60
+ {
61
+ "file": "src/components/registry/dashboardcn/sparkline.tsx",
62
+ "registry": "@dashboardcn/sparkline",
63
+ "origin": "https://dashboardcn.com/r/sparkline.json",
64
+ "licence": "MIT, Copyright (c) 2026 dashboardcn contributors, https://github.com/NoahGdev/dashboardcn/blob/main/LICENSE",
65
+ "copied": "2026-10-26",
66
+ "sha256": "54b43e63d866bcdf8071744df08e87f29385ba60d480ab9ee8aa1fac3eb33d5c"
67
+ },
52
68
  {
53
69
  "file": "src/components/registry/heatmap/calendar-heatmap.tsx",
54
70
  "registry": "@heatmap/calendar-heatmap",