@siyahi/ui 0.1.9-next.2 → 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 (196) hide show
  1. package/DESIGN.md +30 -0
  2. package/README.md +82 -176
  3. package/THIRD_PARTY_NOTICES.md +11 -11
  4. package/agents/examples/accordion.tsx +39 -0
  5. package/agents/examples/alert.tsx +23 -0
  6. package/agents/examples/app-sidebar.tsx +38 -0
  7. package/agents/examples/avatar.tsx +14 -0
  8. package/agents/examples/badge.tsx +18 -0
  9. package/agents/examples/bar-list-labels.tsx +22 -0
  10. package/agents/examples/bar-list-sides.tsx +38 -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 +15 -0
  16. package/agents/examples/callout.tsx +33 -0
  17. package/agents/examples/card.tsx +34 -0
  18. package/agents/examples/change-bar.tsx +104 -0
  19. package/agents/examples/chart-area.tsx +39 -0
  20. package/agents/examples/chart-bar-flows.tsx +32 -0
  21. package/agents/examples/chart-bar-hourly.tsx +28 -0
  22. package/agents/examples/chart-bar-losses.tsx +29 -0
  23. package/agents/examples/chart-bar-sectors.tsx +72 -0
  24. package/agents/examples/chart-bar-years.tsx +40 -0
  25. package/agents/examples/chart-bar.tsx +56 -0
  26. package/agents/examples/chart-category-ticks.tsx +50 -0
  27. package/agents/examples/chart-line-events.tsx +47 -0
  28. package/agents/examples/chart-line-timeline.tsx +41 -0
  29. package/agents/examples/chart-line.tsx +57 -0
  30. package/agents/examples/code-block.tsx +38 -0
  31. package/agents/examples/collapsible.tsx +16 -0
  32. package/agents/examples/command.tsx +27 -0
  33. package/agents/examples/context-menu.tsx +15 -0
  34. package/agents/examples/copy-button.tsx +8 -0
  35. package/agents/examples/data-bar.tsx +121 -0
  36. package/agents/examples/data-table-compact.tsx +95 -0
  37. package/agents/examples/data-table-filters.tsx +103 -0
  38. package/agents/examples/data-table.tsx +98 -0
  39. package/agents/examples/delta.tsx +13 -0
  40. package/agents/examples/dialog.tsx +28 -0
  41. package/agents/examples/distribution-bar-budget.tsx +30 -0
  42. package/agents/examples/distribution-bar-tug.tsx +72 -0
  43. package/agents/examples/dropdown-menu.tsx +31 -0
  44. package/agents/examples/formatters.tsx +42 -0
  45. package/agents/examples/heatmap-calendar.tsx +60 -0
  46. package/agents/examples/heatmap-status.tsx +57 -0
  47. package/agents/examples/heatmap-weekday.tsx +75 -0
  48. package/agents/examples/input-states.tsx +33 -0
  49. package/agents/examples/input.tsx +17 -0
  50. package/agents/examples/local-time.tsx +14 -0
  51. package/agents/examples/metric-strip.tsx +46 -0
  52. package/agents/examples/overview-dashboard.tsx +18 -0
  53. package/agents/examples/page-hero.tsx +38 -0
  54. package/agents/examples/page-section.tsx +52 -0
  55. package/agents/examples/popover.tsx +23 -0
  56. package/agents/examples/price-chart.tsx +32 -0
  57. package/agents/examples/progress-tone.tsx +28 -0
  58. package/agents/examples/progress.tsx +10 -0
  59. package/agents/examples/range-position-table.tsx +101 -0
  60. package/agents/examples/range-position.tsx +53 -0
  61. package/agents/examples/scroll-area.tsx +20 -0
  62. package/agents/examples/section-cards-select.tsx +30 -0
  63. package/agents/examples/section-cards-trend.tsx +30 -0
  64. package/agents/examples/section-cards.tsx +48 -0
  65. package/agents/examples/select.tsx +27 -0
  66. package/agents/examples/separator.tsx +13 -0
  67. package/agents/examples/sheet.tsx +15 -0
  68. package/agents/examples/skeleton.tsx +11 -0
  69. package/agents/examples/slider.tsx +5 -0
  70. package/agents/examples/surfaces.tsx +116 -0
  71. package/agents/examples/table.tsx +50 -0
  72. package/agents/examples/tabs.tsx +16 -0
  73. package/agents/examples/target-rows.tsx +70 -0
  74. package/agents/examples/toggle-group.tsx +12 -0
  75. package/agents/examples/toggle.tsx +11 -0
  76. package/agents/examples/tooltip.tsx +13 -0
  77. package/agents/examples/treemap.tsx +88 -0
  78. package/agents/manifest.json +1081 -0
  79. package/agents/report-rules.json +7 -0
  80. package/dist/components/registry/charts/area-chart-card.js +25 -7
  81. package/dist/components/registry/charts/bar-chart-card.d.ts +0 -1
  82. package/dist/components/registry/charts/bar-chart-card.js +40 -26
  83. package/dist/components/registry/charts/line-chart-card.js +2 -2
  84. package/dist/components/registry/dashboard-01/section-cards.d.ts +2 -0
  85. package/dist/components/registry/dashboard-01/section-cards.js +42 -35
  86. package/dist/components/registry/dashboardcn/bar-list.js +18 -11
  87. package/dist/components/registry/dashboardcn/distribution-bar.js +1 -1
  88. package/dist/components/registry/dashboardcn/kpi-row-card.d.ts +29 -0
  89. package/dist/components/registry/dashboardcn/kpi-row-card.js +81 -0
  90. package/dist/components/registry/dashboardcn/sparkline.d.ts +22 -0
  91. package/dist/components/registry/dashboardcn/sparkline.js +117 -0
  92. package/dist/components/registry/heatmap/calendar-heatmap.d.ts +4 -2
  93. package/dist/components/registry/heatmap/calendar-heatmap.js +74 -33
  94. package/dist/components/registry/heatmap/status-heatmap.d.ts +5 -3
  95. package/dist/components/registry/heatmap/status-heatmap.js +37 -15
  96. package/dist/components/registry/heatmap/weekday-heatmap.d.ts +9 -3
  97. package/dist/components/registry/heatmap/weekday-heatmap.js +82 -47
  98. package/dist/components/registry/niko-table/core/data-table-context.d.ts +18 -1
  99. package/dist/components/registry/niko-table/core/data-table-context.js +7 -1
  100. package/dist/components/registry/niko-table/core/data-table-grouped-row.js +3 -1
  101. package/dist/components/registry/niko-table/core/data-table-root.js +39 -4
  102. package/dist/components/registry/niko-table/core/data-table-structure.js +17 -12
  103. package/dist/components/registry/niko-table/core/data-table.d.ts +2 -2
  104. package/dist/components/registry/niko-table/core/data-table.js +5 -1
  105. package/dist/components/registry/niko-table/filters/table-faceted-filter.js +4 -4
  106. package/dist/components/registry/niko-table/filters/table-search-filter.js +1 -1
  107. package/dist/components/registry/niko-table/lib/flex-columns.d.ts +1 -1
  108. package/dist/components/registry/niko-table/lib/flex-columns.js +3 -3
  109. package/dist/components/registry/niko-table/lib/styles.d.ts +1 -1
  110. package/dist/components/registry/niko-table/lib/styles.js +3 -3
  111. package/dist/components/registry/niko-table/types/index.d.ts +9 -0
  112. package/dist/components/registry/niko-table/ui/table.js +2 -2
  113. package/dist/components/registry/sidebar-07/app-sidebar.d.ts +1 -1
  114. package/dist/components/registry/sidebar-07/app-sidebar.js +3 -1
  115. package/dist/components/registry/sidebar-07/nav-main.d.ts +2 -1
  116. package/dist/components/registry/sidebar-07/nav-main.js +16 -3
  117. package/dist/components/siyahi/callout.d.ts +24 -0
  118. package/dist/components/siyahi/callout.js +40 -0
  119. package/dist/components/siyahi/change-bar.d.ts +21 -0
  120. package/dist/components/siyahi/change-bar.js +35 -0
  121. package/dist/components/siyahi/data-bar.d.ts +19 -0
  122. package/dist/components/siyahi/data-bar.js +29 -0
  123. package/dist/components/siyahi/data-table-compact-columns-toggle.d.ts +4 -0
  124. package/dist/components/siyahi/data-table-compact-columns-toggle.js +19 -0
  125. package/dist/components/siyahi/data-table-footnote.d.ts +12 -0
  126. package/dist/components/siyahi/data-table-footnote.js +35 -0
  127. package/dist/components/siyahi/default-sidebar-open.d.ts +8 -0
  128. package/dist/components/siyahi/default-sidebar-open.js +13 -0
  129. package/dist/components/siyahi/page-hero.d.ts +25 -0
  130. package/dist/components/siyahi/page-hero.js +41 -0
  131. package/dist/components/siyahi/page-section.d.ts +4 -1
  132. package/dist/components/siyahi/page-section.js +6 -3
  133. package/dist/components/siyahi/page-toc.d.ts +20 -0
  134. package/dist/components/siyahi/page-toc.js +76 -0
  135. package/dist/components/siyahi/panel-grid.d.ts +22 -0
  136. package/dist/components/siyahi/panel-grid.js +34 -0
  137. package/dist/components/siyahi/quote.d.ts +12 -0
  138. package/dist/components/siyahi/quote.js +23 -0
  139. package/dist/components/siyahi/range-position.d.ts +24 -0
  140. package/dist/components/siyahi/range-position.js +105 -0
  141. package/dist/components/siyahi/siyahi-provider.d.ts +13 -2
  142. package/dist/components/siyahi/siyahi-provider.js +17 -4
  143. package/dist/components/siyahi/target-rows.d.ts +34 -0
  144. package/dist/components/siyahi/target-rows.js +97 -0
  145. package/dist/components/siyahi/treemap.d.ts +4 -1
  146. package/dist/components/siyahi/treemap.js +78 -28
  147. package/dist/components/siyahi/warning-alert.d.ts +10 -0
  148. package/dist/components/siyahi/warning-alert.js +20 -0
  149. package/dist/components/ui/accordion.js +5 -5
  150. package/dist/components/ui/alert.js +2 -2
  151. package/dist/components/ui/badge.js +1 -1
  152. package/dist/components/ui/breadcrumb.js +2 -2
  153. package/dist/components/ui/button.js +10 -10
  154. package/dist/components/ui/calendar.js +1 -1
  155. package/dist/components/ui/card.js +4 -4
  156. package/dist/components/ui/chart.js +1 -1
  157. package/dist/components/ui/code-block.js +13 -12
  158. package/dist/components/ui/command.js +7 -6
  159. package/dist/components/ui/context-menu.js +7 -7
  160. package/dist/components/ui/dialog.js +5 -5
  161. package/dist/components/ui/dropdown-menu.js +8 -8
  162. package/dist/components/ui/input-group.js +11 -11
  163. package/dist/components/ui/input.js +1 -1
  164. package/dist/components/ui/popover.js +3 -3
  165. package/dist/components/ui/progress.js +1 -1
  166. package/dist/components/ui/select.js +6 -6
  167. package/dist/components/ui/sheet.js +5 -5
  168. package/dist/components/ui/sidebar.js +17 -18
  169. package/dist/components/ui/skeleton.js +1 -1
  170. package/dist/components/ui/slider.js +2 -2
  171. package/dist/components/ui/table.js +2 -2
  172. package/dist/components/ui/tabs.js +2 -2
  173. package/dist/components/ui/textarea.js +1 -1
  174. package/dist/components/ui/toggle-group.js +2 -2
  175. package/dist/components/ui/toggle.js +4 -4
  176. package/dist/components/ui/tooltip.js +2 -2
  177. package/dist/index.d.ts +21 -7
  178. package/dist/index.js +18 -4
  179. package/dist/lib/chart-frame.d.ts +25 -7
  180. package/dist/lib/chart-frame.js +251 -127
  181. package/dist/lib/chart-series.d.ts +12 -6
  182. package/dist/lib/compact-columns.d.ts +19 -0
  183. package/dist/lib/compact-columns.js +55 -0
  184. package/dist/lib/format.d.ts +7 -2
  185. package/dist/lib/format.js +54 -16
  186. package/dist/lib/heatmap-navigation.d.ts +8 -0
  187. package/dist/lib/heatmap-navigation.js +49 -0
  188. package/dist/lib/treemap.js +6 -1
  189. package/dist/lib/use-element-width.d.ts +5 -0
  190. package/dist/lib/use-element-width.js +15 -0
  191. package/package.json +4 -1
  192. package/registry.lock.json +16 -0
  193. package/report.css +33 -32
  194. package/styles/siyahi.css +221 -39
  195. package/dist/lib/recharts.d.ts +0 -2
  196. package/dist/lib/recharts.js +0 -1
