@siyahi/ui 0.1.9-next.24 → 0.1.9-next.25

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 (45) hide show
  1. package/agents/examples/change-grid-money.tsx +36 -0
  2. package/agents/examples/change-grid.tsx +42 -0
  3. package/agents/examples/drawdown-chart.tsx +35 -0
  4. package/agents/examples/heatmap-calendar-signed.tsx +67 -0
  5. package/agents/examples/heatmap-status.tsx +6 -4
  6. package/agents/examples/histogram-chart.tsx +28 -0
  7. package/agents/examples/scatter-chart-quadrants.tsx +63 -0
  8. package/agents/examples/scatter-chart.tsx +75 -0
  9. package/agents/examples/slope-chart-market.tsx +33 -0
  10. package/agents/examples/slope-chart.tsx +26 -0
  11. package/agents/examples/waterfall-chart-stores.tsx +27 -0
  12. package/agents/examples/waterfall-chart.tsx +37 -0
  13. package/agents/manifest.json +160 -0
  14. package/dist/components/registry/heatmap/calendar-heatmap.d.ts +8 -2
  15. package/dist/components/registry/heatmap/calendar-heatmap.js +96 -18
  16. package/dist/components/registry/heatmap/status-heatmap.js +86 -47
  17. package/dist/components/siyahi/change-grid.d.ts +31 -0
  18. package/dist/components/siyahi/change-grid.js +74 -0
  19. package/dist/components/siyahi/drawdown-chart.d.ts +34 -0
  20. package/dist/components/siyahi/drawdown-chart.js +102 -0
  21. package/dist/components/siyahi/histogram-chart.d.ts +57 -0
  22. package/dist/components/siyahi/histogram-chart.js +134 -0
  23. package/dist/components/siyahi/scatter-chart.d.ts +59 -0
  24. package/dist/components/siyahi/scatter-chart.js +367 -0
  25. package/dist/components/siyahi/slope-chart.d.ts +35 -0
  26. package/dist/components/siyahi/slope-chart.js +159 -0
  27. package/dist/components/siyahi/treemap.js +3 -7
  28. package/dist/components/siyahi/waterfall-chart.d.ts +51 -0
  29. package/dist/components/siyahi/waterfall-chart.js +147 -0
  30. package/dist/index.d.ts +9 -2
  31. package/dist/index.js +7 -1
  32. package/dist/lib/chart-frame.d.ts +7 -2
  33. package/dist/lib/chart-frame.js +44 -10
  34. package/dist/lib/chart-series.d.ts +2 -3
  35. package/dist/lib/fit-text.d.ts +6 -0
  36. package/dist/lib/fit-text.js +11 -0
  37. package/dist/lib/heat.d.ts +7 -0
  38. package/dist/lib/heat.js +14 -0
  39. package/dist/lib/scatter.d.ts +98 -0
  40. package/dist/lib/scatter.js +218 -0
  41. package/dist/lib/treemap.d.ts +0 -5
  42. package/dist/lib/treemap.js +1 -16
  43. package/package.json +1 -1
  44. package/report.css +9 -0
  45. package/styles/siyahi.css +16 -0
@@ -64,6 +64,29 @@
64
64
  "guidance": "a series with `color` and `negativeColor`",
65
65
  "components": []
66
66
  },
67
+ {
68
+ "need": "A total built up or bridged step by step, such as a year's profit month by month or revenue from last year to this by store",
69
+ "guidance": "`WaterfallChartCard`; `horizontal` for long names",
70
+ "components": [
71
+ "WaterfallChartCard"
72
+ ]
73
+ },
74
+ {
75
+ "need": "Signed changes by two categories, such as returns or profit by month and year, or by store and quarter",
76
+ "guidance": "`ChangeGrid` in a `Card`, with each row's `total` worked out by the page",
77
+ "components": [
78
+ "ChangeGrid",
79
+ "Card"
80
+ ]
81
+ },
82
+ {
83
+ "need": "Each item's value at two points or against a benchmark, such as share of sales last year and this, or our mix against the market's",
84
+ "guidance": "`SlopeChart` in a `Card`, with the few lines that matter `highlighted`",
85
+ "components": [
86
+ "SlopeChart",
87
+ "Card"
88
+ ]
89
+ },
67
90
  {
68
91
  "need": "Ranked counts or amounts by category (a name, a bar and a figure)",
69
92
  "guidance": "`BarList`; give two lists one `max` to share a scale",
@@ -131,6 +154,20 @@
131
154
  "RangePosition"
132
155
  ]
