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

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 (88) hide show
  1. package/DESIGN.md +30 -0
  2. package/README.md +3 -3
  3. package/agents/examples/alert.tsx +4 -6
  4. package/agents/examples/bar-list-sides.tsx +11 -3
  5. package/agents/examples/button-states.tsx +23 -0
  6. package/agents/examples/callout.tsx +33 -0
  7. package/agents/examples/change-bar.tsx +104 -0
  8. package/agents/examples/chart-area.tsx +5 -4
  9. package/agents/examples/chart-bar-flows.tsx +3 -2
  10. package/agents/examples/chart-bar-sectors.tsx +2 -0
  11. package/agents/examples/data-table-compact.tsx +95 -0
  12. package/agents/examples/heatmap-calendar.tsx +17 -7
  13. package/agents/examples/heatmap-status.tsx +17 -11
  14. package/agents/examples/heatmap-weekday.tsx +33 -11
  15. package/agents/examples/input-states.tsx +33 -0
  16. package/agents/examples/metric-strip.tsx +9 -3
  17. package/agents/examples/overview-dashboard.tsx +18 -0
  18. package/agents/examples/page-hero.tsx +38 -0
  19. package/agents/examples/range-position-table.tsx +101 -0
  20. package/agents/examples/range-position.tsx +53 -0
  21. package/agents/examples/section-cards-select.tsx +8 -5
  22. package/agents/examples/surfaces.tsx +3 -2
  23. package/agents/examples/target-rows.tsx +70 -0
  24. package/agents/examples/treemap.tsx +2 -1
  25. package/agents/manifest.json +153 -3
  26. package/dist/components/registry/charts/area-chart-card.js +25 -7
  27. package/dist/components/registry/charts/bar-chart-card.d.ts +0 -1
  28. package/dist/components/registry/charts/bar-chart-card.js +11 -4
  29. package/dist/components/registry/charts/line-chart-card.js +2 -2
  30. package/dist/components/registry/dashboardcn/bar-list.js +11 -5
  31. package/dist/components/registry/dashboardcn/kpi-row-card.js +2 -2
  32. package/dist/components/registry/dashboardcn/sparkline.d.ts +5 -1
  33. package/dist/components/registry/dashboardcn/sparkline.js +40 -18
  34. package/dist/components/registry/heatmap/calendar-heatmap.d.ts +4 -2
  35. package/dist/components/registry/heatmap/calendar-heatmap.js +67 -33
  36. package/dist/components/registry/heatmap/status-heatmap.d.ts +5 -3
  37. package/dist/components/registry/heatmap/status-heatmap.js +30 -15
  38. package/dist/components/registry/heatmap/weekday-heatmap.d.ts +4 -2
  39. package/dist/components/registry/heatmap/weekday-heatmap.js +28 -12
  40. package/dist/components/registry/niko-table/core/data-table-context.d.ts +16 -1
  41. package/dist/components/registry/niko-table/core/data-table-context.js +5 -20
  42. package/dist/components/registry/niko-table/core/data-table-root.js +39 -4
  43. package/dist/components/registry/niko-table/core/data-table-structure.js +2 -0
  44. package/dist/components/registry/niko-table/lib/styles.js +1 -1
  45. package/dist/components/registry/niko-table/types/index.d.ts +9 -0
  46. package/dist/components/registry/sidebar-07/nav-main.d.ts +1 -0
  47. package/dist/components/registry/sidebar-07/nav-main.js +9 -2
  48. package/dist/components/siyahi/callout.d.ts +24 -0
  49. package/dist/components/siyahi/callout.js +40 -0
  50. package/dist/components/siyahi/change-bar.d.ts +21 -0
  51. package/dist/components/siyahi/change-bar.js +35 -0
  52. package/dist/components/siyahi/data-table-compact-columns-toggle.d.ts +4 -0
  53. package/dist/components/siyahi/data-table-compact-columns-toggle.js +19 -0
  54. package/dist/components/siyahi/data-table-footnote.d.ts +12 -0
  55. package/dist/components/siyahi/data-table-footnote.js +35 -0
  56. package/dist/components/siyahi/default-sidebar-open.d.ts +8 -0
  57. package/dist/components/siyahi/default-sidebar-open.js +13 -0
  58. package/dist/components/siyahi/page-hero.d.ts +25 -0
  59. package/dist/components/siyahi/page-hero.js +41 -0
  60. package/dist/components/siyahi/page-toc.d.ts +20 -0
  61. package/dist/components/siyahi/page-toc.js +76 -0
  62. package/dist/components/siyahi/quote.d.ts +12 -0
  63. package/dist/components/siyahi/quote.js +23 -0
  64. package/dist/components/siyahi/range-position.d.ts +24 -0
  65. package/dist/components/siyahi/range-position.js +105 -0
  66. package/dist/components/siyahi/siyahi-provider.d.ts +1 -1
  67. package/dist/components/siyahi/siyahi-provider.js +1 -1
  68. package/dist/components/siyahi/target-rows.d.ts +34 -0
  69. package/dist/components/siyahi/target-rows.js +97 -0
  70. package/dist/components/siyahi/treemap.d.ts +4 -1
  71. package/dist/components/siyahi/treemap.js +33 -25
  72. package/dist/components/siyahi/warning-alert.d.ts +10 -0
  73. package/dist/components/siyahi/warning-alert.js +20 -0
  74. package/dist/index.d.ts +15 -4
  75. package/dist/index.js +12 -1
  76. package/dist/lib/chart-frame.d.ts +12 -2
  77. package/dist/lib/chart-frame.js +51 -7
  78. package/dist/lib/chart-series.d.ts +5 -2
  79. package/dist/lib/compact-columns.d.ts +19 -0
  80. package/dist/lib/compact-columns.js +55 -0
  81. package/dist/lib/format.d.ts +7 -2
  82. package/dist/lib/format.js +24 -5
  83. package/dist/lib/treemap.js +6 -1
  84. package/dist/lib/use-element-width.d.ts +5 -0
  85. package/dist/lib/use-element-width.js +15 -0
  86. package/package.json +2 -1
  87. package/report.css +26 -25
  88. package/styles/siyahi.css +159 -43
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { ChartFrame } from "../../../lib/chart-frame.js";
2
+ import { ChartFrame, endDot } from "../../../lib/chart-frame.js";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  import { Line, LineChart } from "recharts";
5
5
  //#region src/components/registry/charts/line-chart-card.tsx
