@siyahi/ui 0.1.9-next.3 → 0.1.9-next.31

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 (254) hide show
  1. package/DESIGN.md +251 -0
  2. package/README.md +94 -177
  3. package/THIRD_PARTY_NOTICES.md +11 -11
  4. package/agents/examples/accordion.tsx +39 -0
  5. package/agents/examples/alert.tsx +24 -0
  6. package/agents/examples/app-sidebar.tsx +40 -0
  7. package/agents/examples/avatar.tsx +24 -0
  8. package/agents/examples/badge.tsx +26 -0
  9. package/agents/examples/bar-list-labels.tsx +22 -0
  10. package/agents/examples/bar-list-sides.tsx +40 -0
  11. package/agents/examples/bar-list.tsx +24 -0
  12. package/agents/examples/breadcrumb.tsx +28 -0
  13. package/agents/examples/button-states.tsx +23 -0
  14. package/agents/examples/button.tsx +21 -0
  15. package/agents/examples/calendar.tsx +11 -0
  16. package/agents/examples/callout.tsx +33 -0
  17. package/agents/examples/card.tsx +36 -0
  18. package/agents/examples/change-bar.tsx +104 -0
  19. package/agents/examples/change-grid-money.tsx +36 -0
  20. package/agents/examples/change-grid.tsx +43 -0
  21. package/agents/examples/chart-area-log.tsx +45 -0
  22. package/agents/examples/chart-area.tsx +39 -0
  23. package/agents/examples/chart-bar-flows.tsx +32 -0
  24. package/agents/examples/chart-bar-hourly.tsx +28 -0
  25. package/agents/examples/chart-bar-losses.tsx +29 -0
  26. package/agents/examples/chart-bar-teams.tsx +72 -0
  27. package/agents/examples/chart-bar-years.tsx +42 -0
  28. package/agents/examples/chart-bar.tsx +56 -0
  29. package/agents/examples/chart-category-ticks.tsx +50 -0
  30. package/agents/examples/chart-line-events.tsx +47 -0
  31. package/agents/examples/chart-line-forecast.tsx +36 -0
  32. package/agents/examples/chart-line-log.tsx +39 -0
  33. package/agents/examples/chart-line-quiet.tsx +114 -0
  34. package/agents/examples/chart-line-timeline.tsx +41 -0
  35. package/agents/examples/chart-line.tsx +57 -0
  36. package/agents/examples/chart-panels.tsx +94 -0
  37. package/agents/examples/code-block.tsx +40 -0
  38. package/agents/examples/collapsible.tsx +18 -0
  39. package/agents/examples/command.tsx +29 -0
  40. package/agents/examples/context-menu.tsx +17 -0
  41. package/agents/examples/copy-button.tsx +8 -0
  42. package/agents/examples/data-bar.tsx +114 -0
  43. package/agents/examples/data-table-compact.tsx +93 -0
  44. package/agents/examples/data-table-filters.tsx +103 -0
  45. package/agents/examples/data-table.tsx +96 -0
  46. package/agents/examples/delta.tsx +18 -0
  47. package/agents/examples/dialog.tsx +28 -0
  48. package/agents/examples/distribution-bar-budget.tsx +30 -0
  49. package/agents/examples/distribution-bar-tug.tsx +74 -0
  50. package/agents/examples/drawdown-chart.tsx +35 -0
  51. package/agents/examples/dropdown-menu.tsx +31 -0
  52. package/agents/examples/field.tsx +108 -0
  53. package/agents/examples/formatters.tsx +42 -0
  54. package/agents/examples/heatmap-calendar-signed.tsx +67 -0
  55. package/agents/examples/heatmap-calendar.tsx +62 -0
  56. package/agents/examples/heatmap-status.tsx +61 -0
  57. package/agents/examples/heatmap-weekday.tsx +72 -0
  58. package/agents/examples/histogram-chart.tsx +28 -0
  59. package/agents/examples/input-states.tsx +23 -0
  60. package/agents/examples/input.tsx +17 -0
  61. package/agents/examples/local-time.tsx +14 -0
  62. package/agents/examples/metric-strip.tsx +46 -0
  63. package/agents/examples/overview-dashboard.tsx +180 -0
  64. package/agents/examples/page-hero.tsx +38 -0
  65. package/agents/examples/page-section.tsx +52 -0
  66. package/agents/examples/popover.tsx +23 -0
  67. package/agents/examples/price-chart.tsx +32 -0
  68. package/agents/examples/progress-tone.tsx +29 -0
  69. package/agents/examples/progress.tsx +10 -0
  70. package/agents/examples/range-position-table.tsx +113 -0
  71. package/agents/examples/range-position.tsx +55 -0
  72. package/agents/examples/scatter-chart-quadrants.tsx +63 -0
  73. package/agents/examples/scatter-chart-scales.tsx +46 -0
  74. package/agents/examples/scatter-chart.tsx +78 -0
  75. package/agents/examples/scroll-area.tsx +22 -0
  76. package/agents/examples/section-cards-select.tsx +28 -0
  77. package/agents/examples/section-cards-trend.tsx +32 -0
  78. package/agents/examples/section-cards.tsx +48 -0
  79. package/agents/examples/select.tsx +27 -0
  80. package/agents/examples/separator.tsx +13 -0
  81. package/agents/examples/sheet.tsx +15 -0
  82. package/agents/examples/skeleton.tsx +11 -0
  83. package/agents/examples/slider.tsx +5 -0
  84. package/agents/examples/slope-chart-market.tsx +33 -0
  85. package/agents/examples/slope-chart.tsx +26 -0
  86. package/agents/examples/stale-blocks.tsx +56 -0
  87. package/agents/examples/surfaces.tsx +116 -0
  88. package/agents/examples/table.tsx +60 -0
  89. package/agents/examples/tabs.tsx +16 -0
  90. package/agents/examples/target-rows.tsx +74 -0
  91. package/agents/examples/toggle-group.tsx +12 -0
  92. package/agents/examples/toggle.tsx +11 -0
  93. package/agents/examples/tooltip.tsx +13 -0
  94. package/agents/examples/treemap.tsx +93 -0
  95. package/agents/examples/waterfall-chart-stores.tsx +27 -0
  96. package/agents/examples/waterfall-chart.tsx +37 -0
  97. package/agents/manifest.json +1391 -0
  98. package/agents/report-rules.json +7 -0
  99. package/dist/components/registry/charts/area-chart-card.d.ts +7 -3
  100. package/dist/components/registry/charts/area-chart-card.js +56 -10
  101. package/dist/components/registry/charts/bar-chart-card.d.ts +1 -1
  102. package/dist/components/registry/charts/bar-chart-card.js +20 -22
  103. package/dist/components/registry/charts/line-chart-card.d.ts +3 -3
  104. package/dist/components/registry/charts/line-chart-card.js +44 -9
  105. package/dist/components/registry/dashboard-01/section-cards.d.ts +6 -0
  106. package/dist/components/registry/dashboard-01/section-cards.js +51 -35
  107. package/dist/components/registry/dashboardcn/bar-list.js +17 -10
  108. package/dist/components/registry/dashboardcn/distribution-bar.js +6 -4
  109. package/dist/components/registry/dashboardcn/kpi-row-card.d.ts +29 -0
  110. package/dist/components/registry/dashboardcn/kpi-row-card.js +81 -0
  111. package/dist/components/registry/dashboardcn/sparkline.d.ts +22 -0
  112. package/dist/components/registry/dashboardcn/sparkline.js +117 -0
  113. package/dist/components/registry/heatmap/calendar-heatmap.d.ts +11 -3
  114. package/dist/components/registry/heatmap/calendar-heatmap.js +164 -45
  115. package/dist/components/registry/heatmap/status-heatmap.d.ts +5 -3
  116. package/dist/components/registry/heatmap/status-heatmap.js +115 -54
  117. package/dist/components/registry/heatmap/weekday-heatmap.d.ts +9 -3
  118. package/dist/components/registry/heatmap/weekday-heatmap.js +83 -53
  119. package/dist/components/registry/niko-table/core/data-table-context.d.ts +18 -1
  120. package/dist/components/registry/niko-table/core/data-table-context.js +7 -1
  121. package/dist/components/registry/niko-table/core/data-table-grouped-row.js +3 -1
  122. package/dist/components/registry/niko-table/core/data-table-root.js +39 -4
  123. package/dist/components/registry/niko-table/core/data-table-structure.js +17 -12
  124. package/dist/components/registry/niko-table/core/data-table.d.ts +2 -2
  125. package/dist/components/registry/niko-table/core/data-table.js +5 -1
  126. package/dist/components/registry/niko-table/filters/table-faceted-filter.js +5 -5
  127. package/dist/components/registry/niko-table/filters/table-search-filter.js +1 -1
  128. package/dist/components/registry/niko-table/lib/flex-columns.d.ts +1 -1
  129. package/dist/components/registry/niko-table/lib/flex-columns.js +3 -3
  130. package/dist/components/registry/niko-table/lib/styles.d.ts +1 -1
  131. package/dist/components/registry/niko-table/lib/styles.js +3 -3
  132. package/dist/components/registry/niko-table/types/index.d.ts +9 -0
  133. package/dist/components/registry/niko-table/ui/table.js +2 -2
  134. package/dist/components/registry/sidebar-07/app-sidebar.d.ts +1 -1
  135. package/dist/components/registry/sidebar-07/app-sidebar.js +3 -1
  136. package/dist/components/registry/sidebar-07/nav-main.d.ts +2 -1
  137. package/dist/components/registry/sidebar-07/nav-main.js +16 -3
  138. package/dist/components/siyahi/callout.d.ts +17 -0
  139. package/dist/components/siyahi/callout.js +34 -0
  140. package/dist/components/siyahi/change-bar.d.ts +21 -0
  141. package/dist/components/siyahi/change-bar.js +35 -0
  142. package/dist/components/siyahi/change-grid.d.ts +31 -0
  143. package/dist/components/siyahi/change-grid.js +74 -0
  144. package/dist/components/siyahi/data-bar.d.ts +19 -0
  145. package/dist/components/siyahi/data-bar.js +29 -0
  146. package/dist/components/siyahi/data-table-compact-columns-toggle.d.ts +4 -0
  147. package/dist/components/siyahi/data-table-compact-columns-toggle.js +19 -0
  148. package/dist/components/siyahi/data-table-footnote.d.ts +12 -0
  149. package/dist/components/siyahi/data-table-footnote.js +35 -0
  150. package/dist/components/siyahi/default-sidebar-open.d.ts +8 -0
  151. package/dist/components/siyahi/default-sidebar-open.js +13 -0
  152. package/dist/components/siyahi/delta.d.ts +5 -1
  153. package/dist/components/siyahi/delta.js +14 -9
  154. package/dist/components/siyahi/drawdown-chart.d.ts +34 -0
  155. package/dist/components/siyahi/drawdown-chart.js +102 -0
  156. package/dist/components/siyahi/histogram-chart.d.ts +57 -0
  157. package/dist/components/siyahi/histogram-chart.js +134 -0
  158. package/dist/components/siyahi/page-hero.d.ts +23 -0
  159. package/dist/components/siyahi/page-hero.js +43 -0
  160. package/dist/components/siyahi/page-section.d.ts +4 -1
  161. package/dist/components/siyahi/page-section.js +6 -3
  162. package/dist/components/siyahi/page-toc.d.ts +20 -0
  163. package/dist/components/siyahi/page-toc.js +76 -0
  164. package/dist/components/siyahi/panel-grid.d.ts +24 -0
  165. package/dist/components/siyahi/panel-grid.js +37 -0
  166. package/dist/components/siyahi/quote.d.ts +12 -0
  167. package/dist/components/siyahi/quote.js +23 -0
  168. package/dist/components/siyahi/range-position.d.ts +24 -0
  169. package/dist/components/siyahi/range-position.js +105 -0
  170. package/dist/components/siyahi/scale-toggle.d.ts +32 -0
  171. package/dist/components/siyahi/scale-toggle.js +61 -0
  172. package/dist/components/siyahi/scatter-chart.d.ts +69 -0
  173. package/dist/components/siyahi/scatter-chart.js +395 -0
  174. package/dist/components/siyahi/siyahi-provider.d.ts +15 -2
  175. package/dist/components/siyahi/siyahi-provider.js +17 -4
  176. package/dist/components/siyahi/slope-chart.d.ts +35 -0
  177. package/dist/components/siyahi/slope-chart.js +159 -0
  178. package/dist/components/siyahi/stale-note.d.ts +9 -0
  179. package/dist/components/siyahi/stale-note.js +12 -0
  180. package/dist/components/siyahi/status-alert.d.ts +15 -0
  181. package/dist/components/siyahi/status-alert.js +29 -0
  182. package/dist/components/siyahi/status-badge.d.ts +15 -0
  183. package/dist/components/siyahi/status-badge.js +24 -0
  184. package/dist/components/siyahi/target-rows.d.ts +34 -0
  185. package/dist/components/siyahi/target-rows.js +97 -0
  186. package/dist/components/siyahi/tone-progress.d.ts +1 -1
  187. package/dist/components/siyahi/tone-progress.js +1 -1
  188. package/dist/components/siyahi/treemap.d.ts +8 -5
  189. package/dist/components/siyahi/treemap.js +80 -34
  190. package/dist/components/siyahi/waterfall-chart.d.ts +51 -0
  191. package/dist/components/siyahi/waterfall-chart.js +147 -0
  192. package/dist/components/ui/accordion.js +5 -5
  193. package/dist/components/ui/alert.js +2 -2
  194. package/dist/components/ui/badge.js +1 -1
  195. package/dist/components/ui/breadcrumb.js +2 -2
  196. package/dist/components/ui/button.js +10 -10
  197. package/dist/components/ui/calendar.js +1 -1
  198. package/dist/components/ui/card.js +4 -4
  199. package/dist/components/ui/chart.js +1 -1
  200. package/dist/components/ui/code-block.js +13 -12
  201. package/dist/components/ui/command.js +7 -6
  202. package/dist/components/ui/context-menu.js +7 -7
  203. package/dist/components/ui/dialog.js +5 -5
  204. package/dist/components/ui/dropdown-menu.js +8 -8
  205. package/dist/components/ui/field.d.ts +26 -0
  206. package/dist/components/ui/field.js +109 -0
  207. package/dist/components/ui/input-group.js +11 -11
  208. package/dist/components/ui/input.js +1 -1
  209. package/dist/components/ui/label.d.ts +5 -0
  210. package/dist/components/ui/label.js +13 -0
  211. package/dist/components/ui/popover.js +3 -3
  212. package/dist/components/ui/progress.js +1 -1
  213. package/dist/components/ui/select.js +6 -6
  214. package/dist/components/ui/sheet.js +5 -5
  215. package/dist/components/ui/sidebar.js +17 -18
  216. package/dist/components/ui/skeleton.js +1 -1
  217. package/dist/components/ui/slider.js +2 -2
  218. package/dist/components/ui/table.js +2 -2
  219. package/dist/components/ui/tabs.js +2 -2
  220. package/dist/components/ui/textarea.js +1 -1
  221. package/dist/components/ui/toggle-group.js +2 -2
  222. package/dist/components/ui/toggle.js +4 -4
  223. package/dist/components/ui/tooltip.js +2 -2
  224. package/dist/index.d.ts +35 -8
  225. package/dist/index.js +30 -4
  226. package/dist/lib/chart-frame.d.ts +51 -7
  227. package/dist/lib/chart-frame.js +390 -133
  228. package/dist/lib/chart-marks.d.ts +83 -0
  229. package/dist/lib/chart-marks.js +216 -0
  230. package/dist/lib/chart-series.d.ts +42 -10
  231. package/dist/lib/compact-columns.d.ts +19 -0
  232. package/dist/lib/compact-columns.js +55 -0
  233. package/dist/lib/fit-text.d.ts +6 -0
  234. package/dist/lib/fit-text.js +11 -0
  235. package/dist/lib/format.d.ts +8 -3
  236. package/dist/lib/format.js +55 -17
  237. package/dist/lib/heat.d.ts +7 -0
  238. package/dist/lib/heat.js +14 -0
  239. package/dist/lib/heatmap-navigation.d.ts +8 -0
  240. package/dist/lib/heatmap-navigation.js +49 -0
  241. package/dist/lib/scatter.d.ts +88 -0
  242. package/dist/lib/scatter.js +179 -0
  243. package/dist/lib/ticks.d.ts +27 -0
  244. package/dist/lib/ticks.js +67 -0
  245. package/dist/lib/tone.d.ts +36 -0
  246. package/dist/lib/tone.js +35 -0
  247. package/dist/lib/treemap.d.ts +0 -5
  248. package/dist/lib/treemap.js +1 -11
  249. package/dist/lib/use-element-width.d.ts +5 -0
  250. package/dist/lib/use-element-width.js +15 -0
  251. package/package.json +5 -1
  252. package/registry.lock.json +16 -0
  253. package/report.css +63 -47
  254. package/styles/siyahi.css +708 -55