133
156
  },
157
+ {
158
+ "need": "How far a level fell below its highest so far, over time, such as a fund's or a cash balance's drawdown",
159
+ "guidance": "`DrawdownChartCard` with the levels in `levelKey`",
160
+ "components": [
161
+ "DrawdownChartCard"
162
+ ]
163
+ },
164
+ {
165
+ "need": "How many values fall in each range, such as response times or each trade's return, with the median or p95 marked",
166
+ "guidance": "`HistogramChartCard` with the raw `values` and `marks`",
167
+ "components": [
168
+ "HistogramChartCard"
169
+ ]
170
+ },
134
171
  {
135
172
  "need": "A long daily series, such as a price or an order value",
136
173
  "guidance": "`PriceChart`",
@@ -145,6 +182,27 @@
145
182
  "Treemap"
146
183
  ]
147
184
  },
185
+ {
186
+ "need": "How two measures relate across many items, such as stores' size against growth",
187
+ "guidance": "`ScatterChartCard`",
188
+ "components": [
189
+ "ScatterChartCard"
190
+ ]
191
+ },
192
+ {
193
+ "need": "Items sorted into four groups by two signed changes, weighted by size, such as products by price change and sales change, sized by revenue",
194
+ "guidance": "`ScatterChartCard` with `quadrants` and each point's `size`",
195
+ "components": [
196
+ "ScatterChartCard"
197
+ ]
198
+ },
199
+ {
200
+ "need": "A gain or loss for every day, such as daily profit or a portfolio's daily change",
201
+ "guidance": "`CalendarHeatmap` with `signed`",
202
+ "components": [
203
+ "CalendarHeatmap"
204
+ ]
205
+ },
148
206
  {
149
207
  "need": "Activity by day, by weekday and hour, or a status over time",
150
208
  "guidance": "`CalendarHeatmap`, `WeekdayHeatmap`, `StatusHeatmap`",
@@ -442,6 +500,28 @@
442
500
  "useFormat"
443
501
  ]
444
502
  },
503
+ {
504
+ "file": "agents/examples/change-grid-money.tsx",
505
+ "components": [
506
+ "Card",
507
+ "CardContent",
508
+ "CardDescription",
509
+ "CardHeader",
510
+ "CardTitle",
511
+ "ChangeGrid"
512
+ ]
513
+ },
514
+ {
515
+ "file": "agents/examples/change-grid.tsx",
516
+ "components": [
517
+ "Card",
518
+ "CardContent",
519
+ "CardDescription",
520
+ "CardHeader",
521
+ "CardTitle",
522
+ "ChangeGrid"
523
+ ]
524
+ },
445
525
  {
446
526
  "file": "agents/examples/chart-area.tsx",
447
527
  "components": [
@@ -704,6 +784,13 @@
704
784
  "DistributionBar"
705
785
  ]
706
786
  },
787
+ {
788
+ "file": "agents/examples/drawdown-chart.tsx",
789
+ "components": [
790
+ "DrawdownChartCard",
791
+ "useFormat"
792
+ ]
793
+ },
707
794
  {
708
795
  "file": "agents/examples/dropdown-menu.tsx",
709
796
  "components": [
@@ -730,6 +817,22 @@
730
817
  "useFormat"
731
818
  ]
732
819
  },