@@ -13,7 +13,7 @@ function LineChartCard({ curve = "monotone", ...props }) {
13
13
  type: curve,
14
14
  stroke: color,
15
15
  strokeWidth: 2,
16
- dot: false
16
+ dot: endDot(color, props.data.length)
17
17
  }, key))
18
18
  });
19
19
  }
@@ -20,7 +20,7 @@ function BarList({ data, valueFormatter, sortOrder = "descending", color = "var(
20
20
  const total = items.reduce((sum, item) => sum + item.value, 0);
21
21
  return /* @__PURE__ */ jsx("div", {
22
22
  "data-slot": "bar-list",
23
- className: cn("@container/bar-list flex flex-col gap-1.5", className),
23
+ className: cn("@container/bar-list grid grid-cols-[minmax(0,1fr)_auto] gap-x-3 gap-y-1.5", className),
24
24
  ...props,
25
25
  children: items.map((item) => {
26
26
  const width = max > 0 ? item.value / max * 100 : 0;
@@ -29,16 +29,22 @@ function BarList({ data, valueFormatter, sortOrder = "descending", color = "var(
29
29
  const Label = item.href ? "a" : onItemClick ? "button" : "div";
30
30
  return /* @__PURE__ */ jsxs("div", {
31
31
  "data-slot": "bar-list-row",
32
- className: "flex items-center gap-3 text-sm @max-sm/bar-list:items-start",
32
+ className: "col-span-2 grid grid-cols-subgrid items-center text-sm @max-sm/bar-list:items-start",
33
33
  children: [/* @__PURE__ */ jsxs("div", {
34
34
  className: "relative flex h-8 min-w-0 flex-1 items-center @max-sm/bar-list:h-auto @max-sm/bar-list:min-h-8 @max-sm/bar-list:items-start @max-sm/bar-list:pb-3",
35
35
  children: [
36
36
  /* @__PURE__ */ jsx("div", {
37
+ "data-slot": "bar-list-track",
38
+ "aria-hidden": "true",
39
+ className: "bg-muted/60 absolute inset-0 rounded-md @max-sm/bar-list:inset-y-auto @max-sm/bar-list:bottom-0 @max-sm/bar-list:h-1.5"
40
+ }),
41
+ /* @__PURE__ */ jsx("div", {
42
+ "data-slot": "bar-list-bar",
37
43
  "aria-hidden": "true",
38
44
  className: "absolute inset-y-0 left-0 rounded-md transition-[width] duration-300 @max-sm/bar-list:inset-y-auto @max-sm/bar-list:bottom-0 @max-sm/bar-list:h-1.5",
39
45
  style: {
40
46
  width: `${width}%`,
41
- backgroundColor: `color-mix(in oklab, ${color} 20%, transparent)`
47
+ backgroundColor: item.detail ? "color-mix(in oklab, var(--muted-foreground) 20%, transparent)" : `color-mix(in oklab, ${color} 32%, transparent)`
42
48
  }
43
49
  }),
44
50
  /* @__PURE__ */ jsxs(Label, {
@@ -47,7 +53,7 @@ function BarList({ data, valueFormatter, sortOrder = "descending", color = "var(
47
53
  type: "button",
48
54
  onClick: () => onItemClick(item)
49
55
  } : {},
50
- className: cn("relative flex min-w-0 items-center gap-2 px-2 text-left", interactive && "hover:underline"),
56
+ className: cn("text-foreground relative flex min-w-0 items-center gap-2 px-2 text-left", interactive && "hover:underline"),
51
57
  children: [item.icon ? /* @__PURE__ */ jsx("span", {
52
58
  className: "text-muted-foreground shrink-0 [&>svg]:size-4",
53
59
  children: item.icon
@@ -63,7 +69,7 @@ function BarList({ data, valueFormatter, sortOrder = "descending", color = "var(
63
69
  }) : null
64
70
  ]
65
71
  }), /* @__PURE__ */ jsxs("div", {
66
- className: "flex shrink-0 items-center gap-2 tabular-nums",
72
+ className: "flex items-center justify-end gap-2 tabular-nums",
67
73
  children: [/* @__PURE__ */ jsx("span", {
68
74
  className: "font-medium",
69
75
  children: format(item.value)
@@ -39,7 +39,7 @@ function MetricStrip({ title, description, action, items, footer, className, ...
39
39
  className: "-mt-px -ml-px flex flex-wrap",
40
40
  children: items.map((item) => /* @__PURE__ */ jsxs("div", {
41
41
  "data-slot": "metric-strip-item",
42
- className: cn("flex min-w-0 flex-col gap-1 border-t border-l px-4 py-3", columns),
42
+ className: cn("flex min-w-0 flex-col gap-1.5 border-t border-l px-4 py-4", columns),
43
43
  children: [/* @__PURE__ */ jsx("div", {
44
44
  className: "truncate text-xs text-muted-foreground",
45
45
  children: item.label
@@ -64,7 +64,7 @@ function MetricStrip({ title, description, action, items, footer, className, ...
64
64
  data: item.trend,
65
65
  variant: "line",
66
66
  color: trendColor(item.change),
67
- className: "h-8 w-16 shrink-0"
67
+ className: "h-9 w-20 shrink-0"
68
68
  })]
69
69
  })]
70
70
  }, item.label))
@@ -5,9 +5,13 @@ export interface SparklineProps extends React$1.ComponentProps<"div"> {
5
5
  /** Series to plot, oldest first. */
6
6
  data: number[];
7
7
  variant?: "area" | "line";
8
- /** A chart slot, or var(--positive) or var(--negative) for a gain or a loss. Defaults to var(--chart-1). */
8
+ /**
9
+ * The last step and the dot on the latest value: a chart slot, or var(--positive) or var(--negative) for a gain or
10
+ * a loss. Defaults to var(--chart-1). The run before it is muted.
11
+ */
9
12
  color?: ChartColor;
10
13
  curve?: "monotone" | "linear" | "step";
14
+ /** The run's line; the last step is 2px. */
11
15
  strokeWidth?: number;
12
16
  /** Animate after hydration and when data changes. Respects reduced motion. */
13
17
  animate?: boolean;
@@ -15,24 +15,41 @@ function getReducedMotionSnapshot() {
15
15
  function getReducedMotionServerSnapshot() {
16
16
  return true;
17
17
  }
18
- function Sparkline({ data, variant = "area", color = "var(--chart-1)", curve = "monotone", strokeWidth = 1.5, animate = true, animationDuration = 500, className, ...props }) {
18
+ function Sparkline({ data, variant = "area", color = "var(--chart-1)", curve = "linear", strokeWidth = 1.25, animate = true, animationDuration = 500, className, ...props }) {
19
19
  const id = React$1.useId();
20
20
  const reducedMotion = React$1.useSyncExternalStore(subscribeToReducedMotion, getReducedMotionSnapshot, getReducedMotionServerSnapshot);
21
21
  const animationActive = animate && !reducedMotion;
22
22
  const points = React$1.useMemo(() => data.map((y, i) => ({
23
23
  i,
24
- y
24
+ y,
25
+ now: i >= data.length - 2 ? y : void 0
25
26
  })), [data]);
26
27
  const margin = {
27
- top: 2,
28
- right: 0,
29
- bottom: 2,
28
+ top: 3,
29
+ right: 3,
30
+ bottom: 3,
30
31
  left: 0
31
32
  };
32
33
  const axis = /* @__PURE__ */ jsx(YAxis, {
33
34
  hide: true,
34
35
  domain: ["dataMin", "dataMax"]
35
36
  });
37
+ const now = /* @__PURE__ */ jsx(Line, {
38
+ className: "sparkline-now",
39
+ type: curve,
40
+ dataKey: "now",
41
+ stroke: color,
42
+ strokeWidth: 2,
43
+ dot: (dot) => dot.index === data.length - 1 ? /* @__PURE__ */ jsx("circle", {
44
+ cx: dot.cx,
45
+ cy: dot.cy,
46
+ r: 3,
47
+ fill: color
48
+ }, "now") : /* @__PURE__ */ jsx("g", {}, dot.index),
49
+ isAnimationActive: animationActive,
50
+ animationBegin: animationDuration,
51
+ animationDuration: 150
52
+ });
36
53
  return /* @__PURE__ */ jsx("div", {
37
54
  "data-slot": "sparkline",
38
55
  "aria-hidden": "true",
@@ -44,16 +61,20 @@ function Sparkline({ data, variant = "area", color = "var(--chart-1)", curve = "
44
61
  children: variant === "line" ? /* @__PURE__ */ jsxs(LineChart, {
45
62
  data: points,
46
63
  margin,
47
- children: [axis, /* @__PURE__ */ jsx(Line, {
48
- type: curve,
49
- dataKey: "y",
50
- stroke: color,
51
- strokeWidth,
52
- dot: false,
53
- isAnimationActive: animationActive,
54
- animationDuration,
55
- animationEasing: "ease-out"
56
- })]
64
+ children: [
65
+ axis,
66
+ /* @__PURE__ */ jsx(Line, {
67
+ type: curve,
68
+ dataKey: "y",
69
+ stroke: "var(--muted-foreground)",
70
+ strokeWidth,
71
+ dot: false,
72
+ isAnimationActive: animationActive,
73
+ animationDuration,
74
+ animationEasing: "ease-out"
75
+ }),
76
+ now
77
+ ]
57
78
  }) : /* @__PURE__ */ jsxs(AreaChart, {
58
79
  data: points,
59
80
  margin,
@@ -68,7 +89,7 @@ function Sparkline({ data, variant = "area", color = "var(--chart-1)", curve = "
68
89
  children: [/* @__PURE__ */ jsx("stop", {
69
90
  offset: "0%",
70
91
  stopColor: color,
71
- stopOpacity: .3
92
+ stopOpacity: .18
72
93
  }), /* @__PURE__ */ jsx("stop", {
73
94
  offset: "100%",
74
95
  stopColor: color,
@@ -78,14 +99,15 @@ function Sparkline({ data, variant = "area", color = "var(--chart-1)", curve = "
78
99
  /* @__PURE__ */ jsx(Area, {
79
100
  type: curve,
80
101
  dataKey: "y",
81
- stroke: color,
102
+ stroke: "var(--muted-foreground)",
82
103
  strokeWidth,
83
104
  fill: `url(#${id})`,
84
105
  dot: false,
85
106
  isAnimationActive: animationActive,
86
107
  animationDuration,
87
108
  animationEasing: "ease-out"
88
- })
109
+ }),
110
+ now
89
111
  ]
90
112
  })
91
113
  })
@@ -75,8 +75,10 @@ export type CalendarHeatmapProps = HTMLAttributes<HTMLDivElement> & {
75
75
  * @param levels - Total number of legend cells (including empty when not normalized). Default: 5
76
76
  * @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
77
  */
78
- export declare const CalendarHeatmap: ({ data, weekStart, continuousMonths, hasEmptyColumn, blockSize, 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;
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;
79
79
  export type CalendarHeatmapBlockProps = HTMLAttributes<SVGRectElement> & {
80
+ /** The cell the page has picked: drawn with a ring and announced as pressed. */
81
+ selected?: boolean;
80
82
  activity: ActivityWithLevel;
81
83
  dayIndex: number;
82
84
  weekIndex: number;
@@ -84,7 +86,7 @@ export type CalendarHeatmapBlockProps = HTMLAttributes<SVGRectElement> & {
84
86
  onCellClick?: (activity: ActivityWithLevel) => void;
85
87
  onCellHover?: (activity: ActivityWithLevel | null) => void;
86
88
  };
87
- export declare function CalendarHeatmapBlock({ ref, activity, dayIndex, weekIndex, highlighted, onCellClick, onCellHover, onClick, onKeyDown, onMouseEnter, onMouseLeave, className, style: styleProp, ...props }: CalendarHeatmapBlockProps & {
89
+ export declare function CalendarHeatmapBlock({ ref, activity, dayIndex, weekIndex, highlighted, selected, onCellClick, onCellHover, onClick, onKeyDown, onMouseEnter, onMouseLeave, className, style: styleProp, ...props }: CalendarHeatmapBlockProps & {
88
90
  ref?: React.RefObject<SVGRectElement | null>;
89
91
  }): import("react").JSX.Element;
90
92
  export declare namespace CalendarHeatmapBlock {
@@ -1,7 +1,8 @@
1
1
  import { cn } from "../../../lib/utils.js";
2
2
  import { useHeatmapNavigation } from "../../../lib/heatmap-navigation.js";
3
+ import { useElementWidth } from "../../../lib/use-element-width.js";
3
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
- import { Fragment as Fragment$1, createContext, use, useMemo } from "react";
5
+ import { Fragment as Fragment$1, createContext, use, useLayoutEffect, useMemo } from "react";
5
6
  import { differenceInCalendarDays, eachDayOfInterval, format, formatISO, getDay, getMonth, getYear, nextDay, parseISO, subWeeks } from "date-fns";
6
7
  //#region src/components/registry/heatmap/calendar-heatmap.tsx
7
8
  const colorStepCount = (levels, isNormalized) => Math.max(1, isNormalized ? levels : levels - 1);
@@ -41,6 +42,12 @@ const generateWeekdayLabels = (locale) => {
41
42
  };
42
43
  const EMPTY_STYLE = {};
43
44
  const LABEL_MARGIN = 8;
45
+ const WEEKDAY_LABEL_WIDTH = 40;
46
+ const STROKE_PADDING = 3;
47
+ const LEGEND_SWATCH = 12;
48
+ const MIN_FITTED_BLOCK = 10;
49
+ const MAX_FITTED_BLOCK = 20;
50
+ const DEFAULT_BLOCK = 12;
44
51
  const CalendarHeatmapContext = createContext(null);
45
52
  const useCalendarHeatmap = () => {
46
53
  const context = use(CalendarHeatmapContext);
@@ -132,26 +139,18 @@ const groupContinuous = (activities, weekStart = 0) => {
132
139
  const padded = [...new Array(leadingPad).fill(void 0), ...normalizedActivities];
133
140
  const numberOfWeeks = Math.ceil(padded.length / 7);
134
141
  const allWeeks = new Array(numberOfWeeks).fill(void 0).map((_, i) => padded.slice(i * 7, i * 7 + 7));
135
- const yearMap = /* @__PURE__ */ new Map();
136
- for (const week of allWeeks) {
137
- const firstActivity = week.find((a) => a !== void 0);
138
- const year = firstActivity ? getYear(parseISO(firstActivity.date)) : void 0;
139
- if (year !== void 0) {
140
- if (!yearMap.has(year)) yearMap.set(year, []);
141
- yearMap.get(year)?.push(week);
142
- }
143
- }
144
- return Array.from(yearMap.entries()).sort(([a], [b]) => a - b).map(([year, weeks]) => ({
145
- year,
142
+ return [{
143
+ year: getYear(firstDate),
146
144
  startMonth: 0,
147
- weeks
148
- }));
145
+ weeks: allWeeks
146
+ }];
149
147
  };
150
- const getMonthLabels = (weeks, monthNames = generateMonthLabels()) => {
151
- return weeks.reduce((labels, week, weekIndex) => {
148
+ const getMonthLabels = (weeks, monthNames = generateMonthLabels(), yearTicks = false) => {
149
+ const ticks = weeks.reduce((labels, week, weekIndex) => {
152
150
  const firstActivity = week.find((activity) => activity !== void 0);
153
151
  if (!firstActivity) return labels;
154
- const month = monthNames[getMonth(parseISO(firstActivity.date))];
152
+ const date = parseISO(firstActivity.date);
153
+ const month = monthNames[getMonth(date)];
155
154
  if (!month) {
156
155
  const monthName = new Date(firstActivity.date).toLocaleString("en-US", { month: "short" });
157
156
  throw new Error(`Unexpected error: undefined month label for ${monthName}.`);
@@ -159,7 +158,8 @@ const getMonthLabels = (weeks, monthNames = generateMonthLabels()) => {
159
158
  const prevLabel = labels.at(-1);
160
159
  if (weekIndex === 0 || !prevLabel || prevLabel.label !== month) return labels.concat({
161
160
  weekIndex,
162
- label: month
161
+ label: month,
162
+ year: getMonth(date) === 0 ? getYear(date) : void 0
163
163
  });
164
164
  return labels;
165
165
  }, []).filter(({ weekIndex }, index, labels) => {
@@ -168,6 +168,22 @@ const getMonthLabels = (weeks, monthNames = generateMonthLabels()) => {
168
168
  if (index === labels.length - 1) return weeks.slice(weekIndex).length >= minWeeks;
169
169
  return true;
170
170
  });
171
+ if (!yearTicks) return ticks;
172
+ const hasJanuary = ticks.some(({ year }) => year !== void 0);
173
+ return ticks.map((tick, index) => {
174
+ if (tick.year !== void 0) return {
175
+ ...tick,
176
+ label: String(tick.year)
177
+ };
178
+ if (index === 0 && !hasJanuary) {
179
+ const week = weeks[tick.weekIndex]?.find((activity) => activity !== void 0);
180
+ return week ? {
181
+ ...tick,
182
+ label: `${tick.label} ${getYear(parseISO(week.date))}`
183
+ } : tick;
184
+ }
185
+ return tick;
186
+ });
171
187
  };
172
188
  /**
173
189
  * Calendar Heatmap
@@ -202,8 +218,9 @@ const getMonthLabels = (weeks, monthNames = generateMonthLabels()) => {
202
218
  * @param levels - Total number of legend cells (including empty when not normalized). Default: 5
203
219
  * @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.
204
220
  */
205
- const CalendarHeatmap = ({ data, weekStart = 0, continuousMonths = true, hasEmptyColumn = false, blockSize = 12, 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 }) => {
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 }) => {
206
222
  const levels = Math.max(1, levelsProp);
223
+ const [rootRef, rootWidth] = useElementWidth();
207
224
  const dataWithLevels = useMemo(() => {
208
225
  if (data.length === 0) return [];
209
226
  const maxCount = data.reduce((max, d) => Math.max(max, d.value), 1);
@@ -236,6 +253,8 @@ const CalendarHeatmap = ({ data, weekStart = 0, continuousMonths = true, hasEmpt
236
253
  const labelHeight = fontSize + LABEL_MARGIN;
237
254
  const year = data.length > 0 ? getYear(parseISO(data[0].date)) : (/* @__PURE__ */ new Date()).getFullYear();
238
255
  const totalCount = typeof totalCountProp === "number" ? totalCountProp : dataWithLevels.reduce((sum, activity) => sum + activity.value, 0);
256
+ const fitted = (rootWidth - WEEKDAY_LABEL_WIDTH - 6 + blockMargin) / Math.max(weeks.length, 1) - blockMargin;
257
+ const blockSize = blockSizeProp ?? (rootWidth > 0 ? Math.min(MAX_FITTED_BLOCK, Math.max(MIN_FITTED_BLOCK, fitted)) / blockAspectRatio : DEFAULT_BLOCK);
239
258
  const blockWidth = blockSize * blockAspectRatio;
240
259
  const width = weeks.length * (blockWidth + blockMargin) - blockMargin;
241
260
  const height = labelHeight + (blockSize + blockMargin) * 7 - blockMargin;
@@ -290,8 +309,9 @@ const CalendarHeatmap = ({ data, weekStart = 0, continuousMonths = true, hasEmpt
290
309
  return /* @__PURE__ */ jsx(CalendarHeatmapContext, {
291
310
  value: contextValue,
292
311
  children: /* @__PURE__ */ jsx("div", {
312
+ ref: rootRef,
293
313
  "data-slot": "calendar-heatmap",
294
- className: cn("flex w-max max-w-full flex-col gap-2", className),
314
+ className: cn("flex flex-col gap-2", blockSizeProp === void 0 ? "w-full" : "w-max max-w-full", className),
295
315
  style: {
296
316
  fontSize,
297
317
  ...style
@@ -301,7 +321,7 @@ const CalendarHeatmap = ({ data, weekStart = 0, continuousMonths = true, hasEmpt
301
321
  })
302
322
  });
303
323
  };
304
- const CalendarHeatmapBlock = ({ ref, activity, dayIndex, weekIndex, highlighted = false, onCellClick, onCellHover, onClick, onKeyDown, onMouseEnter, onMouseLeave, className, style: styleProp, ...props }) => {
324
+ const CalendarHeatmapBlock = ({ ref, activity, dayIndex, weekIndex, highlighted = false, selected = false, onCellClick, onCellHover, onClick, onKeyDown, onMouseEnter, onMouseLeave, className, style: styleProp, ...props }) => {
305
325
  const { blockSize, blockWidth, blockMargin, blockRadius, labelHeight, labels, levels, isNormalized, colors } = useCalendarHeatmap();
306
326
  const level = Math.max(0, Math.min(colorStepCount(levels, isNormalized), activity.level));
307
327
  const ariaLabel = (labels.cellLabel ?? "{{date}}: {{value}} contributions").replace("{{date}}", activity.date).replace("{{value}}", String(activity.value));
@@ -309,6 +329,7 @@ const CalendarHeatmapBlock = ({ ref, activity, dayIndex, weekIndex, highlighted
309
329
  ref,
310
330
  "data-slot": "calendar-heatmap-block",
311
331
  role: onCellClick ? "button" : "img",
332
+ "aria-pressed": onCellClick ? selected : void 0,
312
333
  tabIndex: onCellClick ? 0 : -1,
313
334
  "aria-label": ariaLabel,
314
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),
@@ -316,6 +337,7 @@ const CalendarHeatmapBlock = ({ ref, activity, dayIndex, weekIndex, highlighted
316
337
  "data-date": activity.date,
317
338
  "data-level": level,
318
339
  "data-highlighted": highlighted || void 0,
340
+ "data-selected": selected || void 0,
319
341
  height: blockSize,
320
342
  rx: blockRadius,
321
343
  ry: blockRadius,
@@ -324,6 +346,10 @@ const CalendarHeatmapBlock = ({ ref, activity, dayIndex, weekIndex, highlighted
324
346
  y: labelHeight + (blockSize + blockMargin) * dayIndex,
325
347
  style: {
326
348
  fill: getLevelFill(level, levels, isNormalized, highlighted, colors),
349
+ ...selected ? {
350
+ stroke: "var(--foreground)",
351
+ strokeWidth: 2
352
+ } : {},
327
353
  ...styleProp
328
354
  },
329
355
  onClick: (event) => {
@@ -351,17 +377,17 @@ const CalendarHeatmapBlock = ({ ref, activity, dayIndex, weekIndex, highlighted
351
377
  CalendarHeatmapBlock.displayName = "CalendarHeatmapBlock";
352
378
  const CalendarHeatmapBody = ({ hideMonthLabels = false, hideWeekdayLabels = false, hideYearLabels = false, className, labelClassName, yearClassName, children, renderYearFooter, ...props }) => {
353
379
  const navigation = useHeatmapNavigation();
354
- const { yearRows, blockSize, blockWidth, blockMargin, labels, labelHeight, fontSize, weekStart, data } = useCalendarHeatmap();
355
- const weekdayLabelWidth = hideWeekdayLabels ? 0 : 40;
356
- const strokePadding = 3;
380
+ const { yearRows, blockSize, blockWidth, blockMargin, labels, labelHeight, fontSize, weekStart, continuousMonths } = useCalendarHeatmap();
381
+ const weekdayLabelWidth = hideWeekdayLabels ? 0 : WEEKDAY_LABEL_WIDTH;
382
+ const strokePadding = STROKE_PADDING;
357
383
  const rowData = useMemo(() => {
358
384
  return yearRows.map((yearRow) => {
359
385
  return {
360
386
  yearRow,
361
387
  width: yearRow.weeks.length * (blockWidth + blockMargin) - blockMargin,
362
388
  height: labelHeight + (blockSize + blockMargin) * 7 - blockMargin,
363
- monthLabels: getMonthLabels(yearRow.weeks, labels.months),
364
- yearTotalCount: data.filter((activity) => getYear(parseISO(activity.date)) === yearRow.year).reduce((sum, activity) => sum + activity.value, 0)
389
+ monthLabels: getMonthLabels(yearRow.weeks, labels.months, continuousMonths),
390
+ yearTotalCount: yearRow.weeks.flat().reduce((sum, activity) => sum + (activity?.value ?? 0), 0)
365
391
  };
366
392
  });
367
393
  }, [
@@ -371,9 +397,17 @@ const CalendarHeatmapBody = ({ hideMonthLabels = false, hideWeekdayLabels = fals
371
397
  blockMargin,
372
398
  labelHeight,
373
399
  labels.months,
374
- data
400
+ continuousMonths
375
401
  ]);
376
402
  const totalWidth = weekdayLabelWidth + Math.max(...rowData.map((r) => r.width)) + 6;
403
+ const scroller = navigation.ref;
404
+ useLayoutEffect(() => {
405
+ if (continuousMonths && scroller.current) scroller.current.scrollLeft = scroller.current.scrollWidth;
406
+ }, [
407
+ continuousMonths,
408
+ scroller,
409
+ totalWidth
410
+ ]);
377
411
  return /* @__PURE__ */ jsx("div", {
378
412
  "data-slot": "calendar-heatmap-body",
379
413
  ref: navigation.ref,
@@ -384,7 +418,7 @@ const CalendarHeatmapBody = ({ hideMonthLabels = false, hideWeekdayLabels = fals
384
418
  if (!event.defaultPrevented) navigation.onKeyDown(event);
385
419
  },
386
420
  children: rowData.map(({ yearRow, height, monthLabels, yearTotalCount }) => /* @__PURE__ */ jsxs("div", { children: [
387
- !hideYearLabels && /* @__PURE__ */ jsx("div", {
421
+ !hideYearLabels && !continuousMonths && /* @__PURE__ */ jsx("div", {
388
422
  className: cn("mb-2 text-muted-foreground", yearClassName),
389
423
  children: yearRow.year
390
424
  }),
@@ -465,7 +499,7 @@ const CalendarHeatmapStat = ({ compute, label, className, children, ...props })
465
499
  });
466
500
  };
467
501
  const CalendarHeatmapLegend = ({ labels: labelsProp, className, children, ...props }) => {
468
- const { levels, isNormalized, blockSize, blockWidth, blockRadius, colors, labels } = useCalendarHeatmap();
502
+ const { levels, isNormalized, blockRadius, colors, labels } = useCalendarHeatmap();
469
503
  const lessLabel = labelsProp?.less ?? "Less";
470
504
  const moreLabel = labelsProp?.more ?? "More";
471
505
  const legendLevels = Array.from({ length: levels }, (_, i) => isNormalized ? i + 1 : i);
@@ -482,14 +516,14 @@ const CalendarHeatmapLegend = ({ labels: labelsProp, className, children, ...pro
482
516
  legendLevels.map((level) => children ? /* @__PURE__ */ jsx(Fragment$1, { children: children({ level }) }, `legend-level-${level}`) : /* @__PURE__ */ jsx("svg", {
483
517
  role: "img",
484
518
  "aria-label": (labels.legendLevelLabel ?? "{{level}} contributions").replace("{{level}}", String(level)),
485
- height: blockSize,
486
- width: blockWidth,
519
+ height: LEGEND_SWATCH,
520
+ width: LEGEND_SWATCH,
487
521
  children: /* @__PURE__ */ jsx("rect", {
488
522
  "data-level": level,
489
- height: blockSize,
523
+ height: LEGEND_SWATCH,
490
524
  rx: blockRadius,
491
525
  ry: blockRadius,
492
- width: blockWidth,
526
+ width: LEGEND_SWATCH,
493
527
  style: { fill: getLevelFill(level, levels, isNormalized, false, colors) }
494
528
  })
495
529
  }, `legend-level-${level}`)),
@@ -69,11 +69,13 @@ export type StatusHeatmapProps = HTMLAttributes<HTMLDivElement> & {
69
69
  *
70
70
  * @param data - Array of activities with date (YYYY-MM-DD) and status value (0-3)
71
71
  * @param dateFormat - Date format string for tooltips. Default: "MMM d"
72
- * @param blockAspectRatio - Width/height ratio of blocks. Default: 0.2 (narrow vertical bars)
72
+ * @param blockAspectRatio - Width/height ratio of blocks. Default: bars share the full width
73
73
  * @param colors - Custom colors for critical, degraded, and healthy states
74
74
  */
75
- export declare const StatusHeatmap: ({ data, dateFormat, blockSize, blockMargin, blockRadius, blockAspectRatio, colors, locale, labels: labelsProp, fontSize, emptyState, statusValues, healthyValue, style, className, children, ...props }: StatusHeatmapProps) => 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;
75
+ export declare const StatusHeatmap: ({ data, dateFormat, blockSize, blockMargin, blockRadius, blockAspectRatio: blockAspectRatioProp, colors, locale, labels: labelsProp, fontSize, emptyState, statusValues, healthyValue, style, className, children, ...props }: StatusHeatmapProps) => 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;
76
76
  export type StatusHeatmapBlockProps = HTMLAttributes<SVGRectElement> & {
77
+ /** The cell the page has picked: drawn with a ring and announced as pressed. */
78
+ selected?: boolean;
77
79
  activity: StatusActivity;
78
80
  dayIndex: number;
79
81
  highlighted?: boolean;
@@ -81,7 +83,7 @@ export type StatusHeatmapBlockProps = HTMLAttributes<SVGRectElement> & {
81
83
  onCellHover?: (activity: StatusActivity | null) => void;
82
84
  className?: string;
83
85
  };
84
- export declare function StatusHeatmapBlock({ ref, activity, dayIndex, highlighted, onCellClick, onCellHover, onClick, onKeyDown, onMouseEnter, onMouseLeave, className, style: styleProp, ...props }: StatusHeatmapBlockProps & {
86
+ export declare function StatusHeatmapBlock({ ref, activity, dayIndex, highlighted, selected, onCellClick, onCellHover, onClick, onKeyDown, onMouseEnter, onMouseLeave, className, style: styleProp, ...props }: StatusHeatmapBlockProps & {
85
87
  ref?: React.RefObject<SVGRectElement | null>;
86
88
  }): import("react").JSX.Element;
87
89
  export declare namespace StatusHeatmapBlock {
@@ -1,14 +1,15 @@
1
1
  import { cn } from "../../../lib/utils.js";
2
2
  import { useHeatmapNavigation } from "../../../lib/heatmap-navigation.js";
3
+ import { useElementWidth } from "../../../lib/use-element-width.js";
3
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
5
  import { Fragment as Fragment$1, createContext, use, useMemo } from "react";
5
6
  import { format } from "date-fns";
6
7
  //#region src/components/registry/heatmap/status-heatmap.tsx
7
8
  const DEFAULT_COLORS = {
8
9
  0: "var(--color-secondary)",
9
- 1: "var(--negative)",
10
+ 1: "var(--destructive)",
10
11
  2: "var(--warning)",
11
- 3: "var(--positive)"
12
+ 3: "color-mix(in oklab, var(--muted-foreground) 40%, transparent)"
12
13
  };
13
14
  const SEMANTIC_KEYS = {
14
15
  0: "noData",
@@ -52,6 +53,9 @@ const getStatusFill = (value, colors, highlighted = false) => {
52
53
  return `color-mix(in oklch, ${base} 60%, transparent)`;
53
54
  };
54
55
  const EMPTY_STYLE = {};
56
+ const LEGEND_SWATCH = 10;
57
+ const MIN_FITTED_BAR = 2;
58
+ const DEFAULT_ASPECT_RATIO = .2;
55
59
  const StatusHeatmapContext = createContext(null);
56
60
  const useStatusHeatmap = () => {
57
61
  const context = use(StatusHeatmapContext);
@@ -85,15 +89,16 @@ const useStatusHeatmap = () => {
85
89
  *
86
90
  * @param data - Array of activities with date (YYYY-MM-DD) and status value (0-3)
87
91
  * @param dateFormat - Date format string for tooltips. Default: "MMM d"
88
- * @param blockAspectRatio - Width/height ratio of blocks. Default: 0.2 (narrow vertical bars)
92
+ * @param blockAspectRatio - Width/height ratio of blocks. Default: bars share the full width
89
93
  * @param colors - Custom colors for critical, degraded, and healthy states
90
94
  */
91
- const StatusHeatmap = ({ data, dateFormat = "MMM d", blockSize = 40, blockMargin = 2, blockRadius = 2, blockAspectRatio = .2, colors, locale, labels: labelsProp, fontSize = 14, emptyState, statusValues = [
95
+ const StatusHeatmap = ({ data, dateFormat = "MMM d", blockSize = 40, blockMargin = 2, blockRadius = 2, blockAspectRatio: blockAspectRatioProp, colors, locale, labels: labelsProp, fontSize = 14, emptyState, statusValues = [
92
96
  0,
93
97
  1,
94
98
  2,
95
99
  3
96
100
  ], healthyValue = 3, style = EMPTY_STYLE, className, children, ...props }) => {
101
+ const [rootRef, rootWidth] = useElementWidth();
97
102
  const labels = useMemo(() => ({
98
103
  statuses: {
99
104
  noData: "No Data",
@@ -113,7 +118,10 @@ const StatusHeatmap = ({ data, dateFormat = "MMM d", blockSize = 40, blockMargin
113
118
  });
114
119
  return Array.from(uniqueDates).sort();
115
120
  }, [data]);
116
- const blockWidth = blockSize * blockAspectRatio;
121
+ const fits = blockAspectRatioProp === void 0;
122
+ const fitted = (rootWidth - 6 + blockMargin) / Math.max(dates.length, 1) - blockMargin;
123
+ const blockWidth = fits && rootWidth > 0 ? Math.max(MIN_FITTED_BAR, fitted) : blockSize * (blockAspectRatioProp ?? DEFAULT_ASPECT_RATIO);
124
+ const blockAspectRatio = blockWidth / blockSize;
117
125
  const width = dates.length * (blockWidth + blockMargin) - blockMargin;
118
126
  const height = blockSize;
119
127
  const contextValue = useMemo(() => ({
@@ -155,8 +163,9 @@ const StatusHeatmap = ({ data, dateFormat = "MMM d", blockSize = 40, blockMargin
155
163
  return /* @__PURE__ */ jsx(StatusHeatmapContext, {
156
164
  value: contextValue,
157
165
  children: /* @__PURE__ */ jsx("div", {
166
+ ref: rootRef,
158
167
  "data-slot": "status-heatmap",
159
- className: cn("flex w-max max-w-full flex-col gap-2", className),
168
+ className: cn("flex flex-col gap-2", fits ? "w-full" : "w-max max-w-full", className),
160
169
  style: {
161
170
  fontSize,
162
171
  ...style
@@ -166,7 +175,7 @@ const StatusHeatmap = ({ data, dateFormat = "MMM d", blockSize = 40, blockMargin
166
175
  })
167
176
  });
168
177
  };
169
- const StatusHeatmapBlock = ({ ref, activity, dayIndex, highlighted = false, onCellClick, onCellHover, onClick, onKeyDown, onMouseEnter, onMouseLeave, className, style: styleProp, ...props }) => {
178
+ const StatusHeatmapBlock = ({ ref, activity, dayIndex, highlighted = false, selected = false, onCellClick, onCellHover, onClick, onKeyDown, onMouseEnter, onMouseLeave, className, style: styleProp, ...props }) => {
170
179
  const { blockSize, blockWidth, blockMargin, blockRadius, labels, colors } = useStatusHeatmap();
171
180
  const statusText = resolveStatusLabel(activity.value, labels.statuses);
172
181
  const ariaLabel = (labels.cellLabel ?? "{{date}}: {{status}}").replace("{{date}}", activity.date).replace("{{status}}", statusText);
@@ -175,12 +184,14 @@ const StatusHeatmapBlock = ({ ref, activity, dayIndex, highlighted = false, onCe
175
184
  ref,
176
185
  "data-slot": "status-heatmap-block",
177
186
  role: onCellClick ? "button" : "img",
187
+ "aria-pressed": onCellClick ? selected : void 0,
178
188
  tabIndex: onCellClick ? 0 : -1,
179
189
  "aria-label": ariaLabel,
180
190
  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),
181
191
  "data-value": activity.value,
182
192
  "data-date": activity.date,
183
193
  "data-highlighted": highlighted || void 0,
194
+ "data-selected": selected || void 0,
184
195
  height: blockSize,
185
196
  rx: blockRadius,
186
197
  ry: blockRadius,
@@ -189,6 +200,10 @@ const StatusHeatmapBlock = ({ ref, activity, dayIndex, highlighted = false, onCe
189
200
  y: 0,
190
201
  style: {
191
202
  fill: getStatusFill(activity.value, colors, highlighted),
203
+ ...selected ? {
204
+ stroke: "var(--foreground)",
205
+ strokeWidth: 2
206
+ } : {},
192
207
  ...styleProp
193
208
  },
194
209
  onClick: (event) => {
@@ -247,9 +262,9 @@ const StatusHeatmapBody = ({ hideDateLabels = false, labelInterval = 30, classNa
247
262
  role: "img",
248
263
  "aria-label": labels.heatmapLabel ?? "Status heatmap",
249
264
  className: "block overflow-visible rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
250
- height: height + labelHeight + 40,
251
- viewBox: `-20 -20 ${width + 40} ${height + labelHeight + 40}`,
252
- width: width + 40,
265
+ height: height + labelHeight + 6,
266
+ viewBox: `-3 -3 ${width + 6} ${height + labelHeight + 6}`,
267
+ width: width + 6,
253
268
  children: [!hideDateLabels && /* @__PURE__ */ jsx("g", {
254
269
  className: cn("fill-current font-mono", labelClassName),
255
270
  children: dates.map((date, dayIndex) => {
@@ -299,7 +314,7 @@ const StatusHeatmapStat = ({ compute, label, className, children, ...props }) =>
299
314
  });
300
315
  };
301
316
  const StatusHeatmapLegend = ({ labels: labelsProp, className, children, ...props }) => {
302
- const { labels, statusValues, blockSize, blockWidth, blockRadius, colors } = useStatusHeatmap();
317
+ const { labels, statusValues, blockRadius, colors } = useStatusHeatmap();
303
318
  const statuses = statusValues.map((value) => ({
304
319
  value,
305
320
  label: resolveStatusLabel(value, labelsProp ?? labels.statuses)
@@ -316,14 +331,14 @@ const StatusHeatmapLegend = ({ labels: labelsProp, className, children, ...props
316
331
  className: "flex items-center gap-1",
317
332
  children: [/* @__PURE__ */ jsx("svg", {
318
333
  "aria-hidden": "true",
319
- height: blockSize / 2,
320
- width: blockWidth,
334
+ height: LEGEND_SWATCH,
335
+ width: LEGEND_SWATCH,
321
336
  children: /* @__PURE__ */ jsx("rect", {
322
337
  "data-value": status.value,
323
- height: blockSize / 2,
338
+ height: LEGEND_SWATCH,
324
339
  rx: blockRadius,
325
340
  ry: blockRadius,
326
- width: blockWidth,
341
+ width: LEGEND_SWATCH,
327
342
  style: { fill: getStatusFill(status.value, colors) }
328
343
  })
329
344
  }), /* @__PURE__ */ jsx("span", {