@@ -0,0 +1,83 @@
1
+ import { ChartSeries } from "./chart-series.js";
2
+ //#region src/lib/chart-marks.d.ts
3
+ /** The colour a series takes when it is one of many drawn quietly behind the few a page names in colour. */
4
+ export declare const QUIET = "var(--muted-foreground)";
5
+ /** Quiet series first, so the named ones draw over them; otherwise the page's order. */
6
+ export declare function drawOrder<T>(series: ChartSeries<T>): ChartSeries<T>;
7
+ /** Room the names need right of the plot: the widest name as drawn, after its leader and colour square. */
8
+ export declare function endLabelRoom(names: readonly string[]): number;
9
+ /**
10
+ * Whether every line ends on the plot's right edge: each series has a value in the last row, there is more than one
11
+ * row, and a numeric x axis runs no further than the data. A line that stops short would need a long leader across
12
+ * the others to reach its name, so the chart keeps its legend then.
13
+ */
14
+ export declare function linesEndAtEdge<T extends object, K extends keyof T>(data: readonly T[], xKey: K, series: ChartSeries<T>, xDomain?: readonly [number, number]): boolean;
15
+ /**
16
+ * Whether the names fit at the line ends: a card at least 400px wide, names taking at most 30% of it, and a plot
17
+ * tall enough to hold them all 14px apart. Otherwise the chart keeps its legend below.
18
+ */
19
+ export declare function endLabelsFit(names: readonly string[], width: number, plotHeight: number): boolean;
20
+ /** A series' line end, in pixels, and the name it carries there. */
21
+ export interface LineEnd {
22
+ /** The series' key: names can repeat, keys cannot. */
23
+ id: string;
24
+ name: string;
25
+ color: string;
26
+ x: number;
27
+ y: number;
28
+ /** A quiet (grey) series: its name is muted, with no colour square; its place says which line it is. */
29
+ quiet: boolean;
30
+ }
31
+ /** A name placed in the gutter right of the plot. */
32
+ export interface PlacedEnd extends LineEnd {
33
+ /** The level it sits at, after names that would collide are spread apart. */
34
+ at: number;
35
+ /** The plot's right edge, where every name starts, whichever point its line ends on. */
36
+ gutter: number;
37
+ /** A leader joins the name to its line's end: moved off the end's level, or the line ends short of the edge. */
38
+ leader: boolean;
39
+ }
40
+ /** The part of the chart the series are drawn in, as Recharts' usePlotArea gives it. */
41
+ export interface PlotBox {
42
+ x: number;
43
+ y: number;
44
+ width: number;
45
+ height: number;
46
+ }
47
+ /**
48
+ * Where each name goes: every line's name at its end's level, spread to 14px apart, top to bottom, pushed back up
49
+ * from the plot's foot and never above its top. A point that maps to no number (a scale being replaced) is left out.
50
+ */
51
+ export declare function placeEnds(lines: readonly LineEnd[], plot: PlotBox): PlacedEnd[];
52
+ interface EndLabelsProps<T extends object, K extends keyof T> {
53
+ data: readonly T[];
54
+ xKey: K;
55
+ series: ChartSeries<T>;
56
+ }
57
+ /**
58
+ * Each series' name in the gutter right of the plot, in the text colour beside a square of the line's colour, in
59
+ * place of a legend the reader has to match colours against. A leader joins each name to its line's end where the
60
+ * name was moved off the end's level or the line ends short of the plot's edge. A quiet (grey) series' name is
61
+ * muted, with no square: its place beside its line's end says which line it is.
62
+ */
63
+ export declare function EndLabels<T extends object, K extends keyof T>({ data, xKey, series }: EndLabelsProps<T, K>): import("react").JSX.Element | null;
64
+ interface TimeMarksProps<T extends object, K extends keyof T> {
65
+ data: readonly T[];
66
+ xKey: K;
67
+ /** The x value from which the data is a forecast. */
68
+ forecastFrom?: T[K];
69
+ /** The x value of today. */
70
+ today?: T[K];
71
+ /** Prefix of the clip paths the series' actual and forecast copies use (`<id>-actual`, `<id>-forecast`). */
72
+ clipId: string;
73
+ /** Hatch the forecast stretch; a stack's fills already cover it, so a stacked chart only labels it. */
74
+ hatch: boolean;
75
+ }
76
+ /**
77
+ * A forecast stretch, from forecastFrom to the last point: a faint hatched band labelled FORECAST behind the lines,
78
+ * and the clip paths that keep each line solid before it and dashed after. And a "today" rule: 1px in the link
79
+ * colour with a small mono capital label.
80
+ */
81
+ export declare function TimeMarks<T extends object, K extends keyof T>({ data, xKey, forecastFrom, today, clipId, hatch }: TimeMarksProps<T, K>): import("react").JSX.Element | null;
82
+ //#endregion
83
+ export {};
@@ -0,0 +1,216 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { usePlotArea, useXAxisScale, useYAxisScale } from "recharts";
3
+ //#region src/lib/chart-marks.tsx
4
+ /** The colour a series takes when it is one of many drawn quietly behind the few a page names in colour. */
5
+ const QUIET = "var(--muted-foreground)";
6
+ /** Quiet series first, so the named ones draw over them; otherwise the page's order. */
7
+ function drawOrder(series) {
8
+ return [...series.filter(({ color }) => color === QUIET), ...series.filter(({ color }) => color !== QUIET)];
9
+ }
10
+ const NAME_START = 24;
11
+ const NAME_FONT = "500 11px \"Geist Variable\", \"Geist Fallback\", ui-sans-serif, sans-serif";
12
+ const NAME_WIDTH_GUESS = 7.5;
13
+ const NAME_GAP = 14;
14
+ let measure;
15
+ /** A name's width as the browser draws it, measured on a canvas; a wide guess where there is none. */
16
+ function nameWidth(name) {
17
+ if (measure === void 0) measure = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d");
18
+ if (!measure) return name.length * NAME_WIDTH_GUESS;
19
+ measure.font = NAME_FONT;
20
+ return measure.measureText(name).width;
21
+ }
22
+ /** Room the names need right of the plot: the widest name as drawn, after its leader and colour square. */
23
+ function endLabelRoom(names) {
24
+ return Math.ceil(NAME_START + Math.max(0, ...names.map(nameWidth)) + 8);
25
+ }
26
+ /**
27
+ * Whether every line ends on the plot's right edge: each series has a value in the last row, there is more than one
28
+ * row, and a numeric x axis runs no further than the data. A line that stops short would need a long leader across
29
+ * the others to reach its name, so the chart keeps its legend then.
30
+ */
31
+ function linesEndAtEdge(data, xKey, series, xDomain) {
32
+ const last = data.at(-1);
33
+ if (!last || data.length < 2) return false;
34
+ if (xDomain && Number(last[xKey]) < xDomain[1]) return false;
35
+ return series.every(({ key }) => Number.isFinite(last[key]));
36
+ }
37
+ const END_LABELS_MIN_WIDTH = 400;
38
+ /**
39
+ * Whether the names fit at the line ends: a card at least 400px wide, names taking at most 30% of it, and a plot
40
+ * tall enough to hold them all 14px apart. Otherwise the chart keeps its legend below.
41
+ */
42
+ function endLabelsFit(names, width, plotHeight) {
43
+ return width >= END_LABELS_MIN_WIDTH && endLabelRoom(names) <= width * .3 && (names.length - 1) * NAME_GAP + 12 <= plotHeight;
44
+ }
45
+ /**
46
+ * Where each name goes: every line's name at its end's level, spread to 14px apart, top to bottom, pushed back up
47
+ * from the plot's foot and never above its top. A point that maps to no number (a scale being replaced) is left out.
48
+ */
49
+ function placeEnds(lines, plot) {
50
+ const gutter = plot.x + plot.width;
51
+ const ends = [];
52
+ for (const end of lines) if (Number.isFinite(end.x) && Number.isFinite(end.y)) ends.push({
53
+ ...end,
54
+ at: end.y,
55
+ gutter,
56
+ leader: false
57
+ });
58
+ ends.sort((a, b) => a.y - b.y);
59
+ let previous = -Infinity;
60
+ for (const end of ends) {
61
+ end.at = Math.max(end.y, previous + NAME_GAP);
62
+ previous = end.at;
63
+ }
64
+ let limit = plot.y + plot.height;
65
+ for (const end of ends.toReversed()) {
66
+ end.at = Math.min(end.at, limit);
67
+ limit = end.at - NAME_GAP;
68
+ }
69
+ for (const end of ends) {
70
+ end.at = Math.max(end.at, plot.y + 4);
71
+ end.leader = Math.abs(end.at - end.y) > 3 || end.x < gutter - 1;
72
+ }
73
+ return ends;
74
+ }
75
+ /**
76
+ * Each series' name in the gutter right of the plot, in the text colour beside a square of the line's colour, in
77
+ * place of a legend the reader has to match colours against. A leader joins each name to its line's end where the
78
+ * name was moved off the end's level or the line ends short of the plot's edge. A quiet (grey) series' name is
79
+ * muted, with no square: its place beside its line's end says which line it is.
80
+ */
81
+ function EndLabels({ data, xKey, series }) {
82
+ const xScale = useXAxisScale();
83
+ const yScale = useYAxisScale();
84
+ const plot = usePlotArea();
85
+ if (!xScale || !yScale || !plot) return null;
86
+ const lines = [];
87
+ for (const { key, label, color } of series) {
88
+ const row = data.findLast((candidate) => Number.isFinite(candidate[key]));
89
+ const x = row && xScale(row[xKey]);
90
+ const y = row && yScale(row[key]);
91
+ if (x !== void 0 && y !== void 0) lines.push({
92
+ id: String(key),
93
+ name: label,
94
+ color,
95
+ x,
96
+ y,
97
+ quiet: color === QUIET
98
+ });
99
+ }
100
+ return /* @__PURE__ */ jsx("g", {
101
+ "data-slot": "end-labels",
102
+ children: placeEnds(lines, plot).map(({ gutter, ...end }) => /* @__PURE__ */ jsxs("g", { children: [
103
+ end.leader && /* @__PURE__ */ jsx("polyline", {
104
+ points: `${end.x + 4},${end.y} ${Math.max(end.x + 4, gutter) + 6},${end.at}`,
105
+ fill: "none",
106
+ stroke: end.color,
107
+ strokeOpacity: .55
108
+ }),
109
+ !end.quiet && /* @__PURE__ */ jsx("rect", {
110
+ x: gutter + 13,
111
+ y: end.at - 3.5,
112
+ width: 7,
113
+ height: 7,
114
+ fill: end.color
115
+ }),
116
+ /* @__PURE__ */ jsx("text", {
117
+ x: gutter + (end.quiet ? 13 : NAME_START),
118
+ y: end.at + 4,
119
+ className: end.quiet ? "fill-muted-foreground text-2xs" : "fill-foreground text-2xs font-medium",
120
+ children: end.name
121
+ })
122
+ ] }, end.id))
123
+ });
124
+ }
125
+ const MARK_LABEL = "stroke-card font-mono text-2xs uppercase";
126
+ const HALO = {
127
+ paintOrder: "stroke",
128
+ strokeWidth: 3,
129
+ strokeLinejoin: "round"
130
+ };
131
+ /**
132
+ * A forecast stretch, from forecastFrom to the last point: a faint hatched band labelled FORECAST behind the lines,
133
+ * and the clip paths that keep each line solid before it and dashed after. And a "today" rule: 1px in the link
134
+ * colour with a small mono capital label.
135
+ */
136
+ function TimeMarks({ data, xKey, forecastFrom, today, clipId, hatch }) {
137
+ const xScale = useXAxisScale();
138
+ const plot = usePlotArea();
139
+ if (!xScale || !plot) return null;
140
+ const top = plot.y;
141
+ const bottom = plot.y + plot.height;
142
+ const cut = forecastFrom === void 0 ? void 0 : xScale(forecastFrom);
143
+ const last = data.at(-1);
144
+ const end = (last && xScale(last[xKey])) ?? plot.x + plot.width;
145
+ const now = today === void 0 ? void 0 : xScale(today);
146
+ return /* @__PURE__ */ jsxs("g", {
147
+ "data-slot": "time-marks",
148
+ children: [cut !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
149
+ /* @__PURE__ */ jsxs("defs", { children: [
150
+ /* @__PURE__ */ jsx("clipPath", {
151
+ id: `${clipId}-actual`,
152
+ children: /* @__PURE__ */ jsx("rect", {
153
+ x: -1e4,
154
+ y: -1e4,
155
+ width: cut + 1e4,
156
+ height: 2e4
157
+ })
158
+ }),
159
+ /* @__PURE__ */ jsx("clipPath", {
160
+ id: `${clipId}-forecast`,
161
+ children: /* @__PURE__ */ jsx("rect", {
162
+ x: cut,
163
+ y: -1e4,
164
+ width: 2e4,
165
+ height: 2e4
166
+ })
167
+ }),
168
+ /* @__PURE__ */ jsx("pattern", {
169
+ id: `${clipId}-hatch`,
170
+ width: 7,
171
+ height: 7,
172
+ patternUnits: "userSpaceOnUse",
173
+ patternTransform: "rotate(45)",
174
+ children: /* @__PURE__ */ jsx("line", {
175
+ x1: 0,
176
+ y1: 0,
177
+ x2: 0,
178
+ y2: 7,
179
+ stroke: "var(--foreground)",
180
+ strokeOpacity: .16
181
+ })
182
+ })
183
+ ] }),
184
+ hatch && /* @__PURE__ */ jsx("rect", {
185
+ x: cut,
186
+ y: top,
187
+ width: Math.max(0, end - cut),
188
+ height: plot.height,
189
+ fill: `url(#${clipId}-hatch)`
190
+ }),
191
+ /* @__PURE__ */ jsx("text", {
192
+ x: end - 4,
193
+ y: top + 12,
194
+ textAnchor: "end",
195
+ className: `${MARK_LABEL} fill-muted-foreground`,
196
+ style: HALO,
197
+ children: "Forecast"
198
+ })
199
+ ] }), now !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("line", {
200
+ x1: now,
201
+ x2: now,
202
+ y1: top,
203
+ y2: bottom,
204
+ stroke: "var(--link)"
205
+ }), /* @__PURE__ */ jsx("text", {
206
+ x: now - 5,
207
+ y: top + 12,
208
+ textAnchor: "end",
209
+ className: `${MARK_LABEL} fill-link`,
210
+ style: HALO,
211
+ children: "Today"
212
+ })] })]
213
+ });
214
+ }
215
+ //#endregion
216
+ export { EndLabels, QUIET, TimeMarks, drawOrder, endLabelRoom, endLabelsFit, linesEndAtEdge, placeEnds };
@@ -1,3 +1,4 @@
1
+ import { AxisScale } from "./ticks.js";
1
2
  import { ReactNode } from "react";
