@sableui/react 0.16.0 → 0.18.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.
Files changed (95) hide show
  1. package/AGENTS.md +10 -3
  2. package/CHANGELOG.md +132 -0
  3. package/README.md +11 -4
  4. package/bin/sable.mjs +160 -18
  5. package/dist/activity-DT8VK9fx.d.ts +25 -0
  6. package/dist/adapters/github.d.ts +41 -0
  7. package/dist/adapters/github.js +24 -0
  8. package/dist/blocks/approval-block.js +30 -48
  9. package/dist/blocks/approval-block.meta.js +7 -8
  10. package/dist/blocks/index.d.ts +1 -1
  11. package/dist/components/activity-heatmap/activity-heatmap.js +420 -0
  12. package/dist/components/activity-heatmap/activity-heatmap.meta.js +142 -0
  13. package/dist/components/activity-heatmap/index.js +9 -0
  14. package/dist/components/code-block/code-block.js +5 -3
  15. package/dist/components/code-block/code-block.meta.js +6 -0
  16. package/dist/components/dialog/dialog.js +25 -5
  17. package/dist/components/dialog/dialog.meta.js +26 -5
  18. package/dist/components/media-image/media-image.js +3 -1
  19. package/dist/components/media-image/media-image.meta.js +15 -1
  20. package/dist/components/media-tile/media-tile.js +13 -1
  21. package/dist/components/media-tile/media-tile.meta.js +10 -0
  22. package/dist/components/page-header/page-header.js +9 -4
  23. package/dist/components/page-header/page-header.meta.js +26 -1
  24. package/dist/components/pill/pill.meta.js +15 -6
  25. package/dist/components/pill/pill.variants.js +1 -0
  26. package/dist/components/search-input/search-input.js +30 -5
  27. package/dist/components/search-input/search-input.meta.js +29 -4
  28. package/dist/components/section-header/section-header.js +27 -17
  29. package/dist/components/section-header/section-header.meta.js +27 -5
  30. package/dist/components/section-rail/index.js +7 -0
  31. package/dist/components/section-rail/section-rail.js +153 -0
  32. package/dist/components/section-rail/section-rail.meta.js +189 -0
  33. package/dist/components/sheet/sheet.js +15 -3
  34. package/dist/components/sheet/sheet.meta.js +2 -2
  35. package/dist/file-hashes.json +53 -39
  36. package/dist/icon-sets/reicon.js +23 -1
  37. package/dist/index.d.ts +364 -27
  38. package/dist/index.js +11 -1
  39. package/dist/layout/AspectBox.js +2 -5
  40. package/dist/layout/Bleed.js +10 -6
  41. package/dist/layout/Center.js +11 -3
  42. package/dist/layout/Columns.js +6 -3
  43. package/dist/layout/Grid.js +14 -6
  44. package/dist/layout/GridOverlay.js +7 -7
  45. package/dist/layout/Region.js +18 -3
  46. package/dist/layout/Row.js +27 -20
  47. package/dist/layout/Spacer.js +13 -2
  48. package/dist/layout/SplitPane.js +1 -1
  49. package/dist/layout/Stack.js +25 -18
  50. package/dist/layout/index.d.ts +57 -6
  51. package/dist/lib/chart/activity.js +32 -0
  52. package/dist/lib/chart/calendar.js +67 -0
  53. package/dist/lib/chart/chart-tooltip.js +2 -2
  54. package/dist/lib/chart/heat.js +55 -0
  55. package/dist/lib/focus-return.js +53 -0
  56. package/dist/lib/taxonomy.js +2 -0
  57. package/dist/lib/use-active-section.js +59 -0
  58. package/dist/styles.css +122 -114
  59. package/llms/blocks/approval-block.md +40 -55
  60. package/llms/components/activity-heatmap.md +133 -0
  61. package/llms/components/code-block.md +1 -0
  62. package/llms/components/dialog.md +34 -3
  63. package/llms/components/media-image.md +12 -0
  64. package/llms/components/media-tile.md +21 -0
  65. package/llms/components/page-header.md +16 -1
  66. package/llms/components/pill.md +24 -5
  67. package/llms/components/search-input.md +37 -4
  68. package/llms/components/section-header.md +28 -4
  69. package/llms/components/section-rail.md +101 -0
  70. package/llms/components/sheet.md +2 -2
  71. package/llms/layout/aspect-box.md +1 -1
  72. package/llms/layout/bleed.md +1 -1
  73. package/llms/layout/center.md +1 -1
  74. package/llms/layout/columns.md +1 -1
  75. package/llms/layout/grid.md +1 -1
  76. package/llms/layout/region.md +1 -1
  77. package/llms/layout/row.md +9 -1
  78. package/llms/layout/spacer.md +1 -1
  79. package/llms/layout/split-pane.md +1 -1
  80. package/llms/layout/stack.md +1 -1
  81. package/llms/tasks/auditing-an-app.md +31 -0
  82. package/llms/tasks/getting-started.md +10 -3
  83. package/llms/tasks/using-presets.md +1 -2
  84. package/llms-full.txt +516 -90
  85. package/llms.txt +5 -3
  86. package/package.json +6 -1
  87. package/registry.json +745 -46
  88. package/scripts/adoption.mjs +239 -0
  89. package/scripts/agent-setup.mjs +110 -18
  90. package/scripts/grade.mjs +17 -0
  91. package/scripts/wiring.mjs +2 -1
  92. package/skills/entry/SKILL.md +1 -1
  93. package/skills/sable/SKILL.md +33 -4
  94. package/skills/sable/agents/openai.yaml +1 -1
  95. package/targets/ios.json +18 -0