820
+ {
821
+ "file": "agents/examples/heatmap-calendar-signed.tsx",
822
+ "components": [
823
+ "CalendarHeatmap",
824
+ "CalendarHeatmapBlock",
825
+ "CalendarHeatmapBody",
826
+ "CalendarHeatmapFooter",
827
+ "CalendarHeatmapLegend",
828
+ "Card",
829
+ "CardContent",
830
+ "CardDescription",
831
+ "CardHeader",
832
+ "CardTitle",
833
+ "useFormat"
834
+ ]
835
+ },
733
836
  {
734
837
  "file": "agents/examples/heatmap-calendar.tsx",
735
838
  "components": [
@@ -776,6 +879,13 @@
776
879
  "WeekdayHeatmapLegend"
777
880
  ]
778
881
  },
882
+ {
883
+ "file": "agents/examples/histogram-chart.tsx",
884
+ "components": [
885
+ "HistogramChartCard",
886
+ "useFormat"
887
+ ]
888
+ },
779
889
  {
780
890
  "file": "agents/examples/input-states.tsx",
781
891
  "components": [
@@ -905,6 +1015,20 @@
905
1015
  "useFormat"
906
1016
  ]
907
1017
  },
1018
+ {
1019
+ "file": "agents/examples/scatter-chart-quadrants.tsx",
1020
+ "components": [
1021
+ "ScatterChartCard",
1022
+ "useFormat"
1023
+ ]
1024
+ },
1025
+ {
1026
+ "file": "agents/examples/scatter-chart.tsx",
1027
+ "components": [
1028
+ "ScatterChartCard",
1029
+ "useFormat"
1030
+ ]
1031
+ },
908
1032
  {
909
1033
  "file": "agents/examples/scroll-area.tsx",
910
1034
  "components": [
@@ -979,6 +1103,28 @@
979
1103
  "Slider"
980
1104
  ]
981
1105
  },
1106
+ {
1107
+ "file": "agents/examples/slope-chart-market.tsx",
1108
+ "components": [
1109
+ "Card",
1110
+ "CardContent",
1111
+ "CardDescription",
1112
+ "CardHeader",
1113
+ "CardTitle",
1114
+ "SlopeChart"
1115
+ ]
1116
+ },
1117
+ {
1118
+ "file": "agents/examples/slope-chart.tsx",
1119
+ "components": [
1120
+ "Card",
1121
+ "CardContent",
1122
+ "CardDescription",
1123
+ "CardHeader",
1124
+ "CardTitle",
1125
+ "SlopeChart"
1126
+ ]
1127
+ },
982
1128
  {
983
1129
  "file": "agents/examples/surfaces.tsx",
984
1130
  "components": [
@@ -1076,6 +1222,20 @@
1076
1222
  "TreemapLegend",
1077
1223
  "useFormat"
1078
1224
  ]
1225
+ },
1226
+ {
1227
+ "file": "agents/examples/waterfall-chart-stores.tsx",
1228
+ "components": [
1229
+ "WaterfallChartCard",
1230
+ "useFormat"
1231
+ ]
1232
+ },
1233
+ {
1234
+ "file": "agents/examples/waterfall-chart.tsx",
1235
+ "components": [
1236
+ "WaterfallChartCard",
1237
+ "useFormat"
1238
+ ]
1079
1239
  }
1080
1240
  ]
1081
1241
  }