2
3
  //#region src/lib/chart-series.d.ts
3
4
  /**
@@ -6,7 +7,7 @@ import { ReactNode } from "react";
6
7
  */
7
8
  export type ChartColor = "var(--chart-1)" | "var(--chart-2)" | "var(--chart-3)" | "var(--chart-4)" | "var(--chart-5)" | "var(--muted-foreground)" | "var(--positive)" | "var(--negative)";
8
9
  /** The fields of a row that hold numbers: the ones a series can plot. */
9
- type NumericKey<T> = { [K in keyof T]-?: T[K] extends number | null | undefined ? K : never; }[keyof T] & string;
10
+ export type NumericKey<T> = { [K in keyof T]-?: T[K] extends number | null | undefined ? K : never; }[keyof T] & string;
10
11
  /** One series: the field it plots, its name in the legend and tooltip, and its colour. */
11
12
  export interface ChartSeriesItem<T> {
12
13
  key: NumericKey<T>;
@@ -49,21 +50,29 @@ export interface SeriesChartProps<T extends object, K extends keyof T> {
49
50
  /** 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
51
  xKey: K;
51
52
  series: ChartSeries<NoInfer<T>>;
53
+ /** Categories to label, or tick positions with xDomain. Every named tick is drawn; other rows keep their marks and tooltips. */
54
+ xTicks?: readonly T[K][];
52
55
  /** The x axis ticks, and the tooltip's heading unless xTooltipFormatter is set. Defaults to the value as text. */
53
56
  xFormatter?: (value: T[K]) => string;
54
57
  /** The tooltip's heading, when it should say more than a tick: "19:34:13 IST, 10 s average" against "19:34". */
55
58
  xTooltipFormatter?: (value: T[K]) => string;
56
59
  /** A series' value in the tooltip, and the y axis unless yTickFormatter is set. Defaults to the locale's numbers. */
57
60
  valueFormatter?: (value: number) => string;
58
- /** The y axis, when its ticks should be shorter than the tooltip's figures: moneyCompact against money. */
59
- 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. */
61
+ /**
62
+ * The y axis, when its ticks should be shorter than the tooltip's figures: moneyCompact against money. It gets the
63
+ * axis' largest tick as `scale`, so moneyCompact writes every tick in one unit.
64
+ */
65
+ yTickFormatter?: (value: number, scale: number) => string;
66
+ /**
67
+ * A fixed range for the value axis, such as [0, 100] for a percentage. By default it fits the data; a bar
68
+ * chart's always reaches zero.
69
+ */
61
70
  yDomain?: [number, number];
62
71
  /** Where the value axis puts its ticks, such as [0, 25, 50, 75, 100]. */
63
72
  yTicks?: readonly number[];
64
- /** Height of the plot and legend, in pixels. */
73
+ /** Height of the plot and legend, in pixels. Sideways bars grow when wrapped tick details need more room. */
65
74
  height: number;
66
- /** A second line under each x tick, so a row's figure reads without a hover: "FY24" then "▲₹1.2L". */
75
+ /** A second line under each x tick, so a row's figure reads without a hover: "FY24" then "+₹1.2L". */
67
76
  xTickDetail?: (row: T) => string;
68
77
  /** A title under the x axis, such as "Hour of day (IST)". Not drawn on sideways bars. */
69
78
  xLabel?: string;
@@ -75,18 +84,41 @@ export interface SeriesChartProps<T extends object, K extends keyof T> {
75
84
  placeholder?: ReactNode;
76
85
  /** Lines under the chart, such as a caption or a takeaway, in the card's footer. */
77
86
  footer?: ReactNode;
87
+ /**
88
+ * Marks the data as old, with since when, such as "3 Nov": the chart greys out and a small "Stale · 3 Nov" note
89
+ * sits under the title. The page decides; nothing is inferred from dates.
90
+ */
91
+ stale?: ReactNode;
92
+ }
93
+ /** What a chart of lines takes, LineChartCard and AreaChartCard: a forecast stretch and today. */
94
+ export interface LineMarksProps<T extends object, K extends keyof T> {
95
+ /**
96
+ * The x value from which the data is a forecast, such as the last month-end: each line goes dashed from it, a faint
97
+ * hatched band labelled FORECAST covers the stretch, and the end dots are hollow.
98
+ */
99
+ forecastFrom?: NoInfer<T[K]>;
100
+ /** The x value of today: a 1px rule in the link colour with a small TODAY label. */
101
+ today?: NoInfer<T[K]>;
78
102
  }
79
103
  /** A numeric x axis, for samples in time or any measure where a mark can sit between two rows. */
80
104
  export interface NumericXProps {
81
105
  /** 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
106
  xDomain?: [number, number];
83
- /** Where the x axis puts its ticks, in the xKey field's units; give xDomain with it, so the axis is numeric. */
84
- xTicks?: readonly number[];
107
+ }
108
+ /** A value axis that may run on a log scale: lines and areas, which need not start at zero as bars must. */
109
+ export interface ValueScaleProps {
110
+ /**
111
+ * "log" spreads values across powers of ten, for values that run from tiny to large; the first selection when
112
+ * yScaleToggle is set. Log draws only when every value, and yDomain, is above zero; otherwise the axis is linear.
113
+ * On log the axis ticks at 1, 2 and 5 inside yDomain, if given, and yTicks is not used.
114
+ */
115
+ yScale?: AxisScale;
116
+ /** Linear and Log buttons for the value axis in the header, after the action, so a reader can switch. */
117
+ yScaleToggle?: boolean;
85
118
  }
86
119
  /**
87
120
  * How a line or an area's edge joins its points: "linear" draws straight segments, as measured samples should
88
121
  * be drawn; "monotone", the default, smooths between them without overshooting a point.
89
122
  */
90
123
  export type ChartCurve = "monotone" | "linear";
91
- //#endregion
92
- export {};
124
+ //#endregion
@@ -0,0 +1,19 @@
1
+ import { DataTableColumnDef } from "../components/registry/niko-table/types/index.js";
2
+ import { ColumnVisibilityState, RowData } from "@tanstack/react-table";
3
+ //#region src/lib/compact-columns.d.ts
4
+ /** Reports the element's inner width whenever it changes. */
5
+ export declare function useTableWidth(element: HTMLElement | null, onChange: (width: number) => void): void;
6
+ /** Narrower than 40rem, the width a phone gives a table: freezing is suspended and toolbars fold. */
7
+ export declare function isCompactWidth(width: number): boolean;
8
+ export interface FittedColumns {
9
+ /** The caller's visibility with the columns that do not fit turned off. */
10
+ visibility: ColumnVisibilityState;
11
+ /** The columns turned off to fit; empty when everything fits. */
12
+ hidden: ReadonlySet<string>;
13
+ }
14
+ /**
15
+ * Which columns to hide so the table fits `width`: the rightmost columns without `meta.compact` go first. Null
16
+ * when no column sets `meta.compact`, so a table that never opted in keeps every column and scrolls.
17
+ */
18
+ export declare function fitColumns<TData extends RowData>(columns: DataTableColumnDef<TData>[], order: string[], visibility: ColumnVisibilityState, width: number): FittedColumns | null;
19
+ //#endregion
@@ -0,0 +1,55 @@
1
+ import React from "react";
2
+ //#region src/lib/compact-columns.ts
3
+ const DEFAULT_SIZE = 150;
4
+ /** Reports the element's inner width whenever it changes. */
5
+ function useTableWidth(element, onChange) {
6
+ React.useLayoutEffect(() => {
7
+ if (!element) return;
8
+ const measure = () => {
9
+ if (element.clientWidth > 0) onChange(element.clientWidth);
10
+ };
11
+ measure();
12
+ const observer = new ResizeObserver(measure);
13
+ observer.observe(element);
14
+ return () => observer.disconnect();
15
+ }, [element, onChange]);
16
+ }
17
+ /** Narrower than 40rem, the width a phone gives a table: freezing is suspended and toolbars fold. */
18
+ function isCompactWidth(width) {
19
+ if (width <= 0) return false;
20
+ return width < 40 * Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
21
+ }
22
+ function columnId(column) {
23
+ if (column.id) return column.id;
24
+ const key = column.accessorKey;
25
+ return typeof key === "string" ? key.replaceAll(".", "_") : void 0;
26
+ }
27
+ /**
28
+ * Which columns to hide so the table fits `width`: the rightmost columns without `meta.compact` go first. Null
29
+ * when no column sets `meta.compact`, so a table that never opted in keeps every column and scrolls.
30
+ */
31
+ function fitColumns(columns, order, visibility, width) {
32
+ if (width <= 0 || !columns.some((column) => column.meta?.compact)) return null;
33
+ const byId = new Map(columns.flatMap((column) => {
34
+ const id = columnId(column);
35
+ return id ? [[id, column]] : [];
36
+ }));
37
+ const shown = [...order.filter((id) => byId.has(id)), ...[...byId.keys()].filter((id) => !order.includes(id))].filter((id) => visibility[id] !== false);
38
+ let total = shown.reduce((sum, id) => sum + (byId.get(id)?.size ?? DEFAULT_SIZE), 0);
39
+ const hidden = /* @__PURE__ */ new Set();
40
+ for (const id of shown.toReversed()) {
41
+ if (total <= width) break;
42
+ const column = byId.get(id);
43
+ if (column?.meta?.compact) continue;
44
+ hidden.add(id);
45
+ total -= column?.size ?? DEFAULT_SIZE;
46
+ }
47
+ const fitted = { ...visibility };
48
+ for (const id of hidden) fitted[id] = false;
49
+ return {
50
+ visibility: fitted,
51
+ hidden
52
+ };
53
+ }
54
+ //#endregion
55
+ export { fitColumns, isCompactWidth, useTableWidth };
@@ -0,0 +1,6 @@
1
+ //#region src/lib/fit-text.d.ts
2
+ /** About how wide a text-xs label draws, in pixels. */
3
+ export declare const textWidth: (text: string) => number;
4
+ /** The label, cut with an ellipsis when it would draw wider than `width` pixels at text-xs. */
5
+ export declare function fit(text: string, width: number): string;
6
+ //#endregion
@@ -0,0 +1,11 @@
1
+ //#region src/lib/fit-text.ts
2
+ const CHAR_PX = 6.6;
3
+ /** About how wide a text-xs label draws, in pixels. */
4
+ const textWidth = (text) => text.length * CHAR_PX;
5
+ /** The label, cut with an ellipsis when it would draw wider than `width` pixels at text-xs. */
6
+ function fit(text, width) {
7
+ const room = Math.floor(width / CHAR_PX);
8
+ return text.length <= room ? text : `${text.slice(0, Math.max(room - 1, 1)).trimEnd()}…`;
9
+ }
10
+ //#endregion
11
+ export { fit, textWidth };
@@ -6,9 +6,13 @@ export declare function money(value: number, locale?: SiyahiLocale, fractionDigi
6
6
  * A short amount: ₹2.4L and ₹40Cr in en-IN, $2.4M in en-US. Indian readers
7
7
  * count in thousands up to a lakh, so in an Indian-region locale amounts under
8
8
  * one lakh print in full (₹99,999), never as ₹99.9K. Signed figures belong in
9
- * `Delta`, which adds the arrow.
9
+ * `Delta`, which adds the sign and a trend icon.
10
+ *
11
+ * Give `scale`, the largest amount on the same axis, and every tick takes that
12
+ * amount's unit, to two decimals so close ticks stay apart: ₹1.5L, ₹0.75L,
13
+ * ₹0, rather than switching to ₹75,000 halfway up.
10
14
  */
11
- export declare function moneyCompact(value: number, locale?: SiyahiLocale): string;
15
+ export declare function moneyCompact(value: number, locale?: SiyahiLocale, scale?: number): string;
12
16
  /** A whole count with the locale's grouping: 1,53,110 in en-IN. */
13
17
  export declare function count(value: number, locale?: SiyahiLocale): string;
14
18
  /** A fraction as a percentage: 0.1234 is 12.3%. */
@@ -22,7 +26,8 @@ export declare function time(value: Date | number | string, locale?: SiyahiLocal
22
26
  export interface Formatters {
23
27
  count: (value: number) => string;
24
28
  money: (value: number, fractionDigits?: number) => string;
25
- moneyCompact: (value: number) => string;
29
+ /** Pass `scale` on an axis so every tick shares one unit; a chart's yTickFormatter gets it as its second argument. */
30
+ moneyCompact: (value: number, scale?: number) => string;
26
31
  pct: (value: number, fractionDigits?: number) => string;
27
32
  time: (value: Date | number | string, options?: TimeOptions) => string;
28
33
  }
@@ -2,37 +2,69 @@ 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,
11
23
  maximumFractionDigits: fractionDigits
12
24
  }).format(value);
13
25
  }
26
+ const DIGITS = /* @__PURE__ */ new Set(["integer", "fraction"]);
27
+ const NUMBER = /* @__PURE__ */ new Set([
28
+ "integer",
29
+ "group",
30
+ "decimal",
31
+ "fraction"
32
+ ]);
14
33
  /**
15
34
  * A short amount: ₹2.4L and ₹40Cr in en-IN, $2.4M in en-US. Indian readers
16
35
  * count in thousands up to a lakh, so in an Indian-region locale amounts under
17
36
  * one lakh print in full (₹99,999), never as ₹99.9K. Signed figures belong in
18
- * `Delta`, which adds the arrow.
37
+ * `Delta`, which adds the sign and a trend icon.
38
+ *
39
+ * Give `scale`, the largest amount on the same axis, and every tick takes that
40
+ * amount's unit, to two decimals so close ticks stay apart: ₹1.5L, ₹0.75L,
41
+ * ₹0, rather than switching to ₹75,000 halfway up.
19
42
  */
20
- 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, {
43
+ function moneyCompact(value, locale = defaultLocale, scale) {
44
+ if (!regions.has(locale.numberLocale)) regions.set(locale.numberLocale, new Intl.Locale(locale.numberLocale).region);
45
+ if (regions.get(locale.numberLocale) === "IN" && (scale ?? Math.abs(value)) < 1e5) return money(value, locale);
46
+ const compact = numberFormat(locale.numberLocale, {
23
47
  style: "currency",
24
48
  currency: locale.currency,
25
49
  notation: "compact",
26
50
  maximumFractionDigits: 1
27
- }).format(value);
51
+ });
52
+ if (scale === void 0 || value === 0) return compact.format(value);
53
+ const parts = compact.formatToParts(Math.sign(value) * scale);
54
+ const shown = Number(parts.flatMap((part) => part.type === "decimal" ? "." : DIGITS.has(part.type) ? part.value : []).join(""));
55
+ const unit = 10 ** Math.round(Math.log10(scale / shown));
56
+ if (unit === 1) return compact.format(value);
57
+ const digits = numberFormat(locale.numberLocale, { maximumFractionDigits: 2 }).format(Math.abs(value) / unit);
58
+ const first = parts.findIndex((part) => part.type === "integer");
59
+ return parts.flatMap((part, index) => index === first ? digits : NUMBER.has(part.type) ? [] : part.value).join("");
28
60
  }
