@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.
- package/AGENTS.md +10 -3
- package/CHANGELOG.md +132 -0
- package/README.md +11 -4
- package/bin/sable.mjs +160 -18
- 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/approval-block.js +30 -48
- package/dist/blocks/approval-block.meta.js +7 -8
- 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/code-block/code-block.js +5 -3
- package/dist/components/code-block/code-block.meta.js +6 -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/page-header/page-header.js +9 -4
- package/dist/components/page-header/page-header.meta.js +26 -1
- 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/section-rail/index.js +7 -0
- package/dist/components/section-rail/section-rail.js +153 -0
- package/dist/components/section-rail/section-rail.meta.js +189 -0
- package/dist/components/sheet/sheet.js +15 -3
- package/dist/components/sheet/sheet.meta.js +2 -2
- package/dist/file-hashes.json +53 -39
- package/dist/icon-sets/reicon.js +23 -1
- package/dist/index.d.ts +364 -27
- package/dist/index.js +11 -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/focus-return.js +53 -0
- package/dist/lib/taxonomy.js +2 -0
- package/dist/lib/use-active-section.js +59 -0
- package/dist/styles.css +122 -114
- package/llms/blocks/approval-block.md +40 -55
- package/llms/components/activity-heatmap.md +133 -0
- package/llms/components/code-block.md +1 -0
- 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/page-header.md +16 -1
- 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/section-rail.md +101 -0
- package/llms/components/sheet.md +2 -2
- 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/tasks/auditing-an-app.md +31 -0
- package/llms/tasks/getting-started.md +10 -3
- package/llms/tasks/using-presets.md +1 -2
- package/llms-full.txt +516 -90
- package/llms.txt +5 -3
- package/package.json +6 -1
- package/registry.json +745 -46
- package/scripts/adoption.mjs +239 -0
- package/scripts/agent-setup.mjs +110 -18
- package/scripts/grade.mjs +17 -0
- package/scripts/wiring.mjs +2 -1
- package/skills/entry/SKILL.md +1 -1
- package/skills/sable/SKILL.md +33 -4
- package/skills/sable/agents/openai.yaml +1 -1
- 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,
|
|
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 {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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__ */
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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: "
|
|
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
|
-
|
|
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:
|
|
8
|
-
intent:
|
|
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
|
-
|
|
11
|
-
"The
|
|
12
|
-
"
|
|
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
|
|
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 {
|
package/dist/blocks/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
+
};
|