@@ -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
+ };
@@ -1,59 +1,48 @@
1
1
  // src/blocks/approval-block.tsx
2
- import { useEffect, useId, useRef, useState } from "react";
2
+ import { useEffect, useRef, useState } from "react";
3
3
  import { Button } from "../components/button/index.js";
4
4
  import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../components/card/index.js";
5
- import { Checkbox } from "../components/checkbox/index.js";
6
- import { Code } from "../components/code/index.js";
7
5
  import { InlineStatus } from "../components/inline-status/index.js";
8
- import { Label } from "../components/label/index.js";
6
+ import { ToolCall } from "../components/tool-call/index.js";
9
7
  import { cn } from "../lib/cn.js";
10
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
9
  var request = {
12
- tool: "send_email",
13
- title: "Send the invoice to Ada?",
14
- description: "The agent wants to email invoice INV-1042. An email can't be unsent.",
15
- details: [
16
- { label: "To", value: "ada@example.com" },
17
- { label: "Subject", value: "INV-1042 for October" },
18
- { label: "Attached", value: "INV-1042.pdf" }
19
- ],
20
- allowed: "Allowed. Sending the email.",
21
- denied: "Denied. The agent won't send it."
10
+ title: "Run this command?",
11
+ description: "The agent wants to migrate the production database in acme/api. It changes live data.",
12
+ tool: "run_command",
13
+ action: "Run a command",
14
+ // A command reads as the command; a structured call would pass its arguments object.
15
+ input: "pnpm db:migrate --env production",
16
+ allowed: "Allowed. Running the migration.",
17
+ denied: "Denied. The agent won't run it."
22
18
  };
23
19
  function ApprovalBlock({ className, framed = true, onApprove, onDeny }) {
24
20
  const [decision, setDecision] = useState(null);
25
- const [remember, setRemember] = useState(false);
26
- const id = useId();
27
21
  const outcome = useRef(null);
28
22
  useEffect(() => {
29
23
  if (decision) outcome.current?.focus();
30
24
  }, [decision]);
25
+ const approve = (remember) => {
26
+ setDecision("allowed");
27
+ onApprove?.({ remember });
28
+ };
31
29
  return /* @__PURE__ */ jsxs(Card, { variant: framed ? "default" : "flat", className: cn("w-full max-w-md", className), children: [
32
30
  /* @__PURE__ */ jsxs(CardHeader, { children: [
33
31
  /* @__PURE__ */ jsx(CardTitle, { children: request.title }),
34
32
  /* @__PURE__ */ jsx(CardDescription, { children: request.description })
35
33
  ] }),
36
- /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col gap-4", children: [
37
- /* @__PURE__ */ jsxs("dl", { className: "grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-body-sm", children: [
38
- request.details.map((d) => /* @__PURE__ */ jsxs("div", { className: "contents", children: [
39
- /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: d.label }),
40
- /* @__PURE__ */ jsx("dd", { className: "truncate text-foreground", children: d.value })
41
- ] }, d.label)),
42
- /* @__PURE__ */ jsxs("div", { className: "contents", children: [
43
- /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Tool" }),
44
- /* @__PURE__ */ jsx("dd", { className: "min-w-0", children: /* @__PURE__ */ jsx(Code, { children: request.tool }) })
45
- ] })
46
- ] }),
47
- decision ? null : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
48
- /* @__PURE__ */ jsx(Checkbox, { id: `${id}-remember`, checked: remember, onCheckedChange: (v) => setRemember(v) }),
49
- /* @__PURE__ */ jsxs(Label, { htmlFor: `${id}-remember`, className: "text-body-sm", children: [
50
- "Allow ",
51
- /* @__PURE__ */ jsx(Code, { children: request.tool }),
52
- " for the rest of this chat"
53
- ] })
54
- ] })
55
- ] }),
56
- /* @__PURE__ */ jsx(CardFooter, { actions: true, className: "justify-end", children: decision ? /* @__PURE__ */ jsx(
34
+ /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx(
35
+ ToolCall,
36
+ {
37
+ name: request.tool,
38
+ title: request.action,
39
+ status: decision === "allowed" ? "complete" : decision === "denied" ? "denied" : "awaiting",
40
+ statusLabel: decision === "allowed" ? "Allowed" : void 0,
41
+ input: request.input,
42
+ defaultOpen: true
43
+ }
44
+ ) }),
45
+ /* @__PURE__ */ jsx(CardFooter, { actions: true, children: decision ? /* @__PURE__ */ jsx(
57
46
  InlineStatus,
58
47
  {
59
48
  ref: outcome,
@@ -67,7 +56,8 @@ function ApprovalBlock({ className, framed = true, onApprove, onDeny }) {
67
56
  /* @__PURE__ */ jsx(
68
57
  Button,
69
58
  {
70
- variant: "outline",
59
+ variant: "ghost",
60
+ className: "me-auto",
71
61
  onClick: () => {
72
62
  setDecision("denied");
73
63
  onDeny?.();
@@ -75,16 +65,8 @@ function ApprovalBlock({ className, framed = true, onApprove, onDeny }) {
75
65
  children: "Deny"
76
66
  }
77
67
  ),
78
- /* @__PURE__ */ jsx(
79
- Button,
80
- {
81
- onClick: () => {
82
- setDecision("allowed");
83
- onApprove?.({ remember });
84
- },
85
- children: "Allow"
86
- }
87
- )
68
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => approve(true), children: "Always allow" }),
69
+ /* @__PURE__ */ jsx(Button, { onClick: () => approve(false), children: "Allow once" })
88
70
  ] }) })
89
71
  ] });
