@sableui/react 0.17.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 (86) hide show
  1. package/AGENTS.md +10 -3
  2. package/CHANGELOG.md +106 -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/components/activity-heatmap/activity-heatmap.js +420 -0
  9. package/dist/components/activity-heatmap/activity-heatmap.meta.js +142 -0
  10. package/dist/components/activity-heatmap/index.js +9 -0
  11. package/dist/components/code-block/code-block.js +5 -3
  12. package/dist/components/code-block/code-block.meta.js +6 -0
  13. package/dist/components/dialog/dialog.js +25 -5
  14. package/dist/components/dialog/dialog.meta.js +26 -5
  15. package/dist/components/media-image/media-image.js +3 -1
  16. package/dist/components/media-image/media-image.meta.js +15 -1
  17. package/dist/components/media-tile/media-tile.js +13 -1
  18. package/dist/components/media-tile/media-tile.meta.js +10 -0
  19. package/dist/components/page-header/page-header.js +9 -4
  20. package/dist/components/page-header/page-header.meta.js +26 -1
  21. package/dist/components/pill/pill.meta.js +15 -6
  22. package/dist/components/pill/pill.variants.js +1 -0
  23. package/dist/components/search-input/search-input.js +30 -5
  24. package/dist/components/search-input/search-input.meta.js +29 -4
  25. package/dist/components/section-header/section-header.js +27 -17
  26. package/dist/components/section-header/section-header.meta.js +27 -5
  27. package/dist/components/sheet/sheet.js +15 -3
  28. package/dist/components/sheet/sheet.meta.js +2 -2
  29. package/dist/file-hashes.json +46 -36
  30. package/dist/icon-sets/reicon.js +23 -1
  31. package/dist/index.d.ts +270 -27
  32. package/dist/index.js +8 -1
  33. package/dist/layout/AspectBox.js +2 -5
  34. package/dist/layout/Bleed.js +10 -6
  35. package/dist/layout/Center.js +11 -3
  36. package/dist/layout/Columns.js +6 -3
  37. package/dist/layout/Grid.js +14 -6
  38. package/dist/layout/GridOverlay.js +7 -7
  39. package/dist/layout/Region.js +18 -3
  40. package/dist/layout/Row.js +27 -20
  41. package/dist/layout/Spacer.js +13 -2
  42. package/dist/layout/SplitPane.js +1 -1
  43. package/dist/layout/Stack.js +25 -18
  44. package/dist/layout/index.d.ts +57 -6
  45. package/dist/lib/chart/activity.js +32 -0
  46. package/dist/lib/chart/calendar.js +67 -0
  47. package/dist/lib/chart/chart-tooltip.js +2 -2
  48. package/dist/lib/chart/heat.js +55 -0
  49. package/dist/lib/focus-return.js +53 -0
  50. package/dist/lib/taxonomy.js +1 -0
  51. package/dist/styles.css +122 -114
  52. package/llms/components/activity-heatmap.md +133 -0
  53. package/llms/components/code-block.md +1 -0
  54. package/llms/components/dialog.md +34 -3
  55. package/llms/components/media-image.md +12 -0
  56. package/llms/components/media-tile.md +21 -0
  57. package/llms/components/page-header.md +16 -1
  58. package/llms/components/pill.md +24 -5
  59. package/llms/components/search-input.md +37 -4
  60. package/llms/components/section-header.md +28 -4
  61. package/llms/components/sheet.md +2 -2
  62. package/llms/layout/aspect-box.md +1 -1
  63. package/llms/layout/bleed.md +1 -1
  64. package/llms/layout/center.md +1 -1
  65. package/llms/layout/columns.md +1 -1
  66. package/llms/layout/grid.md +1 -1
  67. package/llms/layout/region.md +1 -1
  68. package/llms/layout/row.md +9 -1
  69. package/llms/layout/spacer.md +1 -1
  70. package/llms/layout/split-pane.md +1 -1
  71. package/llms/layout/stack.md +1 -1
  72. package/llms/tasks/auditing-an-app.md +31 -0
  73. package/llms/tasks/getting-started.md +10 -3
  74. package/llms/tasks/using-presets.md +1 -2
  75. package/llms-full.txt +372 -35
  76. package/llms.txt +3 -2
  77. package/package.json +6 -1
  78. package/registry.json +532 -36
  79. package/scripts/adoption.mjs +239 -0
  80. package/scripts/agent-setup.mjs +110 -18
  81. package/scripts/grade.mjs +17 -0
  82. package/scripts/wiring.mjs +2 -1
  83. package/skills/entry/SKILL.md +1 -1
  84. package/skills/sable/SKILL.md +33 -4
  85. package/skills/sable/agents/openai.yaml +1 -1
  86. package/targets/ios.json +9 -0