@@ -32,6 +32,10 @@ export type CalendarHeatmapProps = HTMLAttributes<HTMLDivElement> & {
32
32
  blockAspectRatio?: number;
33
33
  levels?: number;
34
34
  isNormalized?: boolean;
35
+ /** Colour each day green or red by its sign, deeper the larger it is, for a daily gain or loss. */
36
+ signed?: boolean;
37
+ /** With signed: the size that takes the deepest step. Defaults to the largest day's. */
38
+ range?: number;
35
39
  colors?: ColorConfig;
36
40
  locale?: Locale;
37
41
  labels?: Labels;
@@ -75,7 +79,7 @@ export type CalendarHeatmapProps = HTMLAttributes<HTMLDivElement> & {
75
79
  * @param levels - Total number of legend cells (including empty when not normalized). Default: 5
76
80
  * @param isNormalized - When true, uses min-max normalization across the dataset (suitable for signed values). When false (default), treats 0 as empty and scales from 0 to max.
77
81
  */
78
- export declare const CalendarHeatmap: ({ data, weekStart, continuousMonths, hasEmptyColumn, blockSize: blockSizeProp, blockMargin, blockRadius, blockAspectRatio, levels: levelsProp, isNormalized, colors, locale, labels: labelsProp, fontSize, emptyState, totalCount: totalCountProp, style, className, children, ...props }: CalendarHeatmapProps) => string | number | bigint | true | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | null;
82
+ export declare const CalendarHeatmap: ({ data, weekStart, continuousMonths, hasEmptyColumn, blockSize: blockSizeProp, blockMargin, blockRadius, blockAspectRatio, levels: levelsProp, isNormalized, signed, range, colors, locale, labels: labelsProp, fontSize, emptyState, totalCount: totalCountProp, style, className, children, ...props }: CalendarHeatmapProps) => string | number | bigint | true | import("react").JSX.Element | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | null;
79
83
  export type CalendarHeatmapBlockProps = HTMLAttributes<SVGRectElement> & {
80
84
  /** The cell the page has picked: drawn with a ring and announced as pressed. */
81
85
  selected?: boolean;
@@ -127,10 +131,12 @@ export type CalendarHeatmapLegendProps = Omit<HTMLAttributes<HTMLFieldSetElement
127
131
  less?: string;
128
132
  more?: string;
129
133
  };
134
+ /** A signed heatmap's figures in the legend's scale line, such as useFormat().money. */
135
+ valueFormatter?: (value: number) => string;
130
136
  children?: (props: {
131
137
  level: number;
132
138
  }) => ReactNode;
133
139
  };
134
- export declare const CalendarHeatmapLegend: ({ labels: labelsProp, className, children, ...props }: CalendarHeatmapLegendProps) => import("react").JSX.Element;
140
+ export declare const CalendarHeatmapLegend: ({ labels: labelsProp, valueFormatter, className, children, ...props }: CalendarHeatmapLegendProps) => import("react").JSX.Element;
135
141
  //#endregion
136
142
  export {};
@@ -1,18 +1,18 @@
1
1
  import { cn } from "../../../lib/utils.js";
2
2
  import { useHeatmapNavigation } from "../../../lib/heatmap-navigation.js";
3
3
  import { useElementWidth } from "../../../lib/use-element-width.js";
4
+ import { heat, heatFill } from "../../../lib/heat.js";
4
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
6
  import { Fragment as Fragment$1, createContext, use, useLayoutEffect, useMemo } from "react";
6
7
  import { differenceInCalendarDays, eachDayOfInterval, format, formatISO, getDay, getMonth, getYear, nextDay, parseISO, subWeeks } from "date-fns";
7
8
  //#region src/components/registry/heatmap/calendar-heatmap.tsx
8
9
  const colorStepCount = (levels, isNormalized) => Math.max(1, isNormalized ? levels : levels - 1);
9
- const getLevelFill = (level, levels, isNormalized, highlighted = false, colors) => {
10
+ const getLevelFill = (level, levels, isNormalized, colors) => {
10
11
  const emptyColor = colors?.empty ?? "var(--color-secondary)";
11
12
  const scaleColor = colors?.scale ?? "var(--color-chart-1)";
12
13
  if (level === 0) return emptyColor;
13
14
  const steps = colorStepCount(levels, isNormalized);
14
- const opacity = steps === 1 ? 100 : Math.round(20 + (level - 1) * 80 / (steps - 1));
15
- return `color-mix(in oklch, ${scaleColor} ${highlighted ? Math.round(opacity * .6) : opacity}%, transparent)`;
15
+ return `color-mix(in oklch, ${scaleColor} ${steps === 1 ? 100 : Math.round(20 + (level - 1) * 80 / (steps - 1))}%, transparent)`;
16
16
  };
17
17
  const calculateLevel = (value, minValue, maxValue, levels, isNormalized) => {
18
18
  const steps = colorStepCount(levels, isNormalized);
@@ -40,6 +40,14 @@ const generateWeekdayLabels = (locale) => {
40
40
  return date.toLocaleString("en-US", { weekday: "short" });
41
41
  });
42
42
  };
43
+ /** How a signed day is drawn: its heat tint, the edge that carries its sign's colour, and whether it is round. */
44
+ const signedMark = (value) => {
45
+ const known = Number.isFinite(value) ? value : 0;
46
+ return {
47
+ stroke: known > 0 ? "var(--positive)" : known < 0 ? "var(--negative)" : "var(--input)",
48
+ round: known < 0
49
+ };
50
+ };
43
51
  const EMPTY_STYLE = {};
44
52
  const LABEL_MARGIN = 8;
45
53
  const WEEKDAY_LABEL_WIDTH = 40;
@@ -218,7 +226,7 @@ const getMonthLabels = (weeks, monthNames = generateMonthLabels(), yearTicks = f
218
226
  * @param levels - Total number of legend cells (including empty when not normalized). Default: 5
219
227
  * @param isNormalized - When true, uses min-max normalization across the dataset (suitable for signed values). When false (default), treats 0 as empty and scales from 0 to max.
220
228
  */
221
- const CalendarHeatmap = ({ data, weekStart = 0, continuousMonths = true, hasEmptyColumn = false, blockSize: blockSizeProp, blockMargin = 4, blockRadius = 2, blockAspectRatio = 1, levels: levelsProp = 5, isNormalized = false, colors, locale, labels: labelsProp, fontSize = 14, emptyState, totalCount: totalCountProp, style = EMPTY_STYLE, className, children, ...props }) => {
229
+ const CalendarHeatmap = ({ data, weekStart = 0, continuousMonths = true, hasEmptyColumn = false, blockSize: blockSizeProp, blockMargin = 4, blockRadius = 2, blockAspectRatio = 1, levels: levelsProp = 5, isNormalized = false, signed = false, range, colors, locale, labels: labelsProp, fontSize = 14, emptyState, totalCount: totalCountProp, style = EMPTY_STYLE, className, children, ...props }) => {
222
230
  const levels = Math.max(1, levelsProp);
223
231
  const [rootRef, rootWidth] = useElementWidth();
224
232
  const dataWithLevels = useMemo(() => {
@@ -241,6 +249,15 @@ const CalendarHeatmap = ({ data, weekStart = 0, continuousMonths = true, hasEmpt
241
249
  continuousMonths
242
250
  ]);
243
251
  const weeks = useMemo(() => yearRows.flatMap((r) => r.weeks), [yearRows]);
252
+ const signedRange = useMemo(() => {
253
+ if (!signed) return null;
254
+ if (range !== void 0 && Number.isFinite(range) && range > 0) return range;
255
+ return Math.max(0, ...data.map((d) => Number.isFinite(d.value) ? Math.abs(d.value) : 0));
256
+ }, [
257
+ signed,
258
+ range,
259
+ data
260
+ ]);
244
261
  const labels = useMemo(() => ({
245
262
  months: generateMonthLabels(locale),
246
263
  weekdays: generateWeekdayLabels(locale),
@@ -280,7 +297,8 @@ const CalendarHeatmap = ({ data, weekStart = 0, continuousMonths = true, hasEmpt
280
297
  hasEmptyColumn,
281
298
  continuousMonths,
282
299
  locale,
283
- colors
300
+ colors,
301
+ signedRange
284
302
  }), [
285
303
  dataWithLevels,
286
304
  weeks,
@@ -303,7 +321,8 @@ const CalendarHeatmap = ({ data, weekStart = 0, continuousMonths = true, hasEmpt
303
321
  hasEmptyColumn,
304
322
  continuousMonths,
305
323
  locale,
306
- colors
324
+ colors,
325
+ signedRange
307
326
  ]);
308
327
  if (data.length === 0) return emptyState ? emptyState : null;
309
328
  return /* @__PURE__ */ jsx(CalendarHeatmapContext, {
@@ -322,8 +341,9 @@ const CalendarHeatmap = ({ data, weekStart = 0, continuousMonths = true, hasEmpt
322
341
  });
323
342
  };
324
343
  const CalendarHeatmapBlock = ({ ref, activity, dayIndex, weekIndex, highlighted = false, selected = false, onCellClick, onCellHover, onClick, onKeyDown, onMouseEnter, onMouseLeave, className, style: styleProp, ...props }) => {
325
- const { blockSize, blockWidth, blockMargin, blockRadius, labelHeight, labels, levels, isNormalized, colors } = useCalendarHeatmap();
344
+ const { blockSize, blockWidth, blockMargin, blockRadius, labelHeight, labels, levels, isNormalized, colors, signedRange } = useCalendarHeatmap();
326
345
  const level = Math.max(0, Math.min(colorStepCount(levels, isNormalized), activity.level));
346
+ const mark = signedRange === null ? null : signedMark(activity.value);
327
347
  const ariaLabel = (labels.cellLabel ?? "{{date}}: {{value}} contributions").replace("{{date}}", activity.date).replace("{{value}}", String(activity.value));
328
348
  return /* @__PURE__ */ jsx("rect", {
329
349
  ref,
@@ -332,24 +352,22 @@ const CalendarHeatmapBlock = ({ ref, activity, dayIndex, weekIndex, highlighted
332
352
  "aria-pressed": onCellClick ? selected : void 0,
333
353
  tabIndex: onCellClick ? 0 : -1,
334
354
  "aria-label": ariaLabel,
335
- className: cn("motion-safe:transition-opacity motion-safe:hover:opacity-70", onCellClick && "cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", className),
355
+ className: cn("hover:stroke-foreground hover:stroke-1", highlighted && "stroke-foreground stroke-1", selected && "stroke-foreground stroke-2 hover:stroke-2", onCellClick && "cursor-pointer focus-visible:outline-none focus-visible:stroke-ring focus-visible:stroke-2", className),
336
356
  "data-value": activity.value,
337
357
  "data-date": activity.date,
338
358
  "data-level": level,
339
359
  "data-highlighted": highlighted || void 0,
340
360
  "data-selected": selected || void 0,
341
361
  height: blockSize,
342
- rx: blockRadius,
343
- ry: blockRadius,
362
+ rx: mark?.round ? blockSize / 2 : blockRadius,
363
+ ry: mark?.round ? blockSize / 2 : blockRadius,
344
364
  width: blockWidth,
365
+ stroke: mark?.stroke,
366
+ strokeWidth: mark ? 1 : void 0,
345
367
  x: (blockWidth + blockMargin) * weekIndex,
346
368
  y: labelHeight + (blockSize + blockMargin) * dayIndex,
347
369
  style: {
348
- fill: getLevelFill(level, levels, isNormalized, highlighted, colors),
349
- ...selected ? {
350
- stroke: "var(--foreground)",
351
- strokeWidth: 2
352
- } : {},
370
+ fill: signedRange === null ? getLevelFill(level, levels, isNormalized, colors) : heat(activity.value, signedRange),
353
371
  ...styleProp
354
372
  },
355
373
  onClick: (event) => {
@@ -498,8 +516,14 @@ const CalendarHeatmapStat = ({ compute, label, className, children, ...props })
498
516
  children: template.replace("{{value}}", String(value)).replace("{{year}}", String(year))
499
517
  });
500
518
  };
501
- const CalendarHeatmapLegend = ({ labels: labelsProp, className, children, ...props }) => {
502
- const { levels, isNormalized, blockRadius, colors, labels } = useCalendarHeatmap();
519
+ const CalendarHeatmapLegend = ({ labels: labelsProp, valueFormatter, className, children, ...props }) => {
520
+ const { levels, isNormalized, blockRadius, colors, labels, signedRange } = useCalendarHeatmap();
521
+ if (signedRange !== null) return /* @__PURE__ */ jsx(SignedLegend, {
522
+ labels: labelsProp,
523
+ valueFormatter,
524
+ className,
525
+ ...props
526
+ });
503
527
  const lessLabel = labelsProp?.less ?? "Less";
504
528
  const moreLabel = labelsProp?.more ?? "More";
505
529
  const legendLevels = Array.from({ length: levels }, (_, i) => isNormalized ? i + 1 : i);
@@ -524,7 +548,7 @@ const CalendarHeatmapLegend = ({ labels: labelsProp, className, children, ...pro
524
548
  rx: blockRadius,
525
549
  ry: blockRadius,
526
550
  width: LEGEND_SWATCH,
527
- style: { fill: getLevelFill(level, levels, isNormalized, false, colors) }
551
+ style: { fill: getLevelFill(level, levels, isNormalized, colors) }
528
552
  })
529
553
  }, `legend-level-${level}`)),
530
554
  /* @__PURE__ */ jsx("span", {
@@ -534,5 +558,59 @@ const CalendarHeatmapLegend = ({ labels: labelsProp, className, children, ...pro
534
558
  ]
535
559
  });
536
560
  };
561
+ /** A signed heatmap's scale: round loss steps deepest first, no change, square gain steps, and what they span. */
562
+ const SignedLegend = ({ labels: labelsProp, valueFormatter = String, className, ...props }) => {
563
+ const { blockRadius, labels, signedRange } = useCalendarHeatmap();
564
+ const steps = Array.from({ length: 5 }, (_, i) => i + 1);
565
+ const swatch = (key, fill, value) => {
566
+ const { stroke, round } = signedMark(value);
567
+ const corner = round ? 11 / 2 : blockRadius;
568
+ return /* @__PURE__ */ jsx("svg", {
569
+ "aria-hidden": "true",
570
+ height: LEGEND_SWATCH,
571
+ width: LEGEND_SWATCH,
572
+ children: /* @__PURE__ */ jsx("rect", {
573
+ x: .5,
574
+ y: .5,
575
+ height: 11,
576
+ width: 11,
577
+ rx: corner,
578
+ ry: corner,
579
+ stroke,
580
+ style: { fill }
581
+ })
582
+ }, key);
583
+ };
584
+ return /* @__PURE__ */ jsxs("fieldset", {
585
+ "data-slot": "calendar-heatmap-legend",
586
+ "aria-label": labels.legendLabel ?? "Activity intensity legend",
587
+ className: cn("ml-auto flex flex-col items-end gap-1 text-muted-foreground", className),
588
+ ...props,
589
+ children: [/* @__PURE__ */ jsxs("div", {
590
+ className: "flex items-center gap-1",
591
+ children: [
592
+ /* @__PURE__ */ jsx("span", {
593
+ className: "mr-1 font-medium text-xs",
594
+ children: labelsProp?.less ?? "Loss"
595
+ }),
596
+ steps.toReversed().map((step) => swatch(`loss-${step}`, heatFill("negative", step), -1)),
597
+ swatch("none", heat(null), 0),
598
+ steps.map((step) => swatch(`gain-${step}`, heatFill("positive", step), 1)),
599
+ /* @__PURE__ */ jsx("span", {
600
+ className: "ml-1 font-medium text-xs",
601
+ children: labelsProp?.more ?? "Gain"
602
+ })
603
+ ]
604
+ }), /* @__PURE__ */ jsxs("p", {
605
+ "data-slot": "calendar-heatmap-legend-scale",
606
+ className: "text-xs",
607
+ children: [
608
+ "Deepest at ±",
609
+ valueFormatter(signedRange ?? 0),
610
+ ". Round days lost, square days gained; grey days had no change."
611
+ ]
612
+ })]
613
+ });
614
+ };
537
615
  //#endregion
538
616
  export { CalendarHeatmap, CalendarHeatmapBlock, CalendarHeatmapBody, CalendarHeatmapFooter, CalendarHeatmapLegend, CalendarHeatmapStat };