29
61
  /** A whole count with the locale's grouping: 1,53,110 in en-IN. */
30
62
  function count(value, locale = defaultLocale) {
31
- return new Intl.NumberFormat(locale.numberLocale, { maximumFractionDigits: 0 }).format(value);
63
+ return numberFormat(locale.numberLocale, { maximumFractionDigits: 0 }).format(value);
32
64
  }
33
65
  /** A fraction as a percentage: 0.1234 is 12.3%. */
34
66
  function pct(value, locale = defaultLocale, fractionDigits = 1) {
35
- return new Intl.NumberFormat(locale.numberLocale, {
67
+ return numberFormat(locale.numberLocale, {
36
68
  style: "percent",
37
69
  minimumFractionDigits: fractionDigits,
38
70
  maximumFractionDigits: fractionDigits
@@ -40,19 +72,25 @@ function pct(value, locale = defaultLocale, fractionDigits = 1) {
40
72
  }
41
73
  /** A 24-hour time in the locale's zone, with the zone written: 14:30 IST. */
42
74
  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,
75
+ const options = {
76
+ ...withDate ? {
77
+ day: "numeric",
78
+ month: "short",
79
+ year: "numeric"
80
+ } : {},
50
81
  hour: "2-digit",
51
82
  minute: "2-digit",
52
83
  hourCycle: "h23",
53
84
  timeZone: locale.timeZone,
54
85
  timeZoneName: "short"
55
- }).format(new Date(value));
86
+ };
87
+ const key = JSON.stringify([locale.numberLocale, options]);
88
+ let formatter = clocks.get(key);
89
+ if (!formatter) {
90
+ formatter = new Intl.DateTimeFormat(locale.numberLocale, options);
91
+ clocks.set(key, formatter);
92
+ }
93
+ return formatter.format(new Date(value));
56
94
  }
57
95
  /** The formatters above, bound to the locale of the nearest `SiyahiProvider`. */
58
96
  function useFormat() {
@@ -60,7 +98,7 @@ function useFormat() {
60
98
  return useMemo(() => ({
61
99
  count: (value) => count(value, locale),
62
100
  money: (value, fractionDigits) => money(value, locale, fractionDigits),
63
- moneyCompact: (value) => moneyCompact(value, locale),
101
+ moneyCompact: (value, scale) => moneyCompact(value, locale, scale),
64
102
  pct: (value, fractionDigits) => pct(value, locale, fractionDigits),
65
103
  time: (value, options) => time(value, locale, options)
66
104
  }), [locale]);
@@ -0,0 +1,7 @@
1
+ //#region src/lib/heat.d.ts
2
+ export declare const HEAT_STEPS = 5;
3
+ /** The range the heat scale uses: one that is not a positive number falls back to ±5%. */
4
+ export declare const heatRange: (range?: number) => number;
5
+ export declare const heatFill: (side: "positive" | "negative", step: number) => string;
6
+ export declare function heat(change: number | null, range?: number): string;
7
+ //#endregion
@@ -0,0 +1,14 @@
1
+ //#region src/lib/heat.ts
2
+ /** The signed heat scale a Treemap tile and a ChangeGrid cell share: five steps of gain or loss mixed into the card. */
3
+ const DEFAULT_RANGE = .05;
4
+ const HEAT_STEPS = 5;
5
+ /** The range the heat scale uses: one that is not a positive number falls back to ±5%. */
6
+ const heatRange = (range = DEFAULT_RANGE) => Number.isFinite(range) && range > 0 ? range : DEFAULT_RANGE;
7
+ const heatFill = (side, step) => `color-mix(in oklab, var(--${side}) var(--heat-${step}), var(--card))`;
8
+ function heat(change, range) {
9
+ if (!change || !Number.isFinite(change)) return "var(--muted)";
10
+ const step = Math.min(5, Math.ceil(Math.abs(change) / heatRange(range) * 5));
11
+ return heatFill(change > 0 ? "positive" : "negative", step);
12
+ }
13
+ //#endregion
14
+ export { HEAT_STEPS, heat, heatFill, heatRange };