@@ -1,6 +1,8 @@
1
1
  import { cn } from "../../../lib/utils.js";
2
+ import { useHeatmapNavigation } from "../../../lib/heatmap-navigation.js";
3
+ import { useElementWidth } from "../../../lib/use-element-width.js";
2
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { Fragment as Fragment$1, createContext, use, useMemo } from "react";
5
+ import { Fragment as Fragment$1, createContext, use, useLayoutEffect, useMemo } from "react";
4
6
  import { differenceInCalendarDays, eachDayOfInterval, format, formatISO, getDay, getMonth, getYear, nextDay, parseISO, subWeeks } from "date-fns";
5
7
  //#region src/components/registry/heatmap/calendar-heatmap.tsx
6
8
  const colorStepCount = (levels, isNormalized) => Math.max(1, isNormalized ? levels : levels - 1);
@@ -40,6 +42,12 @@ const generateWeekdayLabels = (locale) => {
40
42
  };
41
43
  const EMPTY_STYLE = {};
42
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;
43
51
  const CalendarHeatmapContext = createContext(null);
44
52
  const useCalendarHeatmap = () => {
45
53
  const context = use(CalendarHeatmapContext);
@@ -131,26 +139,18 @@ const groupContinuous = (activities, weekStart = 0) => {
131
139
  const padded = [...new Array(leadingPad).fill(void 0), ...normalizedActivities];
132
140
  const numberOfWeeks = Math.ceil(padded.length / 7);
133
141
  const allWeeks = new Array(numberOfWeeks).fill(void 0).map((_, i) => padded.slice(i * 7, i * 7 + 7));
134
- const yearMap = /* @__PURE__ */ new Map();
135
- for (const week of allWeeks) {
136
- const firstActivity = week.find((a) => a !== void 0);
137
- const year = firstActivity ? getYear(parseISO(firstActivity.date)) : void 0;
138
- if (year !== void 0) {
139
- if (!yearMap.has(year)) yearMap.set(year, []);
140
- yearMap.get(year)?.push(week);
141
- }
142
- }
143
- return Array.from(yearMap.entries()).sort(([a], [b]) => a - b).map(([year, weeks]) => ({
144
- year,
142
+ return [{
143
+ year: getYear(firstDate),
145
144
  startMonth: 0,
146
- weeks
147
- }));
145
+ weeks: allWeeks
146
+ }];
148
147
  };
149
- const getMonthLabels = (weeks, monthNames = generateMonthLabels()) => {
150
- return weeks.reduce((labels, week, weekIndex) => {
148
+ const getMonthLabels = (weeks, monthNames = generateMonthLabels(), yearTicks = false) => {
149
+ const ticks = weeks.reduce((labels, week, weekIndex) => {
151
150
  const firstActivity = week.find((activity) => activity !== void 0);
152
151
  if (!firstActivity) return labels;
153
- const month = monthNames[getMonth(parseISO(firstActivity.date))];
152
+ const date = parseISO(firstActivity.date);
153
+ const month = monthNames[getMonth(date)];
154
154
  if (!month) {
155
155
  const monthName = new Date(firstActivity.date).toLocaleString("en-US", { month: "short" });
156
156
  throw new Error(`Unexpected error: undefined month label for ${monthName}.`);
@@ -158,7 +158,8 @@ const getMonthLabels = (weeks, monthNames = generateMonthLabels()) => {
158
158
  const prevLabel = labels.at(-1);
159
159
  if (weekIndex === 0 || !prevLabel || prevLabel.label !== month) return labels.concat({
160
160
  weekIndex,
161
- label: month
161
+ label: month,
162
+ year: getMonth(date) === 0 ? getYear(date) : void 0
162
163
  });
163
164
  return labels;
164
165
  }, []).filter(({ weekIndex }, index, labels) => {
@@ -167,6 +168,22 @@ const getMonthLabels = (weeks, monthNames = generateMonthLabels()) => {
167
168
  if (index === labels.length - 1) return weeks.slice(weekIndex).length >= minWeeks;
168
169
  return true;
169
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
+ });
170
187
  };
171
188
  /**
172
189
  * Calendar Heatmap
@@ -201,8 +218,9 @@ const getMonthLabels = (weeks, monthNames = generateMonthLabels()) => {
201
218
  * @param levels - Total number of legend cells (including empty when not normalized). Default: 5
202
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.
203
220
  */
204
- 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 }) => {
205
222
  const levels = Math.max(1, levelsProp);
223
+ const [rootRef, rootWidth] = useElementWidth();
206
224
  const dataWithLevels = useMemo(() => {
207
225
  if (data.length === 0) return [];
208
226
  const maxCount = data.reduce((max, d) => Math.max(max, d.value), 1);
@@ -235,6 +253,8 @@ const CalendarHeatmap = ({ data, weekStart = 0, continuousMonths = true, hasEmpt
235
253
  const labelHeight = fontSize + LABEL_MARGIN;
236
254
  const year = data.length > 0 ? getYear(parseISO(data[0].date)) : (/* @__PURE__ */ new Date()).getFullYear();
237
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);
238
258
  const blockWidth = blockSize * blockAspectRatio;
239
259
  const width = weeks.length * (blockWidth + blockMargin) - blockMargin;
240
260
  const height = labelHeight + (blockSize + blockMargin) * 7 - blockMargin;
@@ -289,8 +309,9 @@ const CalendarHeatmap = ({ data, weekStart = 0, continuousMonths = true, hasEmpt
289
309
  return /* @__PURE__ */ jsx(CalendarHeatmapContext, {
290
310
  value: contextValue,
291
311
  children: /* @__PURE__ */ jsx("div", {
312
+ ref: rootRef,
292
313
  "data-slot": "calendar-heatmap",
293
- className: cn("flex w-max max-w-full flex-col gap-2 p-4", className),
314
+ className: cn("flex flex-col gap-2", blockSizeProp === void 0 ? "w-full" : "w-max max-w-full", className),
294
315
  style: {
295
316
  fontSize,
296
317
  ...style
@@ -300,7 +321,7 @@ const CalendarHeatmap = ({ data, weekStart = 0, continuousMonths = true, hasEmpt
300
321
  })
301
322
  });
302
323
  };
303
- 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 }) => {
304
325
  const { blockSize, blockWidth, blockMargin, blockRadius, labelHeight, labels, levels, isNormalized, colors } = useCalendarHeatmap();
305
326
  const level = Math.max(0, Math.min(colorStepCount(levels, isNormalized), activity.level));
306
327
  const ariaLabel = (labels.cellLabel ?? "{{date}}: {{value}} contributions").replace("{{date}}", activity.date).replace("{{value}}", String(activity.value));
@@ -308,6 +329,7 @@ const CalendarHeatmapBlock = ({ ref, activity, dayIndex, weekIndex, highlighted
308
329
  ref,
309
330
  "data-slot": "calendar-heatmap-block",
310
331
  role: onCellClick ? "button" : "img",
332
+ "aria-pressed": onCellClick ? selected : void 0,
311
333
  tabIndex: onCellClick ? 0 : -1,
312
334
  "aria-label": ariaLabel,
313
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),
@@ -315,6 +337,7 @@ const CalendarHeatmapBlock = ({ ref, activity, dayIndex, weekIndex, highlighted
315
337
  "data-date": activity.date,
316
338
  "data-level": level,
317
339
  "data-highlighted": highlighted || void 0,
340
+ "data-selected": selected || void 0,
318
341
  height: blockSize,
319
342
  rx: blockRadius,
320
343
  ry: blockRadius,
@@ -323,6 +346,10 @@ const CalendarHeatmapBlock = ({ ref, activity, dayIndex, weekIndex, highlighted
323
346
  y: labelHeight + (blockSize + blockMargin) * dayIndex,
324
347
  style: {
325
348
  fill: getLevelFill(level, levels, isNormalized, highlighted, colors),
349
+ ...selected ? {
350
+ stroke: "var(--foreground)",
351
+ strokeWidth: 2
352
+ } : {},
326
353
  ...styleProp
327
354
  },
328
355
  onClick: (event) => {
@@ -349,17 +376,18 @@ const CalendarHeatmapBlock = ({ ref, activity, dayIndex, weekIndex, highlighted
349
376
  };
350
377
  CalendarHeatmapBlock.displayName = "CalendarHeatmapBlock";
351
378
  const CalendarHeatmapBody = ({ hideMonthLabels = false, hideWeekdayLabels = false, hideYearLabels = false, className, labelClassName, yearClassName, children, renderYearFooter, ...props }) => {
352
- const { yearRows, blockSize, blockWidth, blockMargin, labels, labelHeight, fontSize, weekStart, data } = useCalendarHeatmap();
353
- const weekdayLabelWidth = hideWeekdayLabels ? 0 : 40;
354
- const strokePadding = 3;
379
+ const navigation = useHeatmapNavigation();
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;
355
383
  const rowData = useMemo(() => {
356
384
  return yearRows.map((yearRow) => {
357
385
  return {
358
386
  yearRow,
359
387
  width: yearRow.weeks.length * (blockWidth + blockMargin) - blockMargin,
360
388
  height: labelHeight + (blockSize + blockMargin) * 7 - blockMargin,
361
- monthLabels: getMonthLabels(yearRow.weeks, labels.months),
362
- 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)
363
391
  };
364
392
  });
365
393
  }, [
@@ -369,15 +397,28 @@ const CalendarHeatmapBody = ({ hideMonthLabels = false, hideWeekdayLabels = fals
369
397
  blockMargin,
370
398
  labelHeight,
371
399
  labels.months,
372
- data
400
+ continuousMonths
373
401
  ]);
374
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
+ ]);
375
411
  return /* @__PURE__ */ jsx("div", {
376
412
  "data-slot": "calendar-heatmap-body",
413
+ ref: navigation.ref,
377
414
  className: cn("flex max-w-full flex-col gap-6 overflow-x-auto overflow-y-hidden py-4", className),
378
415
  ...props,
416
+ onKeyDown: (event) => {
417
+ props.onKeyDown?.(event);
418
+ if (!event.defaultPrevented) navigation.onKeyDown(event);
419
+ },
379
420
  children: rowData.map(({ yearRow, height, monthLabels, yearTotalCount }) => /* @__PURE__ */ jsxs("div", { children: [
380
- !hideYearLabels && /* @__PURE__ */ jsx("div", {
421
+ !hideYearLabels && !continuousMonths && /* @__PURE__ */ jsx("div", {
381
422
  className: cn("mb-2 text-muted-foreground", yearClassName),
382
423
  children: yearRow.year
383
424
  }),
@@ -458,7 +499,7 @@ const CalendarHeatmapStat = ({ compute, label, className, children, ...props })
458
499
  });
459
500
  };
460
501
  const CalendarHeatmapLegend = ({ labels: labelsProp, className, children, ...props }) => {
461
- const { levels, isNormalized, blockSize, blockWidth, blockRadius, colors, labels } = useCalendarHeatmap();
502
+ const { levels, isNormalized, blockRadius, colors, labels } = useCalendarHeatmap();
462
503
  const lessLabel = labelsProp?.less ?? "Less";
463
504
  const moreLabel = labelsProp?.more ?? "More";
464
505
  const legendLevels = Array.from({ length: levels }, (_, i) => isNormalized ? i + 1 : i);
@@ -475,14 +516,14 @@ const CalendarHeatmapLegend = ({ labels: labelsProp, className, children, ...pro
475
516
  legendLevels.map((level) => children ? /* @__PURE__ */ jsx(Fragment$1, { children: children({ level }) }, `legend-level-${level}`) : /* @__PURE__ */ jsx("svg", {
476
517
  role: "img",
477
518
  "aria-label": (labels.legendLevelLabel ?? "{{level}} contributions").replace("{{level}}", String(level)),
478
- height: blockSize,
479
- width: blockWidth,
519
+ height: LEGEND_SWATCH,
520
+ width: LEGEND_SWATCH,
480
521
  children: /* @__PURE__ */ jsx("rect", {
481
522
  "data-level": level,
482
- height: blockSize,
523
+ height: LEGEND_SWATCH,
483
524
  rx: blockRadius,
484
525
  ry: blockRadius,
485
- width: blockWidth,
526
+ width: LEGEND_SWATCH,
486
527
  style: { fill: getLevelFill(level, levels, isNormalized, false, colors) }
487
528
  })
488
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,13 +1,15 @@
1
1
  import { cn } from "../../../lib/utils.js";
2
+ import { useHeatmapNavigation } from "../../../lib/heatmap-navigation.js";
3
+ import { useElementWidth } from "../../../lib/use-element-width.js";
2
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
5
  import { Fragment as Fragment$1, createContext, use, useMemo } from "react";
4
6
  import { format } from "date-fns";
5
7
  //#region src/components/registry/heatmap/status-heatmap.tsx
6
8
  const DEFAULT_COLORS = {
7
9
  0: "var(--color-secondary)",
8
- 1: "var(--negative)",
10
+ 1: "var(--destructive)",
9
11
  2: "var(--warning)",
10
- 3: "var(--positive)"
12
+ 3: "color-mix(in oklab, var(--muted-foreground) 40%, transparent)"
11
13
  };
12
14
  const SEMANTIC_KEYS = {
13
15
  0: "noData",
@@ -51,6 +53,9 @@ const getStatusFill = (value, colors, highlighted = false) => {
51
53
  return `color-mix(in oklch, ${base} 60%, transparent)`;
52
54
  };
53
55
  const EMPTY_STYLE = {};
56
+ const LEGEND_SWATCH = 10;
57
+ const MIN_FITTED_BAR = 2;
58
+ const DEFAULT_ASPECT_RATIO = .2;
54
59
  const StatusHeatmapContext = createContext(null);
55
60
  const useStatusHeatmap = () => {
56
61
  const context = use(StatusHeatmapContext);
@@ -84,15 +89,16 @@ const useStatusHeatmap = () => {
84
89
  *
85
90
  * @param data - Array of activities with date (YYYY-MM-DD) and status value (0-3)
86
91
  * @param dateFormat - Date format string for tooltips. Default: "MMM d"
87
- * @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
88
93
  * @param colors - Custom colors for critical, degraded, and healthy states
89
94
  */
90
- 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 = [
91
96
  0,
92
97
  1,
93
98
  2,
94
99
  3
95
100
  ], healthyValue = 3, style = EMPTY_STYLE, className, children, ...props }) => {
101
+ const [rootRef, rootWidth] = useElementWidth();
96
102
  const labels = useMemo(() => ({
97
103
  statuses: {
98
104
  noData: "No Data",
@@ -112,7 +118,10 @@ const StatusHeatmap = ({ data, dateFormat = "MMM d", blockSize = 40, blockMargin
112
118
  });
113
119
  return Array.from(uniqueDates).sort();
114
120
  }, [data]);
115
- 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;
116
125
  const width = dates.length * (blockWidth + blockMargin) - blockMargin;
117
126
  const height = blockSize;
118
127
  const contextValue = useMemo(() => ({
@@ -154,8 +163,9 @@ const StatusHeatmap = ({ data, dateFormat = "MMM d", blockSize = 40, blockMargin
154
163
  return /* @__PURE__ */ jsx(StatusHeatmapContext, {
155
164
  value: contextValue,
156
165
  children: /* @__PURE__ */ jsx("div", {
166
+ ref: rootRef,
157
167
  "data-slot": "status-heatmap",
158
- className: cn("flex w-max max-w-full flex-col gap-2 p-4", className),
168
+ className: cn("flex flex-col gap-2", fits ? "w-full" : "w-max max-w-full", className),
159
169
  style: {
160
170
  fontSize,
161
171
  ...style
@@ -165,7 +175,7 @@ const StatusHeatmap = ({ data, dateFormat = "MMM d", blockSize = 40, blockMargin
165
175
  })
166
176
  });
167
177
  };
168
- 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 }) => {
169
179
  const { blockSize, blockWidth, blockMargin, blockRadius, labels, colors } = useStatusHeatmap();
170
180
  const statusText = resolveStatusLabel(activity.value, labels.statuses);
171
181
  const ariaLabel = (labels.cellLabel ?? "{{date}}: {{status}}").replace("{{date}}", activity.date).replace("{{status}}", statusText);
@@ -174,12 +184,14 @@ const StatusHeatmapBlock = ({ ref, activity, dayIndex, highlighted = false, onCe
174
184
  ref,
175
185
  "data-slot": "status-heatmap-block",
176
186
  role: onCellClick ? "button" : "img",
187
+ "aria-pressed": onCellClick ? selected : void 0,
177
188
  tabIndex: onCellClick ? 0 : -1,
178
189
  "aria-label": ariaLabel,
179
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),
180
191
  "data-value": activity.value,
181
192
  "data-date": activity.date,
182
193
  "data-highlighted": highlighted || void 0,
194
+ "data-selected": selected || void 0,
183
195
  height: blockSize,
184
196
  rx: blockRadius,
185
197
  ry: blockRadius,
@@ -188,6 +200,10 @@ const StatusHeatmapBlock = ({ ref, activity, dayIndex, highlighted = false, onCe
188
200
  y: 0,
189
201
  style: {
190
202
  fill: getStatusFill(activity.value, colors, highlighted),
203
+ ...selected ? {
204
+ stroke: "var(--foreground)",
205
+ strokeWidth: 2
206
+ } : {},
191
207
  ...styleProp
192
208
  },
193
209
  onClick: (event) => {
@@ -214,6 +230,7 @@ const StatusHeatmapBlock = ({ ref, activity, dayIndex, highlighted = false, onCe
214
230
  };
215
231
  StatusHeatmapBlock.displayName = "StatusHeatmapBlock";
216
232
  const StatusHeatmapBody = ({ hideDateLabels = false, labelInterval = 30, className, labelClassName, children, ...props }) => {
233
+ const navigation = useHeatmapNavigation();
217
234
  const { data, dates, width, height, blockWidth, blockMargin, fontSize, dateFormat, locale, labels } = useStatusHeatmap();
218
235
  const activityMap = useMemo(() => {
219
236
  const map = /* @__PURE__ */ new Map();
@@ -234,15 +251,20 @@ const StatusHeatmapBody = ({ hideDateLabels = false, labelInterval = 30, classNa
234
251
  const labelHeight = hideDateLabels ? 0 : labelFontSize * 2;
235
252
  return /* @__PURE__ */ jsx("div", {
236
253
  "data-slot": "status-heatmap-body",
254
+ ref: navigation.ref,
237
255
  className: cn("max-w-full overflow-x-auto overflow-y-hidden", className),
238
256
  ...props,
257
+ onKeyDown: (event) => {
258
+ props.onKeyDown?.(event);
259
+ if (!event.defaultPrevented) navigation.onKeyDown(event);
260
+ },
239
261
  children: /* @__PURE__ */ jsxs("svg", {
240
262
  role: "img",
241
263
  "aria-label": labels.heatmapLabel ?? "Status heatmap",
242
264
  className: "block overflow-visible rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
243
- height: height + labelHeight + 40,
244
- viewBox: `-20 -20 ${width + 40} ${height + labelHeight + 40}`,
245
- width: width + 40,
265
+ height: height + labelHeight + 6,
266
+ viewBox: `-3 -3 ${width + 6} ${height + labelHeight + 6}`,
267
+ width: width + 6,
246
268
  children: [!hideDateLabels && /* @__PURE__ */ jsx("g", {
247
269
  className: cn("fill-current font-mono", labelClassName),
248
270
  children: dates.map((date, dayIndex) => {
@@ -292,7 +314,7 @@ const StatusHeatmapStat = ({ compute, label, className, children, ...props }) =>
292
314
  });
293
315
  };
294
316
  const StatusHeatmapLegend = ({ labels: labelsProp, className, children, ...props }) => {
295
- const { labels, statusValues, blockSize, blockWidth, blockRadius, colors } = useStatusHeatmap();
317
+ const { labels, statusValues, blockRadius, colors } = useStatusHeatmap();
296
318
  const statuses = statusValues.map((value) => ({
297
319
  value,
298
320
  label: resolveStatusLabel(value, labelsProp ?? labels.statuses)
@@ -309,14 +331,14 @@ const StatusHeatmapLegend = ({ labels: labelsProp, className, children, ...props
309
331
  className: "flex items-center gap-1",
310
332
  children: [/* @__PURE__ */ jsx("svg", {
311
333
  "aria-hidden": "true",
312
- height: blockSize / 2,
313
- width: blockWidth,
334
+ height: LEGEND_SWATCH,
335
+ width: LEGEND_SWATCH,
314
336
  children: /* @__PURE__ */ jsx("rect", {
315
337
  "data-value": status.value,
316
- height: blockSize / 2,
338
+ height: LEGEND_SWATCH,
317
339
  rx: blockRadius,
318
340
  ry: blockRadius,
319
- width: blockWidth,
341
+ width: LEGEND_SWATCH,
320
342
  style: { fill: getStatusFill(status.value, colors) }
321
343
  })
322
344
  }), /* @__PURE__ */ jsx("span", {
@@ -36,6 +36,10 @@ export type ColorConfig = {
36
36
  export type WeekdayHeatmapProps = HTMLAttributes<HTMLDivElement> & {
37
37
  data: WeekdayHourlyActivity[];
38
38
  weekStart?: Day;
39
+ /** Displayed hours, in order; defaults to the full day. */
40
+ hours?: readonly WeekdayHour[];
41
+ /** Displayed days, in order; defaults to the full week from weekStart. */
42
+ weekdays?: readonly WeekdayIndex[];
39
43
  use12Hour?: boolean;
40
44
  blockSize?: number;
41
45
  blockMargin?: number;
@@ -86,8 +90,10 @@ export type WeekdayHeatmapProps = HTMLAttributes<HTMLDivElement> & {
86
90
  * @param extraRow - Appends an aggregated row below the grid. `compute` receives all activities and must return 24 values (one per hour). Rendered through `renderExtraRow` on the body, or falls back to `children`.
87
91
  * @param extraColumn - Appends an aggregated column to the right of the grid. `compute` receives all activities and must return 7 values indexed by weekday 0–6. Rendered through `renderExtraColumn` on the body, or falls back to `children`.
88
92
  */
89
- export declare const WeekdayHeatmap: ({ data, weekStart, use12Hour, blockSize, blockMargin, blockRadius, blockAspectRatio, levels: levelsProp, isNormalized, colors, locale, labels: labelsProp, fontSize, emptyState, totalCount: totalCountProp, extraRow, extraColumn, style, className, children, ...props }: WeekdayHeatmapProps) => 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;
93
+ export declare const WeekdayHeatmap: ({ data, weekStart, hours, weekdays: displayedWeekdays, use12Hour, blockSize: blockSizeProp, blockMargin, blockRadius, blockAspectRatio, levels: levelsProp, isNormalized, colors, locale, labels: labelsProp, fontSize, emptyState, totalCount: totalCountProp, extraRow, extraColumn, style, className, children, ...props }: WeekdayHeatmapProps) => 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;
90
94
  export type WeekdayHeatmapBlockProps = HTMLAttributes<SVGRectElement> & {
95
+ /** The cell the page has picked: drawn with a ring and announced as pressed. */
96
+ selected?: boolean;
91
97
  activity: WeekdayHourlyActivityWithLevel;
92
98
  extra?: "row" | "column";
93
99
  highlighted?: boolean;
@@ -95,9 +101,9 @@ export type WeekdayHeatmapBlockProps = HTMLAttributes<SVGRectElement> & {
95
101
  onCellHover?: (activity: WeekdayHourlyActivityWithLevel | null) => void;
96
102
  className?: string;
97
103
  };
98
- export declare function WeekdayHeatmapBlock({ ref, activity, extra, highlighted, onCellClick, onCellHover, onClick, onKeyDown, onMouseEnter, onMouseLeave, className, style: styleProp, ...props }: WeekdayHeatmapBlockProps & {
104
+ export declare function WeekdayHeatmapBlock({ ref, activity, extra, highlighted, selected, onCellClick, onCellHover, onClick, onKeyDown, onMouseEnter, onMouseLeave, className, style: styleProp, ...props }: WeekdayHeatmapBlockProps & {
99
105
  ref?: React.RefObject<SVGRectElement | null>;
100
- }): import("react").JSX.Element;
106
+ }): import("react").JSX.Element | null;
101
107
  export declare namespace WeekdayHeatmapBlock {
102
108
  var displayName: string;
103
109
  }