90
72
  }
@@ -4,18 +4,17 @@ var approvalBlockMeta = {
4
4
  family: "AI & agents",
5
5
  kind: "card",
6
6
  width: { min: "18rem", max: "28rem" },
7
- summary: `An agent asking permission for one tool call: the question, what will happen and what can't be undone, the call's details, an "allow for the rest of this chat" Checkbox, and Deny / Allow.`,
8
- intent: 'Reach for this in a chat or agent run when a tool call waits on the person \u2014 sending, paying, deleting, publishing. The call itself, before and after the decision, is a ToolCall with status="awaiting", then "complete" or "denied". A destructive action outside an agent is DestructiveActionModal.',
7
+ summary: "An agent asking permission for one tool call: the question, what is at stake, the call itself as a ToolCall with its exact input, and Deny / Always allow / Allow once.",
8
+ intent: `Reach for this in a chat or agent run when a tool call waits on the person \u2014 running a command, sending, paying, deleting, publishing. It wraps the call's own ToolCall, which shows status="awaiting" until the person answers and then "complete" or "denied". A destructive action outside an agent is DestructiveActionModal.`,
9
9
  notes: [
10
- `The title is the question in plain words ("Send the invoice to Ada?"), not the tool's name; the tool's name is one row of the details, for the person who wants to know exactly what runs.`,
11
- "The description says what can't be undone. That sentence is what the person is deciding on.",
12
- "Deny comes first and Allow is the primary on the end, the same order as every Sable confirmation. Neither is focused on arrival: an approval that a stray Enter can grant is not an approval.",
13
- "After a decision the buttons give way to one InlineStatus line saying what happened, so the thread keeps a record of the answer."
10
+ 'The title is the question in plain words ("Run this command?"); the description says what is at stake. That sentence is what the person is deciding on.',
11
+ "The call is shown as the agent made it \u2014 tool name and raw input, open on arrival \u2014 because the exact arguments are what a careful person checks. Do not paraphrase them into a form.",
12
+ "Three answers, as agent products settle on: Deny on the start edge, then Always allow and Allow once, the primary on the end. Neither is focused on arrival: an approval that a stray Enter can grant is not an approval.",
13
+ "After a decision the buttons give way to one InlineStatus line saying what happened, and the ToolCall shows the outcome, so the thread keeps a record of the answer."
14
14
  ],
15
15
  a11y: [
16
16
  "Focus moves to the outcome line when the buttons leave, instead of dropping to the page; the line is a role=status region, so the decision is announced.",
17
- "The details are a dl, so each value is read with its label.",
18
- "The remember Checkbox has its own Label, so the tool name inside it is part of its accessible name."
17
+ "The ToolCall announces its own status change, and its input is a labelled code view."
19
18
  ]
20
19
  };
21
20
  export {
@@ -17,7 +17,7 @@ interface ApprovalBlockProps {
17
17
  className?: string;
18
18
  /** false drops the card's surface and border — for a chat column that already frames it. */
19
19
  framed?: boolean;
20
- /** The person allowed the call. `remember` is the "for the rest of this chat" choice. */
20
+ /** The person allowed the call. `remember` is true for "Always allow". */
21
21
  onApprove?: (options: {
22
22
  remember: boolean;
23
23
  }) => void;
@@ -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
+ };