@@ -21,15 +21,23 @@ function Grid({
21
21
  }) {
22
22
  if (minItemWidth) {
23
23
  const mergedStyle2 = {
24
- display: "grid",
25
- gridTemplateColumns: `repeat(${track === "fit" ? "auto-fit" : "auto-fill"}, minmax(min(${minItemWidth}, 100%), 1fr))`,
26
- gap: resolveGap(gap),
24
+ "--sable-grid-gap": resolveGap(gap),
25
+ "--sable-grid-template": `repeat(${track === "fit" ? "auto-fit" : "auto-fill"}, minmax(min(${minItemWidth}, 100%), 1fr))`,
27
26
  ...style
28
27
  };
29
- return /* @__PURE__ */ jsx("div", { ref, className: cn(className), style: mergedStyle2, ...rest, children });
28
+ return /* @__PURE__ */ jsx(
29
+ "div",
30
+ {
31
+ ref,
32
+ className: cn("grid grid-cols-(--sable-grid-template) gap-(--sable-grid-gap)", className),
33
+ style: mergedStyle2,
34
+ ...rest,
35
+ children
36
+ }
37
+ );
30
38
  }
31
39
  const mergedStyle = {
32
- gap: resolveGap(gap),
40
+ "--sable-grid-gap": resolveGap(gap),
33
41
  ...responsiveVars("--cols", cols),
34
42
  ...style
35
43
  };
@@ -38,7 +46,7 @@ function Grid({
38
46
  {
39
47
  ref,
40
48
  "data-sable-grid-cols": true,
41
- className: cn(className),
49
+ className: cn("grid gap-(--sable-grid-gap)", className),
42
50
  style: mergedStyle,
43
51
  ...rest,
44
52
  children
@@ -10,18 +10,18 @@ function GridOverlay({
10
10
  }) {
11
11
  if (!show) return null;
12
12
  const style = {
13
- position: "absolute",
14
- inset: 0,
15
- display: "grid",
16
- gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
17
- gap,
18
- paddingInline: inset ? "var(--page-inset)" : void 0
13
+ "--sable-overlay-cols": `repeat(${columns}, minmax(0, 1fr))`,
14
+ "--sable-overlay-gap": gap
19
15
  };
20
16
  return /* @__PURE__ */ jsx(
21
17
  "div",
22
18
  {
23
19
  "aria-hidden": true,
24
- className: cn("pointer-events-none z-(--z-overlay)", className),
20
+ className: cn(
21
+ "pointer-events-none absolute inset-0 z-(--z-overlay) grid grid-cols-(--sable-overlay-cols) gap-(--sable-overlay-gap)",
22
+ inset && "px-(--page-inset)",
23
+ className
24
+ ),
25
25
  style,
26
26
  children: Array.from({ length: columns }, (_, i) => /* @__PURE__ */ jsx(
27
27
  "div",
@@ -27,12 +27,27 @@ function Region({
27
27
  const maxWidth = max ? NAMED_MAX[max] ?? max : void 0;
28
28
  const safeEdges = safeArea === true ? ["top", "bottom"] : Array.isArray(safeArea) ? safeArea : [];
29
29
  const mergedStyle = {
30
- ...maxWidth ? { maxWidth, marginInline: "auto" } : null,
31
- ...!bleed && inset ? { paddingInline: "var(--page-inset)" } : null,
30
+ ...maxWidth ? { "--sable-region-max": maxWidth } : null,
32
31
  ...safeAreaInsets(safeEdges),
33
32
  ...style
34
33
  };
35
- return /* @__PURE__ */ jsx(Comp, { ref, className: cn("w-full", className), style: mergedStyle, ...rest, children });
34
+ return /* @__PURE__ */ jsx(
35
+ Comp,
36
+ {
37
+ ref,
38
+ className: cn(
39
+ "w-full",
40
+ // The cap class only with a cap: the variable inherits, so an uncapped
41
+ // Region inside a capped one would otherwise take its ancestor's width.
42
+ maxWidth && "mx-auto max-w-(--sable-region-max)",
43
+ !bleed && inset && "px-(--page-inset)",
44
+ className
45
+ ),
46
+ style: mergedStyle,
47
+ ...rest,
48
+ children
49
+ }
50
+ );
36
51
  }
37
52
  export {
38
53
  Region
@@ -2,18 +2,18 @@
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  var ALIGN = {
5
- start: "flex-start",
6
- center: "center",
7
- end: "flex-end",
8
- stretch: "stretch",
9
- baseline: "baseline"
5
+ start: "items-start",
6
+ center: "items-center",
7
+ end: "items-end",
8
+ stretch: "items-stretch",
9
+ baseline: "items-baseline"
10
10
  };
11
11
  var JUSTIFY = {
12
- start: "flex-start",
13
- center: "center",
14
- end: "flex-end",
15
- between: "space-between",
16
- around: "space-around"
12
+ start: "justify-start",
13
+ center: "justify-center",
14
+ end: "justify-end",
15
+ between: "justify-between",
16
+ around: "justify-around"
17
17
  };
18
18
  function resolveGap(gap) {
19
19
  return typeof gap === "number" ? `calc(var(--rhythm-unit) * ${gap})` : gap;
@@ -29,16 +29,23 @@ function Row({
29
29
  ref,
30
30
  ...rest
31
31
  }) {
32
- const mergedStyle = {
33
- display: "flex",
34
- flexDirection: "row",
35
- alignItems: ALIGN[align],
36
- justifyContent: JUSTIFY[justify],
37
- flexWrap: wrap ? "wrap" : "nowrap",
38
- gap: resolveGap(gap),
39
- ...style
40
- };
41
- return /* @__PURE__ */ jsx("div", { ref, className: cn(className), style: mergedStyle, ...rest, children });
32
+ const mergedStyle = { "--sable-row-gap": resolveGap(gap), ...style };
33
+ return /* @__PURE__ */ jsx(
34
+ "div",
35
+ {
36
+ ref,
37
+ className: cn(
38
+ "flex flex-row gap-(--sable-row-gap)",
39
+ ALIGN[align],
40
+ JUSTIFY[justify],
41
+ wrap ? "flex-wrap" : "flex-nowrap",
42
+ className
43
+ ),
44
+ style: mergedStyle,
45
+ ...rest,
46
+ children
47
+ }
48
+ );
42
49
  }
43
50
  export {
44
51
  Row
@@ -1,8 +1,19 @@
1
1
  // src/layout/Spacer.tsx
2
+ import { cn } from "../lib/cn.js";
2
3
  import { jsx } from "react/jsx-runtime";
3
4
  function Spacer({ size = 1, axis = "vertical", grow, className }) {
4
- const style = grow ? { flex: "1 1 0%" } : axis === "vertical" ? { height: `calc(var(--rhythm-unit) * ${size})`, flexShrink: 0 } : { width: `calc(var(--rhythm-unit) * ${size})`, flexShrink: 0 };
5
- return /* @__PURE__ */ jsx("div", { "aria-hidden": true, className, style });
5
+ const style = grow ? void 0 : { "--sable-spacer": `calc(var(--rhythm-unit) * ${size})` };
6
+ return /* @__PURE__ */ jsx(
7
+ "div",
8
+ {
9
+ "aria-hidden": true,
10
+ className: cn(
11
+ grow ? "flex-1" : axis === "vertical" ? "h-(--sable-spacer) shrink-0" : "w-(--sable-spacer) shrink-0",
12
+ className
13
+ ),
14
+ style
15
+ }
16
+ );
6
17
  }
7
18
  export {
8
19
  Spacer
@@ -39,7 +39,7 @@ function SplitPane({
39
39
  ref,
40
40
  "data-sable-split": true,
41
41
  "data-collapse": collapseBelow,
42
- className: cn(className),
42
+ className: cn("grid", className),
43
43
  style: mergedStyle,
44
44
  ...rest,
45
45
  children: [
@@ -2,17 +2,17 @@
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  var ALIGN = {
5
- start: "flex-start",
6
- center: "center",
7
- end: "flex-end",
8
- stretch: "stretch"
5
+ start: "items-start",
6
+ center: "items-center",
7
+ end: "items-end",
8
+ stretch: "items-stretch"
9
9
  };
10
10
  var JUSTIFY = {
11
- start: "flex-start",
12
- center: "center",
13
- end: "flex-end",
14
- between: "space-between",
15
- around: "space-around"
11
+ start: "justify-start",
12
+ center: "justify-center",
13
+ end: "justify-end",
14
+ between: "justify-between",
15
+ around: "justify-around"
16
16
  };
17
17
  function resolveGap(gap) {
18
18
  return typeof gap === "number" ? `calc(var(--rhythm-unit) * ${gap})` : gap;
@@ -27,15 +27,22 @@ function Stack({
27
27
  ref,
28
28
  ...rest
29
29
  }) {
30
- const mergedStyle = {
31
- display: "flex",
32
- flexDirection: "column",
33
- alignItems: ALIGN[align],
34
- justifyContent: JUSTIFY[justify],
35
- gap: resolveGap(gap),
36
- ...style
37
- };
38
- return /* @__PURE__ */ jsx("div", { ref, className: cn(className), style: mergedStyle, ...rest, children });
30
+ const mergedStyle = { "--sable-stack-gap": resolveGap(gap), ...style };
31
+ return /* @__PURE__ */ jsx(
32
+ "div",
33
+ {
34
+ ref,
35
+ className: cn(
36
+ "flex flex-col gap-(--sable-stack-gap)",
37
+ ALIGN[align],
38
+ JUSTIFY[justify],
39
+ className
40
+ ),
41
+ style: mergedStyle,
42
+ ...rest,
43
+ children
44
+ }
45
+ );
39
46
  }
40
47
  export {
41
48
  Stack
@@ -43,6 +43,11 @@ interface RegionProps extends ComponentPropsWithRef<"div"> {
43
43
  * The page region. Fluid-first: full width with a fluid page inset, NOT a
44
44
  * centered fixed-width container. Reach for `max` only when a specific region
45
45
  * (a reading column, a settings pane) genuinely needs a cap.
46
+ *
47
+ * The inset and the cap are classes (`px-(--page-inset)`, `mx-auto
48
+ * max-w-(--sable-region-max)`), never inline style, so a `className` utility
49
+ * overrides them at any breakpoint (`max-sm:px-0`, `hidden lg:block`). Only the
50
+ * `safeArea` padding stays inline: `env()` is already zero where it does not apply.
46
51
  */
47
52
  declare function Region({ as, inset, max, bleed, safeArea, className, style, children, ref, ...rest }: RegionProps): react.JSX.Element;
48
53
 
@@ -54,7 +59,13 @@ interface StackProps extends ComponentPropsWithRef<"div"> {
54
59
  align?: Align$1;
55
60
  justify?: Justify$1;
56
61
  }
57
- /** Vertical flow on the 8px rhythm. The default container for stacked content. */
62
+ /**
63
+ * Vertical flow on the 8px rhythm. The default container for stacked content.
64
+ *
65
+ * Layout is carried by classes (`flex flex-col items-* justify-*`) and the gap
66
+ * by a CSS variable, so any utility in `className` overrides a prop, at any
67
+ * breakpoint: `max-lg:hidden`, `md:flex-row`, `md:gap-8`.
68
+ */
58
69
  declare function Stack({ gap, align, justify, className, style, children, ref, ...rest }: StackProps): react.JSX.Element;
59
70
 
60
71
  interface PageProps extends RegionProps {
@@ -127,6 +138,10 @@ interface GridProps extends ComponentPropsWithRef<"div"> {
127
138
  * `cols` breakpoint map (fewer columns on small screens), or `minItemWidth`
128
139
  * cells that breathe with no breakpoints at all (`track` decides whether a
129
140
  * sparse row keeps its empty columns or stretches its items across them).
141
+ *
142
+ * `display` is the `grid` class and the gap a CSS variable, never inline
143
+ * style, so a `className` utility overrides either at any breakpoint
144
+ * (`hidden md:grid`, `md:gap-6`, `lg:grid-cols-3`).
130
145
  */
131
146
  declare function Grid({ cols, minItemWidth, track, gap, className, style, children, ref, ...rest }: GridProps): react.JSX.Element;
132
147
 
@@ -155,6 +170,11 @@ interface ColProps extends ComponentPropsWithRef<"div"> {
155
170
  * the track itself can thin out on small screens (e.g. 2 → 6 → 12). Pair with
156
171
  * `Col` for spans. The track template resolves through the `[data-sable-grid-cols]`
157
172
  * rules in `styles.css`, mobile-first.
173
+ *
174
+ * `display` is the `grid` class and the gaps are CSS variables, never inline
175
+ * style, so a `className` utility overrides them at any breakpoint
176
+ * (`hidden md:grid`, `md:gap-x-8`). A bare `gap-*` utility replaces both gaps
177
+ * and with them the column-gap cap, so prefer the `gap` prop unless you mean it.
158
178
  */
159
179
  declare function Columns({ count, gap, className, style, children, ref, ...rest }: ColumnsProps): react.JSX.Element;
160
180
  /**
@@ -178,7 +198,13 @@ interface RowProps extends ComponentPropsWithRef<"div"> {
178
198
  justify?: Justify;
179
199
  wrap?: boolean;
180
200
  }
181
- /** Horizontal flow. Inline rows of controls, toolbars, action clusters. */
201
+ /**
202
+ * Horizontal flow. Inline rows of controls, toolbars, action clusters.
203
+ *
204
+ * Layout is carried by classes (`flex flex-row items-* justify-*`) and the gap
205
+ * by a CSS variable, so any utility in `className` overrides a prop, at any
206
+ * breakpoint: `hidden md:flex`, `max-md:flex-col`, `md:gap-6`.
207
+ */
182
208
  declare function Row({ gap, align, justify, wrap, className, style, children, ref, ...rest }: RowProps): react.JSX.Element;
183
209
 
184
210
  interface SpacerProps {
@@ -190,21 +216,37 @@ interface SpacerProps {
190
216
  grow?: boolean;
191
217
  className?: string;
192
218
  }
193
- /** Explicit space on the rhythm, or a flexible `grow` pusher between siblings. */
219
+ /**
220
+ * Explicit space on the rhythm, or a flexible `grow` pusher between siblings.
221
+ *
222
+ * The size is a class reading a CSS variable, never inline style, so a
223
+ * `className` utility resizes or hides it at a breakpoint (`md:h-16`, `max-sm:hidden`).
224
+ */
194
225
  declare function Spacer({ size, axis, grow, className }: SpacerProps): react.JSX.Element;
195
226
 
196
227
  interface CenterProps extends ComponentPropsWithRef<"div"> {
197
228
  /** Cap width: a named region ("prose" | "feed" | "wide" | "ultrawide") or any raw CSS length. */
198
229
  max?: string;
199
230
  }
200
- /** Horizontally center a block, optionally capping its width (commonly "prose"). */
231
+ /**
232
+ * Horizontally center a block, optionally capping its width (commonly "prose").
233
+ *
234
+ * The centering and the cap are classes (`mx-auto`, `max-w-(--sable-center-max)`),
235
+ * never inline style, so a `className` utility overrides them at any breakpoint.
236
+ */
201
237
  declare function Center({ max, className, style, children, ref, ...rest }: CenterProps): react.JSX.Element;
202
238
 
203
239
  interface AspectBoxProps extends HTMLAttributes<HTMLDivElement> {
204
240
  /** Width / height ratio. Default 16/9. Reserves space so late media never shifts layout. */
205
241
  ratio?: number;
206
242
  }
207
- /** Reserves grid space via aspect-ratio — async/media content loads without layout shift. */
243
+ /**
244
+ * Reserves grid space via aspect-ratio — async/media content loads without layout shift.
245
+ *
246
+ * The ratio is a class reading a CSS variable (`aspect-(--sable-aspect)`), never
247
+ * inline style, so a `className` utility reshapes it at a breakpoint
248
+ * (`md:aspect-video`).
249
+ */
208
250
  declare function AspectBox({ ratio, className, style, children, ...rest }: AspectBoxProps): react.JSX.Element;
209
251
 
210
252
  interface BleedProps extends ComponentPropsWithRef<"div"> {
@@ -222,6 +264,10 @@ interface BleedProps extends ComponentPropsWithRef<"div"> {
222
264
  * full-bleed app, or `Bleed` used standalone) `cqw` resolves against the viewport,
223
265
  * so behavior is unchanged there. Nest a `Region` / `Center` inside to re-inset.
224
266
  *
267
+ * The width and margins are classes, never inline style, so a breakpoint can
268
+ * turn the bleed off: `<Bleed className="lg:mx-0 lg:w-auto">` runs edge to edge
269
+ * on a phone and sits inside the column on a desktop.
270
+ *
225
271
  * <Region max="feed">
226
272
  * <Bleed><ProfileCover /></Bleed>
227
273
  * …
@@ -258,7 +304,9 @@ interface SplitPaneProps extends Omit<ComponentPropsWithRef<"div">, "children">
258
304
  * hand-building a desktop two-column layout plus a separate mobile stack.
259
305
  *
260
306
  * The breakpoint switch + ratio resolve through the `[data-sable-split]` rules in
261
- * `styles.css` (inline CSS custom properties), so any `ratio` works.
307
+ * `styles.css` (inline CSS custom properties), so any `ratio` works. `display`
308
+ * is the `grid` class and those rules sit in `@layer components`, so a
309
+ * `className` utility (`hidden lg:grid`, `gap-8`) overrides them.
262
310
  *
263
311
  * `collapseBelow` is **viewport-relative**, not container-relative: it asks how
264
312
  * wide the window is, not how wide this SplitPane is. Dropped inside a narrow
@@ -296,6 +344,9 @@ interface GridOverlayProps {
296
344
  /**
297
345
  * Dev/docs structure visualizer — overlays the column grid so the layout's
298
346
  * skeleton is legible. Place inside a `position: relative` container.
347
+ *
348
+ * Positioning, `display` and the inset are classes, never inline style, so a
349
+ * `className` utility overrides them (`max-md:hidden` to draw it on desktop only).
299
350
  */
300
351
  declare function GridOverlay({ columns, gap, inset, show, className, }: GridOverlayProps): react.JSX.Element | null;
301
352
 
@@ -0,0 +1,32 @@
1
+ // src/lib/chart/activity.ts
2
+ import { dayNumber } from "./calendar.js";
3
+ function normalizeActivity(data) {
4
+ const days = /* @__PURE__ */ new Map();
5
+ const list = Array.isArray(data) ? data : Object.entries(data).map(([date, value]) => ({ date, value }));
6
+ for (const d of list) {
7
+ const n = dayNumber(d.date);
8
+ const prev = days.get(n);
9
+ const value = (prev?.value ?? 0) + (Number.isFinite(d.value) ? d.value : 0);
10
+ const level = d.level == null ? prev?.level : Math.max(d.level, prev?.level ?? 0);
11
+ days.set(n, { value, level, category: d.category ?? prev?.category });
12
+ }
13
+ return days;
14
+ }
15
+ function mergeActivity(sources, { both = "both" } = {}) {
16
+ const merged = /* @__PURE__ */ new Map();
17
+ for (const [key, data] of Object.entries(sources)) {
18
+ const list = Array.isArray(data) ? data : Object.entries(data).map(([date, value]) => ({ date, value }));
19
+ for (const d of list) {
20
+ if (!(d.value > 0)) continue;
21
+ const entry = merged.get(d.date) ?? { value: 0, keys: /* @__PURE__ */ new Set() };
22
+ entry.value += d.value;
23
+ entry.keys.add(key);
24
+ merged.set(d.date, entry);
25
+ }
26
+ }
27
+ return [...merged.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([date, { value, keys }]) => ({ date, value, category: keys.size > 1 ? both : [...keys][0] }));
28
+ }
29
+ export {
30
+ mergeActivity,
31
+ normalizeActivity
32
+ };
@@ -0,0 +1,67 @@
1
+ // src/lib/chart/calendar.ts
2
+ var DAY_MS = 864e5;
3
+ var ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
4
+ function dayNumber(iso) {
5
+ const m = ISO.exec(iso);
6
+ if (!m) throw new Error(`Expected a YYYY-MM-DD date, got "${iso}"`);
7
+ return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) / DAY_MS;
8
+ }
9
+ function isoDay(n) {
10
+ return new Date(n * DAY_MS).toISOString().slice(0, 10);
11
+ }
12
+ function weekday(n) {
13
+ return ((n + 4) % 7 + 7) % 7;
14
+ }
15
+ function dayOfMonth(n) {
16
+ return new Date(n * DAY_MS).getUTCDate();
17
+ }
18
+ function utcDate(n) {
19
+ return new Date(n * DAY_MS);
20
+ }
21
+ function today() {
22
+ const d = /* @__PURE__ */ new Date();
23
+ return Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / DAY_MS;
24
+ }
25
+ function localeWeekStart(locale) {
26
+ try {
27
+ const tag = new Intl.Locale(locale ?? new Intl.DateTimeFormat().resolvedOptions().locale);
28
+ const first = tag.getWeekInfo?.().firstDay ?? tag.weekInfo?.firstDay;
29
+ return first === 1 ? 1 : 0;
30
+ } catch {
31
+ return 0;
32
+ }
33
+ }
34
+ function activityGrid({ start, end, weekStart }) {
35
+ const origin = start - (weekday(start) - weekStart + 7) % 7;
36
+ const weeks = [];
37
+ for (let col = origin; col <= end; col += 7) {
38
+ const days = [];
39
+ for (let r = 0; r < 7; r++) {
40
+ const n = col + r;
41
+ days.push(n >= start && n <= end ? n : null);
42
+ }
43
+ weeks.push({ days });
44
+ }
45
+ const starts = [{ day: start, week: 0 }];
46
+ weeks.forEach((w, i) => {
47
+ const first = w.days.find((n) => n != null && dayOfMonth(n) === 1);
48
+ if (first != null && first !== start) starts.push({ day: first, week: i });
49
+ });
50
+ const months = starts.map((s, i) => ({ ...s, span: (starts[i + 1]?.week ?? weeks.length) - s.week }));
51
+ return { start, end, weekStart, origin, weeks, months: months.filter((m) => m.span > 0) };
52
+ }
53
+ function gridPosition(grid, n) {
54
+ const offset = n - grid.origin;
55
+ return { week: Math.floor(offset / 7), row: offset % 7 };
56
+ }
57
+ export {
58
+ activityGrid,
59
+ dayNumber,
60
+ dayOfMonth,
61
+ gridPosition,
62
+ isoDay,
63
+ localeWeekStart,
64
+ today,
65
+ utcDate,
66
+ weekday
67
+ };
@@ -18,7 +18,7 @@ function ChartTooltipContent({
18
18
  ),
19
19
  children: [
20
20
  label != null ? /* @__PURE__ */ jsx("div", { className: "text-label-sm", children: label }) : null,
21
- /* @__PURE__ */ jsxs("div", { className: "grid gap-1", children: [
21
+ rows.length || total != null ? /* @__PURE__ */ jsxs("div", { className: "grid gap-1", children: [
22
22
  rows.map((row) => /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2", indicator === "line" || indicator === "dashed" ? "items-stretch" : ""), children: [
23
23
  row.icon ? /* @__PURE__ */ jsx(Icon, { name: row.icon, className: "shrink-0 text-muted-foreground" }) : indicator === "dot" ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "size-2.5 shrink-0 rounded-[2px]", style: { background: row.color } }) : indicator === "line" ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "w-1 shrink-0 rounded-full", style: { background: row.color } }) : indicator === "dashed" ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "w-0 shrink-0 border-[1.5px] border-dashed", style: { borderColor: row.color } }) : null,
24
24
  /* @__PURE__ */ jsx("span", { className: "flex-1 text-muted-foreground", children: row.label }),
@@ -28,7 +28,7 @@ function ChartTooltipContent({
28
28
  /* @__PURE__ */ jsx("span", { className: "flex-1 text-label-sm", children: "Total" }),
29
29
  /* @__PURE__ */ jsx("span", { className: "ms-3 font-mono tabular-nums text-foreground", children: total })
30
30
  ] }) : null
31
- ] })
31
+ ] }) : null
32
32
  ]
33
33
  }
34
34
  );
@@ -0,0 +1,55 @@
1
+ // src/lib/chart/heat.ts
2
+ function quantile(sorted, p) {
3
+ if (!sorted.length) return 0;
4
+ const i = (sorted.length - 1) * p;
5
+ const lo = Math.floor(i);
6
+ const hi = Math.ceil(i);
7
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * (i - lo);
8
+ }
9
+ function activityLevels(values, { levels = 5, scale = "quantile" } = {}) {
10
+ const steps = Math.max(2, Math.round(levels));
11
+ const top = steps - 1;
12
+ const positive = values.filter((v) => v > 0).sort((a, b) => a - b);
13
+ const stats = { min: positive[0] ?? 0, max: positive.at(-1) ?? 0, levels: steps };
14
+ const clamp = (l) => Math.min(top, Math.max(0, Math.round(l)));
15
+ if (typeof scale === "function") {
16
+ return { thresholds: [], level: (v) => v > 0 ? Math.max(1, clamp(scale(v, stats))) : 0 };
17
+ }
18
+ let thresholds;
19
+ if (Array.isArray(scale)) {
20
+ thresholds = scale.slice(0, top);
21
+ } else if (scale === "linear") {
22
+ thresholds = Array.from({ length: top }, (_, k) => k === 0 ? Number.MIN_VALUE : stats.max * k / top);
23
+ } else {
24
+ thresholds = Array.from({ length: top }, (_, k) => quantile(positive, k / top));
25
+ }
26
+ return {
27
+ thresholds,
28
+ level: (v) => {
29
+ if (!(v > 0)) return 0;
30
+ let l = 1;
31
+ for (let k = 1; k < thresholds.length; k++) if (v >= thresholds[k]) l = k + 1;
32
+ return l;
33
+ }
34
+ };
35
+ }
36
+ var HEAT_EMPTY = "color-mix(in oklch, var(--foreground) 8%, transparent)";
37
+ function heatColor(base, level, levels) {
38
+ if (level <= 0) return HEAT_EMPTY;
39
+ const top = Math.max(1, levels - 1);
40
+ if (level >= top) return base;
41
+ const share = Math.round(35 + 65 * (level - 1) / (top - 1));
42
+ return `color-mix(in oklch, ${base} ${share}%, ${HEAT_EMPTY})`;
43
+ }
44
+ function levelNames(levels) {
45
+ if (levels === 5) return ["None", "Low", "Medium-low", "Medium-high", "High"];
46
+ if (levels === 2) return ["None", "Some"];
47
+ if (levels === 3) return ["None", "Low", "High"];
48
+ return Array.from({ length: levels }, (_, k) => k === 0 ? "None" : `Level ${k} of ${levels - 1}`);
49
+ }
50
+ export {
51
+ HEAT_EMPTY,
52
+ activityLevels,
53
+ heatColor,
54
+ levelNames
55
+ };
@@ -0,0 +1,53 @@
1
+ // src/lib/focus-return.tsx
2
+ import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
3
+ var FocusReturnContext = createContext(null);
4
+ function focusedElement() {
5
+ if (typeof document === "undefined") return null;
6
+ let element = document.activeElement;
7
+ while (element?.shadowRoot?.activeElement) element = element.shadowRoot.activeElement;
8
+ return element instanceof HTMLElement && element !== document.body ? element : null;
9
+ }
10
+ function useFocusReturn(open, onOpenChange) {
11
+ const [opener, setOpener] = useState(null);
12
+ const popupRef = useRef(null);
13
+ const [wasOpen, setWasOpen] = useState(false);
14
+ if (open !== void 0 && open !== wasOpen) {
15
+ setWasOpen(open);
16
+ if (open) setOpener(focusedElement());
17
+ }
18
+ const handleOpenChange = useCallback(
19
+ (next, ...rest) => {
20
+ if (next && open === void 0) setOpener(focusedElement());
21
+ onOpenChange?.(next, ...rest);
22
+ },
23
+ [open, onOpenChange]
24
+ );
25
+ const value = useMemo(
26
+ () => ({
27
+ popupRef(node) {
28
+ popupRef.current = node;
29
+ },
30
+ // Base UI reads finalFocus when the popup closes, from the latest props,
31
+ // so a new function per opener is picked up.
32
+ finalFocus() {
33
+ const target = opener;
34
+ if (!target?.isConnected) return true;
35
+ const active = focusedElement();
36
+ const popup = popupRef.current;
37
+ if (active && active !== target && popup && !popup.contains(active)) return false;
38
+ return target;
39
+ }
40
+ }),
41
+ [opener]
42
+ );
43
+ return { onOpenChange: handleOpenChange, focusReturn: value };
44
+ }
45
+ var FocusReturnProvider = FocusReturnContext.Provider;
46
+ function useFocusReturnDefault() {
47
+ return useContext(FocusReturnContext);
48
+ }
49
+ export {
50
+ FocusReturnProvider,
51
+ useFocusReturn,
52
+ useFocusReturnDefault
53
+ };
@@ -131,6 +131,7 @@ var COMPONENT_TAXONOMY = {
131
131
  RadarChart: ["Charts", "page"],
132
132
  RadialChart: ["Charts", "page"],
133
133
  Sparkline: ["Charts", "either"],
134
+ ActivityHeatmap: ["Charts", "either"],
134
135
  /* ── Data display ─────────────────────────────────────────────────────────
135
136
  Showing records and readings. `Meter` (bar or ring) is here: it shows a
136
137
  value within a known range (a reading), which is not loading or progress. */