@sableui/react 0.17.0 → 0.19.0
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.
- package/AGENTS.md +10 -3
- package/CHANGELOG.md +235 -0
- package/README.md +11 -4
- package/bin/sable.mjs +266 -30
- package/dist/{_contract-CDa9-NdB.d.ts → _contract-DlklV0Im.d.ts} +2 -2
- package/dist/activity-DT8VK9fx.d.ts +25 -0
- package/dist/adapters/github.d.ts +41 -0
- package/dist/adapters/github.js +24 -0
- package/dist/blocks/index.d.ts +1 -1
- package/dist/components/activity-heatmap/activity-heatmap.js +420 -0
- package/dist/components/activity-heatmap/activity-heatmap.meta.js +142 -0
- package/dist/components/activity-heatmap/index.js +9 -0
- package/dist/components/avatar/avatar.js +16 -7
- package/dist/components/avatar/avatar.meta.js +43 -10
- package/dist/components/avatar/avatar.variants.js +26 -2
- package/dist/components/code-block/code-block.js +5 -3
- package/dist/components/code-block/code-block.meta.js +6 -0
- package/dist/components/collapsible/collapsible.js +6 -5
- package/dist/components/collapsible/collapsible.meta.js +14 -5
- package/dist/components/data-table/data-table.js +132 -45
- package/dist/components/data-table/data-table.meta.js +63 -4
- package/dist/components/date-picker/date-picker.js +13 -6
- package/dist/components/date-picker/date-picker.meta.js +22 -0
- package/dist/components/dialog/dialog.js +25 -5
- package/dist/components/dialog/dialog.meta.js +26 -5
- package/dist/components/media-image/media-image.js +3 -1
- package/dist/components/media-image/media-image.meta.js +15 -1
- package/dist/components/media-tile/media-tile.js +13 -1
- package/dist/components/media-tile/media-tile.meta.js +10 -0
- package/dist/components/menu/menu.js +2 -0
- package/dist/components/menu/menu.meta.js +2 -2
- package/dist/components/page-header/page-header.js +9 -4
- package/dist/components/page-header/page-header.meta.js +26 -1
- package/dist/components/panel-group/panel-group.js +17 -1
- package/dist/components/panel-group/panel-group.meta.js +43 -3
- package/dist/components/pill/pill.meta.js +15 -6
- package/dist/components/pill/pill.variants.js +1 -0
- package/dist/components/search-input/search-input.js +30 -5
- package/dist/components/search-input/search-input.meta.js +29 -4
- package/dist/components/section-header/section-header.js +27 -17
- package/dist/components/section-header/section-header.meta.js +27 -5
- package/dist/components/sheet/sheet.js +15 -3
- package/dist/components/sheet/sheet.meta.js +2 -2
- package/dist/components/side-nav/side-nav.variants.js +8 -3
- package/dist/components/sidebar/index.js +4 -0
- package/dist/components/sidebar/sidebar.js +90 -11
- package/dist/components/sidebar/sidebar.meta.js +38 -4
- package/dist/components/table/table.js +7 -3
- package/dist/components/tabs/tabs.js +10 -2
- package/dist/components/tabs/tabs.meta.js +4 -0
- package/dist/components/workspace/workspace.js +13 -3
- package/dist/components/workspace/workspace.meta.js +109 -1
- package/dist/file-hashes.json +94 -80
- package/dist/foundations/typography.js +13 -0
- package/dist/icon/Icon.js +21 -2
- package/dist/icon/index.d.ts +36 -1
- package/dist/icon/index.js +5 -0
- package/dist/icon/names.js +240 -0
- package/dist/icon-sets/reicon.js +41 -1
- package/dist/index.d.ts +540 -75
- package/dist/index.js +18 -1
- package/dist/layout/AspectBox.js +2 -5
- package/dist/layout/Bleed.js +10 -6
- package/dist/layout/Center.js +11 -3
- package/dist/layout/Columns.js +6 -3
- package/dist/layout/Grid.js +14 -6
- package/dist/layout/GridOverlay.js +7 -7
- package/dist/layout/Region.js +18 -3
- package/dist/layout/Row.js +27 -20
- package/dist/layout/Spacer.js +13 -2
- package/dist/layout/SplitPane.js +1 -1
- package/dist/layout/Stack.js +25 -18
- package/dist/layout/index.d.ts +57 -6
- package/dist/lib/chart/activity.js +32 -0
- package/dist/lib/chart/calendar.js +67 -0
- package/dist/lib/chart/chart-tooltip.js +2 -2
- package/dist/lib/chart/heat.js +55 -0
- package/dist/lib/cn.js +1 -0
- package/dist/lib/focus-return.js +53 -0
- package/dist/lib/taxonomy.js +1 -0
- package/dist/lib/use-media-query.js +41 -0
- package/dist/{meta-DX5r5wQL.d.ts → meta-Dfm9AExe.d.ts} +6 -0
- package/dist/patterns/dashboard-page.js +23 -17
- package/dist/patterns/index.d.ts +7 -2
- package/dist/patterns/index.js +5 -1
- package/dist/patterns/tracker-page.js +392 -0
- package/dist/patterns/tracker-page.meta.js +40 -0
- package/dist/presets/index.d.ts +1 -1
- package/dist/styles.css +301 -188
- package/dist/{theme-provider-BydfM5Z0.d.ts → theme-provider-BUBealk_.d.ts} +1 -1
- package/dist/themes/_contract.js +9 -1
- package/dist/themes/atelier/index.d.ts +1 -1
- package/dist/themes/ember/index.d.ts +1 -1
- package/dist/themes/index.d.ts +2 -2
- package/dist/themes/meridian/index.d.ts +1 -1
- package/dist/themes/poppy/index.d.ts +1 -1
- package/dist/themes/prism/index.d.ts +1 -1
- package/dist/themes/sable/index.d.ts +1 -1
- package/dist/themes/verdant/index.d.ts +1 -1
- package/llms/components/activity-heatmap.md +133 -0
- package/llms/components/avatar.md +32 -9
- package/llms/components/code-block.md +1 -0
- package/llms/components/collapsible.md +12 -4
- package/llms/components/data-table.md +40 -3
- package/llms/components/date-picker.md +15 -1
- package/llms/components/dialog.md +34 -3
- package/llms/components/media-image.md +12 -0
- package/llms/components/media-tile.md +21 -0
- package/llms/components/menu.md +1 -1
- package/llms/components/page-header.md +16 -1
- package/llms/components/panel-group.md +34 -2
- package/llms/components/pill.md +24 -5
- package/llms/components/search-input.md +37 -4
- package/llms/components/section-header.md +28 -4
- package/llms/components/sheet.md +2 -2
- package/llms/components/sidebar.md +17 -4
- package/llms/components/tabs.md +1 -0
- package/llms/components/workspace.md +81 -1
- package/llms/foundations/typography.md +4 -1
- package/llms/layout/aspect-box.md +1 -1
- package/llms/layout/bleed.md +1 -1
- package/llms/layout/center.md +1 -1
- package/llms/layout/columns.md +1 -1
- package/llms/layout/grid.md +1 -1
- package/llms/layout/region.md +1 -1
- package/llms/layout/row.md +9 -1
- package/llms/layout/spacer.md +1 -1
- package/llms/layout/split-pane.md +1 -1
- package/llms/layout/stack.md +1 -1
- package/llms/patterns/dashboard-page.md +18 -15
- package/llms/patterns/tracker-page.md +450 -0
- package/llms/reference/token-intent.md +3 -1
- package/llms/reference/utilities.md +4 -1
- package/llms/tasks/auditing-an-app.md +35 -1
- package/llms/tasks/building-a-page.md +3 -2
- package/llms/tasks/building-a-tool.md +101 -5
- package/llms/tasks/building-with-sable.md +11 -3
- package/llms/tasks/creating-a-preset.md +9 -0
- package/llms/tasks/getting-started.md +10 -3
- package/llms/tasks/using-presets.md +1 -2
- package/llms-full.txt +1215 -89
- package/llms.txt +11 -9
- package/package.json +7 -1
- package/registry.json +1021 -70
- package/scripts/adoption.mjs +252 -0
- package/scripts/agent-setup.mjs +110 -18
- package/scripts/app-css.mjs +129 -0
- package/scripts/grade.mjs +178 -2
- package/scripts/lint-rules.mjs +126 -0
- package/scripts/wiring.mjs +2 -1
- package/skills/entry/SKILL.md +1 -1
- package/skills/sable/SKILL.md +64 -6
- package/skills/sable/agents/openai.yaml +1 -1
- package/targets/ios.json +9 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { A as ActivityDay } from '../activity-DT8VK9fx.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GitHub contribution data → ActivityHeatmap days. Pure transforms: Sable never
|
|
5
|
+
* fetches. GitHub's GraphQL API needs a token, so call it from your server and
|
|
6
|
+
* pass the result through; the docs page shows a route handler that does.
|
|
7
|
+
*/
|
|
8
|
+
/** The `contributionLevel` enum GitHub's GraphQL API returns per day. */
|
|
9
|
+
type GitHubContributionLevel = "NONE" | "FIRST_QUARTILE" | "SECOND_QUARTILE" | "THIRD_QUARTILE" | "FOURTH_QUARTILE";
|
|
10
|
+
/** The slice of `user.contributionsCollection.contributionCalendar` the transform reads. */
|
|
11
|
+
interface GitHubContributionCalendar {
|
|
12
|
+
weeks: {
|
|
13
|
+
contributionDays: {
|
|
14
|
+
date: string;
|
|
15
|
+
contributionCount: number;
|
|
16
|
+
contributionLevel?: GitHubContributionLevel;
|
|
17
|
+
}[];
|
|
18
|
+
}[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* From GraphQL `contributionsCollection { contributionCalendar { weeks { contributionDays
|
|
22
|
+
* { date contributionCount contributionLevel } } } }`. GitHub's own quartile levels pass
|
|
23
|
+
* through as 0–4, so the grid matches the profile page exactly (use the default 5 levels).
|
|
24
|
+
*/
|
|
25
|
+
declare function fromGitHubGraphQL(calendar: GitHubContributionCalendar): ActivityDay[];
|
|
26
|
+
/** The response of the community github-contributions-api (`/v4/{user}`). */
|
|
27
|
+
interface GitHubContributionsApiResponse {
|
|
28
|
+
contributions: {
|
|
29
|
+
date: string;
|
|
30
|
+
count: number;
|
|
31
|
+
level?: number;
|
|
32
|
+
}[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* From the community github-contributions-api (`https://github-contributions-api.jogruber.de/v4/{user}`).
|
|
36
|
+
* It is a third-party scraper of the profile page — cached for an hour and rate
|
|
37
|
+
* limited — so treat it as a convenience for demos, not a dependency.
|
|
38
|
+
*/
|
|
39
|
+
declare function fromGitHubContributionsApi(json: GitHubContributionsApiResponse): ActivityDay[];
|
|
40
|
+
|
|
41
|
+
export { type GitHubContributionCalendar, type GitHubContributionLevel, type GitHubContributionsApiResponse, fromGitHubContributionsApi, fromGitHubGraphQL };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/adapters/github.ts
|
|
2
|
+
var LEVEL = {
|
|
3
|
+
NONE: 0,
|
|
4
|
+
FIRST_QUARTILE: 1,
|
|
5
|
+
SECOND_QUARTILE: 2,
|
|
6
|
+
THIRD_QUARTILE: 3,
|
|
7
|
+
FOURTH_QUARTILE: 4
|
|
8
|
+
};
|
|
9
|
+
function fromGitHubGraphQL(calendar) {
|
|
10
|
+
return calendar.weeks.flatMap(
|
|
11
|
+
(w) => w.contributionDays.map((d) => ({
|
|
12
|
+
date: d.date,
|
|
13
|
+
value: d.contributionCount,
|
|
14
|
+
...d.contributionLevel ? { level: LEVEL[d.contributionLevel] } : {}
|
|
15
|
+
}))
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
function fromGitHubContributionsApi(json) {
|
|
19
|
+
return json.contributions.map((d) => ({ date: d.date, value: d.count, ...d.level != null ? { level: d.level } : {} }));
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
fromGitHubContributionsApi,
|
|
23
|
+
fromGitHubGraphQL
|
|
24
|
+
};
|
package/dist/blocks/index.d.ts
CHANGED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/components/activity-heatmap/activity-heatmap.tsx
|
|
4
|
+
import {
|
|
5
|
+
useCallback,
|
|
6
|
+
useId,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState
|
|
10
|
+
} from "react";
|
|
11
|
+
import { normalizeActivity } from "../../lib/chart/activity.js";
|
|
12
|
+
import { activityGrid, dayNumber, gridPosition, isoDay, localeWeekStart, today, utcDate, weekday } from "../../lib/chart/calendar.js";
|
|
13
|
+
import { ChartLegend } from "../../lib/chart/chart-legend.js";
|
|
14
|
+
import { ChartTooltipContent } from "../../lib/chart/chart-tooltip.js";
|
|
15
|
+
import { centerValueFormat } from "../../lib/chart/format.js";
|
|
16
|
+
import { activityLevels, heatColor, levelNames } from "../../lib/chart/heat.js";
|
|
17
|
+
import { riseTransition, stagger } from "../../lib/chart/motion.js";
|
|
18
|
+
import { categoryStyle, toneColor } from "../../lib/chart/types.js";
|
|
19
|
+
import { useArrival } from "../../lib/chart/use-arrival.js";
|
|
20
|
+
import { cn } from "../../lib/cn.js";
|
|
21
|
+
import { mergeRefs } from "../../lib/refs.js";
|
|
22
|
+
import { useIsomorphicLayoutEffect } from "../../lib/use-isomorphic-layout-effect.js";
|
|
23
|
+
import { useResizeObserver } from "../../lib/use-resize-observer.js";
|
|
24
|
+
import { motionAttribute, useMotionLevel } from "../../motion/MotionProvider.js";
|
|
25
|
+
import { Scroller } from "../scroller/index.js";
|
|
26
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
27
|
+
var CELL_FLOOR = { sm: 10, md: 12, lg: 14 };
|
|
28
|
+
var CELL_MAX = 20;
|
|
29
|
+
var PAD = 4;
|
|
30
|
+
var SHOWN_WEEKDAYS = /* @__PURE__ */ new Set([1, 3, 5]);
|
|
31
|
+
var gapFor = (cell) => Math.max(2, Math.floor(cell * 0.2 * 4) / 4);
|
|
32
|
+
function fitCell(width, weeks, floor, stretch) {
|
|
33
|
+
if (!stretch || !width) return floor;
|
|
34
|
+
const fit = (width - PAD * 2) / (weeks * 1.2 + 0.2);
|
|
35
|
+
return Math.max(floor, Math.min(CELL_MAX, Math.floor(fit * 4) / 4));
|
|
36
|
+
}
|
|
37
|
+
function ActivityHeatmap({
|
|
38
|
+
label,
|
|
39
|
+
data,
|
|
40
|
+
start,
|
|
41
|
+
end,
|
|
42
|
+
year,
|
|
43
|
+
weekStart,
|
|
44
|
+
levels = 5,
|
|
45
|
+
levelScale = "quantile",
|
|
46
|
+
tone = "1",
|
|
47
|
+
categories,
|
|
48
|
+
unit,
|
|
49
|
+
formatValue,
|
|
50
|
+
tooltip = true,
|
|
51
|
+
legend = true,
|
|
52
|
+
legendLabels = ["Less", "More"],
|
|
53
|
+
monthLabels = true,
|
|
54
|
+
weekdayLabels = "alternate",
|
|
55
|
+
size = "md",
|
|
56
|
+
stretch = true,
|
|
57
|
+
activeDate,
|
|
58
|
+
defaultActiveDate = null,
|
|
59
|
+
onActiveDateChange,
|
|
60
|
+
selectedDate,
|
|
61
|
+
onSelectDate,
|
|
62
|
+
renderCell,
|
|
63
|
+
loading = false,
|
|
64
|
+
motion,
|
|
65
|
+
locale,
|
|
66
|
+
className,
|
|
67
|
+
style,
|
|
68
|
+
ref,
|
|
69
|
+
...props
|
|
70
|
+
}) {
|
|
71
|
+
const uid = useId();
|
|
72
|
+
const { expressive } = useMotionLevel(motion);
|
|
73
|
+
const [arrivalRef, inView] = useArrival();
|
|
74
|
+
const figureRef = useMemo(() => mergeRefs(arrivalRef, ref), [arrivalRef, ref]);
|
|
75
|
+
const shown = !expressive || inView;
|
|
76
|
+
const todayN = today();
|
|
77
|
+
const firstDay = weekStart ?? localeWeekStart(locale);
|
|
78
|
+
const endN = year != null ? dayNumber(`${year}-12-31`) : end ? dayNumber(end) : todayN;
|
|
79
|
+
const startN = useMemo(() => {
|
|
80
|
+
if (year != null) return dayNumber(`${year}-01-01`);
|
|
81
|
+
if (start) return dayNumber(start);
|
|
82
|
+
const back = endN - 364;
|
|
83
|
+
return back - (weekday(back) - firstDay + 7) % 7;
|
|
84
|
+
}, [year, start, endN, firstDay]);
|
|
85
|
+
const grid = useMemo(() => activityGrid({ start: startN, end: endN, weekStart: firstDay }), [startN, endN, firstDay]);
|
|
86
|
+
const last = Math.min(endN, todayN);
|
|
87
|
+
const navigable = useCallback((n) => n != null && n >= startN && n <= last, [startN, last]);
|
|
88
|
+
const days = useMemo(() => loading ? /* @__PURE__ */ new Map() : normalizeActivity(data), [data, loading]);
|
|
89
|
+
const scaleLevels = useMemo(
|
|
90
|
+
() => activityLevels([...days.values()].map((d) => d.value), { levels, scale: levelScale }),
|
|
91
|
+
[days, levels, levelScale]
|
|
92
|
+
);
|
|
93
|
+
const categoryKeys = categories ? Object.keys(categories) : [];
|
|
94
|
+
const base = toneColor(tone);
|
|
95
|
+
const resolve = (n) => {
|
|
96
|
+
const d = days.get(n);
|
|
97
|
+
const value = d?.value ?? 0;
|
|
98
|
+
const level = d?.level != null ? Math.min(levels - 1, Math.max(0, d.level)) : scaleLevels.level(value);
|
|
99
|
+
const known = d?.category != null && categories?.[d.category] ? d.category : void 0;
|
|
100
|
+
const cat = known ? categoryStyle(categories, known, categoryKeys.indexOf(known)) : void 0;
|
|
101
|
+
return { date: isoDay(n), value, level, category: d?.category, color: heatColor(cat?.color ?? base, level, levels), categoryLabel: cat?.label };
|
|
102
|
+
};
|
|
103
|
+
const words = useMemo(() => {
|
|
104
|
+
const long = new Intl.DateTimeFormat(locale, { weekday: "long", month: "long", day: "numeric", year: "numeric", timeZone: "UTC" });
|
|
105
|
+
const short = new Intl.DateTimeFormat(locale, { weekday: "short", month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
|
|
106
|
+
const crossesYear = utcDate(startN).getUTCFullYear() !== utcDate(endN).getUTCFullYear();
|
|
107
|
+
const monthLong = new Intl.DateTimeFormat(locale, { month: "long", ...crossesYear ? { year: "numeric" } : {}, timeZone: "UTC" });
|
|
108
|
+
const monthShort = new Intl.DateTimeFormat(locale, { month: "short", timeZone: "UTC" });
|
|
109
|
+
const dayLong = new Intl.DateTimeFormat(locale, { weekday: "long", timeZone: "UTC" });
|
|
110
|
+
const dayShort = new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC" });
|
|
111
|
+
const plural = new Intl.PluralRules(locale);
|
|
112
|
+
const number = (v) => formatValue?.(v) ?? new Intl.NumberFormat(locale, centerValueFormat(v)).format(v);
|
|
113
|
+
const unitWord = (v) => unit == null ? "" : typeof unit === "string" ? unit : plural.select(v) === "one" ? unit.one : unit.other;
|
|
114
|
+
const none = unit == null ? "no activity" : `no ${typeof unit === "string" ? unit : unit.other}`;
|
|
115
|
+
return {
|
|
116
|
+
long: (n) => long.format(utcDate(n)),
|
|
117
|
+
short: (n) => short.format(utcDate(n)),
|
|
118
|
+
monthLong: (n) => monthLong.format(utcDate(n)),
|
|
119
|
+
monthShort: (n) => monthShort.format(utcDate(n)),
|
|
120
|
+
// 1970-01-04 (day 3) was a Sunday, so day 3 + d is weekday d.
|
|
121
|
+
dayLong: (d) => dayLong.format(utcDate(3 + d)),
|
|
122
|
+
dayShort: (d) => dayShort.format(utcDate(3 + d)),
|
|
123
|
+
number,
|
|
124
|
+
value: (v) => v > 0 ? [number(v), unitWord(v)].filter(Boolean).join(" ") : none,
|
|
125
|
+
unitTitle: unit == null ? "Value" : (typeof unit === "string" ? unit : unit.other).replace(/^./, (c) => c.toUpperCase())
|
|
126
|
+
};
|
|
127
|
+
}, [locale, startN, endN, formatValue, unit]);
|
|
128
|
+
const names = levelNames(levels);
|
|
129
|
+
const areaRef = useRef(null);
|
|
130
|
+
const scrollRef = useRef(null);
|
|
131
|
+
const [width, setWidth] = useState(null);
|
|
132
|
+
const measure = useCallback(() => setWidth(areaRef.current?.clientWidth ?? null), []);
|
|
133
|
+
useResizeObserver(measure, [areaRef]);
|
|
134
|
+
const cell = fitCell(width, grid.weeks.length, CELL_FLOOR[size], stretch);
|
|
135
|
+
const gap = gapFor(cell);
|
|
136
|
+
const opened = useRef(false);
|
|
137
|
+
useIsomorphicLayoutEffect(() => {
|
|
138
|
+
const region = scrollRef.current;
|
|
139
|
+
if (!region || width == null || opened.current) return;
|
|
140
|
+
opened.current = true;
|
|
141
|
+
const latest = last >= startN ? cellAt(last) : null;
|
|
142
|
+
if (!latest) return;
|
|
143
|
+
const overshoot = latest.getBoundingClientRect().right - region.getBoundingClientRect().right;
|
|
144
|
+
region.scrollLeft = Math.max(0, region.scrollLeft + overshoot + PAD + gap);
|
|
145
|
+
}, [width, cell]);
|
|
146
|
+
const latestActive = useMemo(() => {
|
|
147
|
+
for (let n = last; n >= startN; n--) if ((days.get(n)?.value ?? 0) > 0) return n;
|
|
148
|
+
return last >= startN ? last : null;
|
|
149
|
+
}, [days, last, startN]);
|
|
150
|
+
const [focusState, setFocusState] = useState(null);
|
|
151
|
+
const focusDay = navigable(focusState) ? focusState : latestActive;
|
|
152
|
+
const [uncontrolledActive, setUncontrolledActive] = useState(defaultActiveDate ? dayNumber(defaultActiveDate) : null);
|
|
153
|
+
const active = activeDate !== void 0 ? activeDate ? dayNumber(activeDate) : null : uncontrolledActive;
|
|
154
|
+
const setActive = (n) => {
|
|
155
|
+
if (n === active) return;
|
|
156
|
+
if (activeDate === void 0) setUncontrolledActive(n);
|
|
157
|
+
onActiveDateChange?.(n == null ? null : isoDay(n));
|
|
158
|
+
};
|
|
159
|
+
const [dismissed, setDismissed] = useState(false);
|
|
160
|
+
const selectable = onSelectDate != null;
|
|
161
|
+
const [uncontrolledSelected, setUncontrolledSelected] = useState(null);
|
|
162
|
+
const selected = selectedDate !== void 0 ? selectedDate ? dayNumber(selectedDate) : null : uncontrolledSelected;
|
|
163
|
+
const select = (n) => {
|
|
164
|
+
if (selectedDate === void 0) setUncontrolledSelected(n);
|
|
165
|
+
onSelectDate?.(isoDay(n));
|
|
166
|
+
};
|
|
167
|
+
const tableRef = useRef(null);
|
|
168
|
+
const cellAt = (n) => tableRef.current?.querySelector(`[data-date="${isoDay(n)}"]`) ?? null;
|
|
169
|
+
const dayOf = (target) => {
|
|
170
|
+
const el = target?.closest?.("td[data-date]");
|
|
171
|
+
return el?.dataset.date ? dayNumber(el.dataset.date) : null;
|
|
172
|
+
};
|
|
173
|
+
const moveFrom = (n, event) => {
|
|
174
|
+
const { week, row } = gridPosition(grid, n);
|
|
175
|
+
const inRow = (w) => grid.weeks[w]?.days[row];
|
|
176
|
+
const inColumn = (r) => grid.weeks[week].days[r];
|
|
177
|
+
const firstOf = (items, pick) => {
|
|
178
|
+
for (const item of items) {
|
|
179
|
+
const d = pick(item);
|
|
180
|
+
if (navigable(d)) return d;
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
};
|
|
184
|
+
const weeks = grid.weeks.map((_, i) => i);
|
|
185
|
+
const rows = [0, 1, 2, 3, 4, 5, 6];
|
|
186
|
+
switch (event.key) {
|
|
187
|
+
case "ArrowUp":
|
|
188
|
+
return row > 0 ? n - 1 : null;
|
|
189
|
+
case "ArrowDown":
|
|
190
|
+
return row < 6 ? n + 1 : null;
|
|
191
|
+
case "ArrowLeft":
|
|
192
|
+
return n - 7;
|
|
193
|
+
case "ArrowRight":
|
|
194
|
+
return n + 7;
|
|
195
|
+
case "Home":
|
|
196
|
+
return event.ctrlKey || event.metaKey ? startN : firstOf(weeks, inRow);
|
|
197
|
+
case "End":
|
|
198
|
+
return event.ctrlKey || event.metaKey ? last : firstOf([...weeks].reverse(), inRow);
|
|
199
|
+
case "PageUp":
|
|
200
|
+
return firstOf(rows, inColumn);
|
|
201
|
+
case "PageDown":
|
|
202
|
+
return firstOf([...rows].reverse(), inColumn);
|
|
203
|
+
default:
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const onKeyDown = (event) => {
|
|
208
|
+
const from = dayOf(event.target);
|
|
209
|
+
if (from == null) return;
|
|
210
|
+
if (event.key === "Escape") {
|
|
211
|
+
setDismissed(true);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if ((event.key === "Enter" || event.key === " ") && selectable) {
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
select(from);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const handled = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"].includes(event.key);
|
|
220
|
+
if (!handled) return;
|
|
221
|
+
event.preventDefault();
|
|
222
|
+
const next = moveFrom(from, event);
|
|
223
|
+
if (!navigable(next)) return;
|
|
224
|
+
setFocusState(next);
|
|
225
|
+
const el = cellAt(next);
|
|
226
|
+
el?.focus({ preventScroll: true });
|
|
227
|
+
el?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
228
|
+
};
|
|
229
|
+
const onFocus = (event) => {
|
|
230
|
+
const n = dayOf(event.target);
|
|
231
|
+
if (n == null) return;
|
|
232
|
+
setFocusState(n);
|
|
233
|
+
setDismissed(false);
|
|
234
|
+
setActive(n);
|
|
235
|
+
};
|
|
236
|
+
const onBlur = (event) => {
|
|
237
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setActive(null);
|
|
238
|
+
};
|
|
239
|
+
const onPointerOver = (event) => {
|
|
240
|
+
const n = dayOf(event.target);
|
|
241
|
+
if (!navigable(n)) return;
|
|
242
|
+
setDismissed(false);
|
|
243
|
+
setActive(n);
|
|
244
|
+
};
|
|
245
|
+
const onPointerLeave = (event) => {
|
|
246
|
+
const focused = event.currentTarget.contains(document.activeElement) ? dayOf(document.activeElement) : null;
|
|
247
|
+
setActive(focused);
|
|
248
|
+
};
|
|
249
|
+
const bodyRef = useRef(null);
|
|
250
|
+
const [tip, setTip] = useState(null);
|
|
251
|
+
const showTip = tooltip !== false && !loading && !dismissed && navigable(active);
|
|
252
|
+
useIsomorphicLayoutEffect(() => {
|
|
253
|
+
if (!showTip || active == null) {
|
|
254
|
+
setTip(null);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const place = () => {
|
|
258
|
+
const el = cellAt(active);
|
|
259
|
+
const host = bodyRef.current;
|
|
260
|
+
if (!el || !host) return setTip(null);
|
|
261
|
+
const c = el.getBoundingClientRect();
|
|
262
|
+
const h = host.getBoundingClientRect();
|
|
263
|
+
const x = c.left - h.left + c.width / 2;
|
|
264
|
+
setTip({ x, y: c.top - h.top, align: x < h.width * 0.2 ? "start" : x > h.width * 0.8 ? "end" : "center" });
|
|
265
|
+
};
|
|
266
|
+
place();
|
|
267
|
+
const region = scrollRef.current;
|
|
268
|
+
region?.addEventListener("scroll", place, { passive: true });
|
|
269
|
+
return () => region?.removeEventListener("scroll", place);
|
|
270
|
+
}, [showTip, active, cell]);
|
|
271
|
+
const tipDay = active != null && showTip ? resolve(active) : null;
|
|
272
|
+
const tipContent = tipDay == null ? null : typeof tooltip === "function" ? /* @__PURE__ */ jsx(ChartTooltipContent, { label: tooltip(tipDay), rows: [] }) : /* @__PURE__ */ jsx(
|
|
273
|
+
ChartTooltipContent,
|
|
274
|
+
{
|
|
275
|
+
label: words.short(dayNumber(tipDay.date)),
|
|
276
|
+
rows: [{ key: "value", label: tipDay.categoryLabel ?? words.unitTitle, value: tipDay.value > 0 ? words.number(tipDay.value) : "0", color: tipDay.color }]
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
const weeksCount = grid.weeks.length;
|
|
280
|
+
const showWeekday = (r) => weekdayLabels === "all" || weekdayLabels === "alternate" && SHOWN_WEEKDAYS.has((firstDay + r) % 7);
|
|
281
|
+
const vars = { "--heat-cell": `${cell}px`, "--heat-gap": `${gap}px` };
|
|
282
|
+
return /* @__PURE__ */ jsxs(
|
|
283
|
+
"figure",
|
|
284
|
+
{
|
|
285
|
+
ref: figureRef,
|
|
286
|
+
"data-reveal": shown ? "in" : "out",
|
|
287
|
+
"data-motion": motionAttribute(motion),
|
|
288
|
+
className: cn("m-0 flex w-full min-w-0 flex-col gap-2", className),
|
|
289
|
+
style: { ...vars, ...style },
|
|
290
|
+
...props,
|
|
291
|
+
children: [
|
|
292
|
+
/* @__PURE__ */ jsxs("div", { ref: bodyRef, className: "relative flex min-w-0 items-start", children: [
|
|
293
|
+
weekdayLabels !== "none" ? (
|
|
294
|
+
// Outside the scroller on purpose: labels that sat inside it, pinned
|
|
295
|
+
// over the cells, let the weeks slide under them on a phone.
|
|
296
|
+
/* @__PURE__ */ jsxs("div", { "aria-hidden": true, className: "flex shrink-0 flex-col gap-(--heat-gap) pe-1 pt-[calc(var(--heat-gap)+--spacing(1))] text-caption text-muted-foreground", children: [
|
|
297
|
+
monthLabels ? /* @__PURE__ */ jsx("span", { className: "h-5" }) : null,
|
|
298
|
+
[0, 1, 2, 3, 4, 5, 6].map((r) => /* @__PURE__ */ jsx("span", { className: "flex h-(--heat-cell) items-center leading-none", children: showWeekday(r) ? words.dayShort((firstDay + r) % 7) : null }, r))
|
|
299
|
+
] })
|
|
300
|
+
) : null,
|
|
301
|
+
/* @__PURE__ */ jsx("div", { ref: areaRef, className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(Scroller, { ref: scrollRef, orientation: "horizontal", tabIndex: -1, role: "presentation", "aria-label": void 0, children: /* @__PURE__ */ jsx("div", { className: "relative w-max p-1", children: /* @__PURE__ */ jsxs(
|
|
302
|
+
"table",
|
|
303
|
+
{
|
|
304
|
+
ref: tableRef,
|
|
305
|
+
role: "grid",
|
|
306
|
+
"aria-readonly": selectable ? void 0 : true,
|
|
307
|
+
"aria-busy": loading || void 0,
|
|
308
|
+
"aria-labelledby": `${uid}-caption`,
|
|
309
|
+
onKeyDown,
|
|
310
|
+
onFocus,
|
|
311
|
+
onBlur,
|
|
312
|
+
onPointerOver,
|
|
313
|
+
onPointerLeave,
|
|
314
|
+
onClick: (event) => {
|
|
315
|
+
const n = dayOf(event.target);
|
|
316
|
+
if (selectable && navigable(n)) select(n);
|
|
317
|
+
},
|
|
318
|
+
className: "border-separate border-spacing-(--heat-gap)",
|
|
319
|
+
children: [
|
|
320
|
+
/* @__PURE__ */ jsx("caption", { id: `${uid}-caption`, className: "sr-only", children: label }),
|
|
321
|
+
monthLabels ? /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
322
|
+
/* @__PURE__ */ jsx("td", { className: "sr-only" }),
|
|
323
|
+
grid.months.map((m, i) => /* @__PURE__ */ jsxs(
|
|
324
|
+
"th",
|
|
325
|
+
{
|
|
326
|
+
scope: "col",
|
|
327
|
+
colSpan: m.span,
|
|
328
|
+
className: "h-5 overflow-visible whitespace-nowrap p-0 text-start align-bottom text-caption text-muted-foreground",
|
|
329
|
+
children: [
|
|
330
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: words.monthLong(m.day) }),
|
|
331
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, children: m.span >= 3 || i > 0 && m.span >= 2 ? words.monthShort(m.day) : null })
|
|
332
|
+
]
|
|
333
|
+
},
|
|
334
|
+
m.day
|
|
335
|
+
))
|
|
336
|
+
] }) }) : null,
|
|
337
|
+
/* @__PURE__ */ jsx("tbody", { children: [0, 1, 2, 3, 4, 5, 6].map((r) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
338
|
+
/* @__PURE__ */ jsx("th", { scope: "row", className: "sr-only", children: words.dayLong((firstDay + r) % 7) }),
|
|
339
|
+
grid.weeks.map((week, w) => {
|
|
340
|
+
const n = week.days[r];
|
|
341
|
+
if (n == null) return /* @__PURE__ */ jsx("td", { "aria-hidden": true, className: "p-0" }, w);
|
|
342
|
+
if (n > last) {
|
|
343
|
+
return /* @__PURE__ */ jsx("td", { "aria-hidden": true, className: "size-(--heat-cell) rounded-xs border border-border-strong p-0" }, w);
|
|
344
|
+
}
|
|
345
|
+
const day = resolve(n);
|
|
346
|
+
const isActive = n === active && showTip;
|
|
347
|
+
return /* @__PURE__ */ jsx(
|
|
348
|
+
"td",
|
|
349
|
+
{
|
|
350
|
+
role: "gridcell",
|
|
351
|
+
"data-date": day.date,
|
|
352
|
+
"data-level": day.level,
|
|
353
|
+
"data-active": isActive || void 0,
|
|
354
|
+
tabIndex: n === focusDay ? 0 : -1,
|
|
355
|
+
"aria-label": `${words.long(n)}: ${words.value(day.value)}${day.categoryLabel ? `, ${day.categoryLabel}` : ""}`,
|
|
356
|
+
"aria-describedby": `${uid}-level-${day.level}`,
|
|
357
|
+
"aria-selected": selectable ? n === selected : void 0,
|
|
358
|
+
className: cn(
|
|
359
|
+
"relative size-(--heat-cell) rounded-[min(var(--radius-xs),calc(var(--heat-cell)/4))] p-0 outline-offset-1 outline-ring",
|
|
360
|
+
"focus-visible:outline-2 data-active:outline-2 aria-selected:outline-2 aria-selected:outline-foreground",
|
|
361
|
+
"[[data-reveal=out]_&]:opacity-0",
|
|
362
|
+
// Forced colours flatten every fill to one system colour, so the
|
|
363
|
+
// level becomes the size of an inner square instead.
|
|
364
|
+
"forced-colors:forced-color-adjust-none forced-colors:bg-[CanvasText] forced-colors:[clip-path:inset(var(--heat-inset))]",
|
|
365
|
+
loading && "motion-shimmer"
|
|
366
|
+
),
|
|
367
|
+
style: {
|
|
368
|
+
backgroundColor: day.color,
|
|
369
|
+
"--heat-inset": `${Math.round((levels - 1 - day.level) / Math.max(1, levels - 1) * 40)}%`,
|
|
370
|
+
transition: `${riseTransition(stagger(Math.round(w * 24 / Math.max(1, weeksCount - 1))))}, background-color var(--motion-duration-base) var(--motion-ease-standard)`
|
|
371
|
+
},
|
|
372
|
+
children: renderCell ? /* @__PURE__ */ jsx("span", { className: "absolute inset-0 flex items-center justify-center", children: renderCell(day, { level: day.level, color: day.color, size: cell }) }) : null
|
|
373
|
+
},
|
|
374
|
+
w
|
|
375
|
+
);
|
|
376
|
+
})
|
|
377
|
+
] }, r)) })
|
|
378
|
+
]
|
|
379
|
+
}
|
|
380
|
+
) }) }) }),
|
|
381
|
+
tip && tipContent ? /* @__PURE__ */ jsx(
|
|
382
|
+
"div",
|
|
383
|
+
{
|
|
384
|
+
"aria-hidden": true,
|
|
385
|
+
className: "pointer-events-none absolute start-0 top-0 z-10 transition-transform duration-(--motion-duration-fast) ease-standard",
|
|
386
|
+
style: { transform: `translate(${Math.round(tip.x)}px, ${Math.round(tip.y)}px)` },
|
|
387
|
+
children: /* @__PURE__ */ jsx(
|
|
388
|
+
"div",
|
|
389
|
+
{
|
|
390
|
+
className: cn(
|
|
391
|
+
"w-max -translate-y-full pb-2",
|
|
392
|
+
tip.align === "center" && "-translate-x-1/2",
|
|
393
|
+
tip.align === "end" && "-translate-x-full"
|
|
394
|
+
),
|
|
395
|
+
children: tipContent
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
) : null
|
|
400
|
+
] }),
|
|
401
|
+
/* @__PURE__ */ jsx("div", { hidden: true, children: names.map((name, k) => /* @__PURE__ */ jsx("span", { id: `${uid}-level-${k}`, children: name }, name)) }),
|
|
402
|
+
legend && categories ? /* @__PURE__ */ jsx(
|
|
403
|
+
ChartLegend,
|
|
404
|
+
{
|
|
405
|
+
className: "justify-start",
|
|
406
|
+
items: categoryKeys.map((key, i) => ({ key, ...categoryStyle(categories, key, i) }))
|
|
407
|
+
}
|
|
408
|
+
) : legend ? /* @__PURE__ */ jsxs("div", { "aria-hidden": true, className: "flex items-center justify-end gap-1 text-caption text-muted-foreground", children: [
|
|
409
|
+
/* @__PURE__ */ jsx("span", { className: "me-1", children: legendLabels[0] }),
|
|
410
|
+
names.map((name, k) => /* @__PURE__ */ jsx("span", { className: "size-2.5 rounded-[2px]", style: { backgroundColor: heatColor(base, k, levels) } }, name)),
|
|
411
|
+
/* @__PURE__ */ jsx("span", { className: "ms-1", children: legendLabels[1] })
|
|
412
|
+
] }) : null
|
|
413
|
+
]
|
|
414
|
+
}
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
ActivityHeatmap.displayName = "ActivityHeatmap";
|
|
418
|
+
export {
|
|
419
|
+
ActivityHeatmap
|
|
420
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/components/activity-heatmap/activity-heatmap.meta.ts
|
|
2
|
+
var activityHeatmapMeta = {
|
|
3
|
+
name: "ActivityHeatmap",
|
|
4
|
+
summary: "A calendar heatmap: one value per day as a grid of weeks by weekdays \u2014 the GitHub contribution graph, for any daily measure. Levels by quartile, categories by source, a real ARIA grid with one tab stop, and a sideways scroll on narrow screens that opens on the latest week.",
|
|
5
|
+
keywords: [
|
|
6
|
+
"heatmap",
|
|
7
|
+
"calendar heatmap",
|
|
8
|
+
"contribution graph",
|
|
9
|
+
"contributions",
|
|
10
|
+
"activity",
|
|
11
|
+
"github",
|
|
12
|
+
"streak",
|
|
13
|
+
"habit",
|
|
14
|
+
"daily",
|
|
15
|
+
"commits",
|
|
16
|
+
"uptime"
|
|
17
|
+
],
|
|
18
|
+
usage: {
|
|
19
|
+
description: "Use ActivityHeatmap to show how often something happened day by day over weeks or months: commits, habits kept, orders, uptime. Pass days as ISO dates with a value; levels, colours, labels, keyboard and tooltips work with no further props. Put it inside a Card and compose any totals beside it (NumberTicker, StatRowBlock). For a value over time that a reader needs to compare precisely, use AreaChart or BarChart; for a month calendar with marked days (a streak calendar), use Calendar.",
|
|
20
|
+
bestPractices: [
|
|
21
|
+
{
|
|
22
|
+
guidance: "do",
|
|
23
|
+
description: 'Pass dates as "YYYY-MM-DD" strings. The grid reads them as calendar days, so no time zone can shift a day \u2014 never build them with `new Date(...).toISOString()` from a local date.'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
guidance: "do",
|
|
27
|
+
description: 'Give it a `unit` ("contribution" / "contributions") so the tooltip and every cell\'s name say what the number counts.'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
guidance: "do",
|
|
31
|
+
description: "Let the default quantile scale pick levels \u2014 one exceptional day then does not wash every other day out. Pass a day's own `level` when the source already has one (GitHub's API)."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
guidance: "dont",
|
|
35
|
+
description: "Use it for a handful of values or for values a reader must compare exactly; a heatmap shows rhythm, not amounts. Use BarChart.",
|
|
36
|
+
instead: ["BarChart", "AreaChart"]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
guidance: "dont",
|
|
40
|
+
description: "Use it for a single month of marked days with a streak; that is Calendar with modifiers.",
|
|
41
|
+
instead: ["Calendar"]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
guidance: "dont",
|
|
45
|
+
description: "Fetch inside the component or call GitHub from the browser with a token. Fetch on your server and pass days in; `@sableui/react/adapters/github` converts GitHub's response."
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
anatomy: [
|
|
49
|
+
{ name: "Weekday labels", required: false, description: "Short weekday names beside the rows, outside the scroller so the weeks never slide under them." },
|
|
50
|
+
{ name: "Month labels", required: false, description: "Short month names over each month's first week; they scroll with their weeks." },
|
|
51
|
+
{ name: "Cell grid", required: true, description: "A role=grid table, one gridcell per day, inside a horizontal Scroller that opens on the latest week." },
|
|
52
|
+
{ name: "Tooltip", required: false, description: "The chart tooltip card, anchored to the hovered, focused or tapped day." },
|
|
53
|
+
{ name: "Legend", required: false, description: "A Less\u2013More ramp for intensity, or swatches for categories." }
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
related: {
|
|
57
|
+
components: ["Card", "NumberTicker", "BarChart", "Calendar", "Scroller"]
|
|
58
|
+
},
|
|
59
|
+
status: "stable",
|
|
60
|
+
props: [
|
|
61
|
+
{ name: "label", type: "string", description: 'What the grid shows ("Contributions in 2026") \u2014 its caption and accessible name.' },
|
|
62
|
+
{
|
|
63
|
+
name: "data",
|
|
64
|
+
type: "ActivityDay[] | Record<string, number>",
|
|
65
|
+
description: 'The days: `{ date: "YYYY-MM-DD", value, level?, category? }[]`, or a map of date to value. Sparse is fine \u2014 a missing day is 0; duplicates sum.'
|
|
66
|
+
},
|
|
67
|
+
{ name: "start", type: "string", default: "53 weeks before end", description: `First day drawn, "YYYY-MM-DD". The default starts at the beginning of that week, as GitHub's does.` },
|
|
68
|
+
{ name: "end", type: "string", default: "today", description: "Last day drawn. Days after today inside the range are drawn hollow and skipped by the keyboard." },
|
|
69
|
+
{ name: "year", type: "number", description: "Draw Jan 1 \u2013 Dec 31 of this year. Wins over start / end." },
|
|
70
|
+
{ name: "weekStart", type: "0 | 1", default: "the locale's", description: "0 puts Sunday on the top row, 1 Monday." },
|
|
71
|
+
{ name: "levels", type: "number", default: "5", description: "How many levels, including the empty level 0." },
|
|
72
|
+
{
|
|
73
|
+
name: "levelScale",
|
|
74
|
+
type: '"quantile" | "linear" | number[] | ((value, stats) => number)',
|
|
75
|
+
default: '"quantile"',
|
|
76
|
+
description: "How a value becomes a level. Quantile splits the positive values evenly, as GitHub does; an array gives each level's lower bound from 1 up; a function decides. A day's own `level` always wins."
|
|
77
|
+
},
|
|
78
|
+
{ name: "tone", type: "ChartTone", default: '"1"', description: 'The hue of the intensity ramp. Decorative tones lead, as on every chart; "success" gives the GitHub look.' },
|
|
79
|
+
{ name: "categories", type: "ChartCategories", description: "Colour days by `day.category` (label, tone or colour per key). Pair with `levels={2}` for flat category colours." },
|
|
80
|
+
{ name: "unit", type: "string | { one: string; other: string }", description: "Names the value in the tooltip and each cell's accessible name, pluralised with Intl.PluralRules." },
|
|
81
|
+
{ name: "formatValue", type: "(value: number) => string", default: "full to 9,999, compact past", description: "Formats a value wherever it is shown or read." },
|
|
82
|
+
{ name: "tooltip", type: "boolean | ((day: ActivityHeatmapDay) => ReactNode)", default: "true", description: "Show the tooltip on hover, focus and tap; a function replaces its content on the same card." },
|
|
83
|
+
{ name: "legend", type: "boolean", default: "true", description: "The Less\u2013More ramp for intensity, swatches for categories." },
|
|
84
|
+
{ name: "legendLabels", type: "[string, string]", default: '["Less", "More"]', description: "The ramp's end words." },
|
|
85
|
+
{ name: "monthLabels", type: "boolean", default: "true", description: "Short month names over each month's first week." },
|
|
86
|
+
{ name: "weekdayLabels", type: '"alternate" | "all" | "none"', default: '"alternate"', description: "Which weekday names to show beside the rows; alternate is Mon, Wed, Fri." },
|
|
87
|
+
{ name: "size", type: '"sm" | "md" | "lg"', default: '"md"', description: "The smallest a cell may get (10, 12, 14 px) before the grid scrolls instead of shrinking." },
|
|
88
|
+
{ name: "stretch", type: "boolean", default: "true", description: "Grow cells to fill the width, up to 20 px. false keeps them at `size`." },
|
|
89
|
+
{ name: "activeDate", type: "string | null", description: "The hovered or focused day, controlled. Pair with defaultActiveDate / onActiveDateChange." },
|
|
90
|
+
{ name: "selectedDate", type: "string | null", description: "The selected day, controlled. Selection is on only when onSelectDate is set." },
|
|
91
|
+
{ name: "onSelectDate", type: "(date: string) => void", description: "Called when a day is clicked or chosen with Enter / Space \u2014 to filter a feed below, as GitHub does." },
|
|
92
|
+
{ name: "renderCell", type: "(day, cell) => ReactNode", description: "Draw something over a cell's fill \u2014 an icon for a milestone, a reward, an outage." },
|
|
93
|
+
{ name: "loading", type: "boolean", default: "false", description: "Draw the empty grid, shimmering, while data loads." },
|
|
94
|
+
{ name: "motion", type: '"full" | "subtle" | "off"', default: '"full"', description: "Turns the arrival down; `subtle` shows the finished grid at once." },
|
|
95
|
+
{ name: "locale", type: "string", default: "runtime locale", description: "Month and weekday names, numbers and plurals." }
|
|
96
|
+
],
|
|
97
|
+
tokensUsed: ["foreground", "muted-foreground", "border-strong", "ring", "tone-1-mark", "tone-2-mark", "tone-3-mark", "success-mark", "tone-info-mark"],
|
|
98
|
+
motion: [
|
|
99
|
+
{
|
|
100
|
+
slot: "arrival",
|
|
101
|
+
recipe: "custom",
|
|
102
|
+
part: "Cell grid",
|
|
103
|
+
why: "Weeks fade in left to right as the grid first scrolls into view, staggered on the chart motion helpers; expressive, so subtle shows the finished grid."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
slot: "response",
|
|
107
|
+
recipe: "custom",
|
|
108
|
+
part: "Cell",
|
|
109
|
+
why: "The active day takes a ring outline at once and the tooltip glides to it; the other cells stay still \u2014 dimming hundreds of cells reads as a flash."
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
slot: "transition",
|
|
113
|
+
recipe: "custom",
|
|
114
|
+
part: "Cell",
|
|
115
|
+
why: "When data or the range changes, each cell's fill cross-fades on the base duration token, so the reader sees which days changed."
|
|
116
|
+
},
|
|
117
|
+
{ slot: "activity", recipe: "motion-shimmer", part: "Cell grid", why: "`loading` shimmers the empty grid until days arrive." }
|
|
118
|
+
],
|
|
119
|
+
a11y: [
|
|
120
|
+
"The grid is a role=grid table, as GitHub's is: weekday row headers and month column headers carry full names for screen readers, and the caption is `label`.",
|
|
121
|
+
"One tab stop. Arrows move a day or a week; Home / End go to the ends of the row, Page Up / Page Down to the ends of the column, Ctrl+Home / Ctrl+End to the first and last day. Enter or Space selects when onSelectDate is set; Escape hides the tooltip.",
|
|
122
|
+
`Each cell is named with its date and value ("Tuesday, October 14, 2026: 12 contributions") and described by its level's words, which the legend repeats visually.`,
|
|
123
|
+
"Levels rise in lightness, so they read without colour; under forced colours each level draws as a larger inner square."
|
|
124
|
+
],
|
|
125
|
+
examples: [
|
|
126
|
+
{
|
|
127
|
+
label: "Contributions, last year",
|
|
128
|
+
code: 'const days = [\n { date: "2026-10-12", value: 4 },\n { date: "2026-10-13", value: 11 },\n { date: "2026-10-14", value: 2 },\n]\n\n<ActivityHeatmap label="Contributions in the last year" data={days} tone="success" unit={{ one: "contribution", other: "contributions" }} />'
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
label: "Two sources, in a card",
|
|
132
|
+
code: 'const github = [{ date: "2026-10-13", value: 6 }]\nconst codex = [{ date: "2026-10-13", value: 1 }, { date: "2026-10-14", value: 3 }]\n\n<Card>\n <CardHeader>\n <CardTitle>Building, day by day</CardTitle>\n </CardHeader>\n <CardContent>\n <ActivityHeatmap\n label="GitHub and Codex activity in 2026"\n year={2026}\n levels={2}\n data={mergeActivity({ github, codex })}\n categories={{ github: { label: "GitHub", tone: "success" }, codex: { label: "Codex", tone: "info" }, both: { label: "Both", tone: "2" } }}\n />\n </CardContent>\n</Card>'
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
label: "Selecting a day",
|
|
136
|
+
code: 'const [day, setDay] = useState<string | null>(null)\n\n<ActivityHeatmap label="Orders per day" data={{ "2026-10-13": 18, "2026-10-14": 25 }} unit="orders" selectedDate={day} onSelectDate={setDay} />'
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
};
|
|
140
|
+
export {
|
|
141
|
+
activityHeatmapMeta
|
|
142
|
+
};
|