@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
package/AGENTS.md CHANGED
@@ -153,6 +153,11 @@ In a monorepo where Sable belongs to one workspace package, run setup in that
153
153
  package with `--root` naming the repository root (for example
154
154
  `pnpm --filter web exec sable setup --agent --root ../..`); the entries go
155
155
  there and resolve Sable through the package.
156
+ Setup also adds a short managed block to the project's `AGENTS.md` and
157
+ `CLAUDE.md`, the instructions an agent reads on every task, so a UI request
158
+ goes through the skill even when it doesn't mention Sable. Only the text between
159
+ its `<!-- sable:begin -->` and `<!-- sable:end -->` markers is Sable's;
160
+ `--no-instructions` skips it.
156
161
  Each entry resolves `skills/sable/SKILL.md` from the installed package, so package
157
162
  upgrades also update the workflow. Other agents can read that file directly.
158
163
  Use `$sable build <idea>`, `inspire <references>`, `audit <target>`,
@@ -160,9 +165,11 @@ Use `$sable build <idea>`, `inspire <references>`, `audit <target>`,
160
165
  Claude Code or Cursor. `create` is a build alias. `fix` takes the short path for
161
166
  one scoped defect and escalates to `polish` when the change grows. These are agent modes, not shell commands.
162
167
 
163
- Upgrade with `pnpm update @sableui/react --latest`, then
164
- `pnpm exec sable setup --agent --update`. Setup only replaces managed files
165
- that have not been locally edited; `--dry-run` checks without writing. Keep
168
+ Upgrade with `pnpm exec sable upgrade`: it updates the package, then runs the
169
+ new version's `init`, which refreshes the skill entries and the instructions
170
+ block and checks the wiring. From 0.17 or earlier, which has no `upgrade`, run
171
+ `pnpm update @sableui/react --latest` and then `pnpm exec sable init` once.
172
+ Setup only replaces managed files that have not been locally edited; `--dry-run` checks without writing. Keep
166
173
  project preferences in project instructions. Package installation alone does not
167
174
  change agent settings. See [building-with-sable.md](./llms/tasks/building-with-sable.md),
168
175
  [creating-a-preset.md](./llms/tasks/creating-a-preset.md), and
package/CHANGELOG.md CHANGED
@@ -1,5 +1,137 @@
1
1
  # What's new
2
2
 
3
+ ## 0.18.0
4
+
5
+ A year at a glance: the contribution graph, built so a phone, a keyboard and a
6
+ screen reader can all read it.
7
+
8
+ - **New: `ActivityHeatmap`.** A calendar heatmap: one value per day as a grid
9
+ of weeks by weekdays — the GitHub contribution graph, for any daily measure.
10
+ Pass days as `"YYYY-MM-DD"` with a value (or a date-to-value map); levels
11
+ come by quartile, as GitHub's do, unless a day brings its own. `categories`
12
+ colour days by source instead of intensity, and `mergeActivity` tags each day
13
+ with its source or `both`. The grid is a real `role="grid"` table with one
14
+ tab stop: arrows step a day or a week, each cell is named with its date and
15
+ value, and the tooltip shows on hover, focus and tap. It fills the width down
16
+ to `size`; narrower, the weeks scroll sideways and open on the latest, while
17
+ the weekday labels stay outside the scroller. Days are read as calendar days,
18
+ never instants, so no time zone shifts one. Prompted by a consumer site that
19
+ hand-built one from 371 buttons and broke on phones.
20
+ - **New: `@sableui/react/adapters/github`.** `fromGitHubGraphQL` and
21
+ `fromGitHubContributionsApi` turn GitHub's contribution data into heatmap
22
+ days. Pure transforms: fetch on your server and pass the result in.
23
+ - **New: `activityLevels` and `activityGrid`.** The level scale and the week
24
+ grid behind the heatmap, exported for a custom legend, a summary stat or
25
+ another layout.
26
+ - **`ChartTooltipContent` with no rows** draws just its label, so a caller's
27
+ own content sits on the standard tooltip card.
28
+ - **Layout primitives take `className` over their own layout.** Row, Stack,
29
+ Grid, Columns, Col, Center, Region, Page, Bleed, AspectBox, Spacer, SplitPane
30
+ and GridOverlay set `display`, direction, alignment, gap, inset, cap, ratio and
31
+ size inline, and an inline declaration beats every utility, so
32
+ `<Row className="max-lg:hidden">` or `hidden md:flex` silently did nothing.
33
+ The grid and split rules in `styles.css` were unlayered too, so `hidden` lost
34
+ to them whatever its specificity. Layout now lives in classes merged through
35
+ `cn()`, with prop values passed as CSS variables those classes read, and the
36
+ layout rules sit in `@layer components`. A utility in `className` wins at any
37
+ breakpoint (`max-md:flex-col`, `md:gap-6`, `lg:grid-cols-3`); the props work
38
+ as before. Migration: a `className` that used to be ignored on a primitive now
39
+ applies, and a test reading `style.gap` or `style.maxWidth` reads
40
+ `--sable-row-gap`, `--sable-region-max` and the like instead.
41
+ - **`PageHeader size="lg"`.** A display-size page title for an editorial page
42
+ (a personal site, a marketing page, an article index): `text-display-lg` over
43
+ a `text-body-lg` lede capped at the prose measure. `md`, the default, is
44
+ unchanged. Keep `lg` off app routes: display type stays out of product chrome.
45
+ - **`SectionHeader size="lg"`.** The major-section tier, `text-heading-2` over a
46
+ `text-body-md` description, for an editorial page's sections under a
47
+ `PageHeader size="lg"`. It stops at heading-2 because heading-1 is the page
48
+ title's, one per route. Actions stay centred on the title's first line at
49
+ every size.
50
+ - **`SearchInput` takes `size`** (`md` | `sm` | `xs`), on the same ladder as
51
+ Input, Select and Combobox. `sm` is the toolbar field on `--control-sm`, `xs`
52
+ the inset tool-panel field; the magnifier, padding and clear button scale with
53
+ it and stay centred. Before, it was fixed at 40px and matched nothing in a
54
+ toolbar.
55
+ - **`Pill size="lg"`.** A 32px chip on `--control-sm`, the toolbar height Button
56
+ `sm` and SearchInput `sm` share, so a filter row beside a search field is one
57
+ height in either density.
58
+ - **`MediaImage` and `MediaTile` take a crop focus.** `position` sets where the
59
+ image sits in its frame and takes any CSS `object-position` value
60
+ (`"35% 50%"`, `"top"`), beside `fit`. MediaTile forwards it to its cover.
61
+ - **Dialogs return focus to whatever opened them.** `Dialog`, `AlertDialog` and
62
+ `Sheet` record the element focused when `open` turns true and send focus back
63
+ to it on close, with or without a trigger. Before, a dialog opened from your
64
+ own button lost focus to the page whenever a field inside autofocused,
65
+ CommandMenu's included. An explicit `finalFocus` still wins.
66
+ - **`DialogContent keepMounted`.** Keeps a closed dialog's content mounted and
67
+ hidden, so a half-written draft survives Esc or a stray click and is still
68
+ there on reopen. Opt in for drafts the user may come back to.
69
+ - **Eight more icon names.** `briefcase`, `layers`, `microphone`, `video`,
70
+ `broadcast`, `newspaper`, `palette` and `gamepad` join the contract, mapped
71
+ in the Reicon set, for a portfolio that imported reicon-react directly for
72
+ its work, audio, video, live, press, design and games sections.
73
+ - **Setup writes the skill into the instructions every task reads.** A skill
74
+ loads only when a request names it, so "add a pricing page" in a wired app
75
+ could skip it: the agent saw Tailwind and a stylesheet, wrote its own Button
76
+ and Modal, and put Sable's tokens on them. `sable setup --agent` (and `sable
77
+ init`) now also writes a short managed block into `AGENTS.md` and
78
+ `CLAUDE.md` that sends every UI request through the skill. Only the text
79
+ between its `<!-- sable:begin -->` and `<!-- sable:end -->` markers is
80
+ Sable's; `--no-instructions` skips it. **Migration:** from 0.17, run
81
+ `pnpm update @sableui/react --latest` then `pnpm exec sable init` once; after
82
+ that, `sable upgrade` does both.
83
+ - **New: `sable upgrade`.** One command to update: installs the latest
84
+ `@sableui/react` with the project's package manager, then runs the new
85
+ version's `init`, so the skill entries, the instructions block and the
86
+ checks are the new release's. Safe on an app that already works.
87
+ - **"Move this app onto Sable" is a request the skill understands.** It checks
88
+ the app, shows the swaps once, asks, and moves them a screen at a time; it
89
+ never starts a new build. `init` says so when an app's UI barely uses Sable,
90
+ and its next step for a ready app is now a plain request, not mode syntax.
91
+ - **Adoption, in `sable check`, `sable grade` and `sable doctor`.** How much of
92
+ the UI composes Sable, which app-built components do by hand what a Sable
93
+ component already does (a `Modal` → Dialog, a `SearchBar` → SearchInput,
94
+ behaviour first), and what the app already stands on: nothing, its own
95
+ primitives, or another library. Never a failure. `check` lists the swaps and,
96
+ when adoption is low, makes "ask the user about moving onto Sable" the first
97
+ review step; `doctor` warns. The skill and `sable docs auditing-an-app` say
98
+ how to put the choice to the user, more firmly when the app has no
99
+ foundation of its own.
100
+ - **The skill triggers on any UI work in a Sable app.** Its description now
101
+ says so, even when the request doesn't mention Sable.
102
+ - **`sable init --hook`.** Adds the Claude Code Stop hook that runs `sable
103
+ check` before a task can end. Run in a terminal, init asks; `--no-hook`
104
+ doesn't.
105
+ - **`CodeBlock wrap`.** Wraps long lines instead of scrolling them, for prose
106
+ in a code face: a prompt to paste into an agent, a log line. Code keeps
107
+ scrolling.
108
+
109
+ ## 0.17.0
110
+
111
+ Knowing where you are: a rail that follows a long read, and an approval
112
+ that shows an agent's call the way agent products do.
113
+
114
+ - **New: `SectionRail`.** A slim vertical rail of dots beside a long read —
115
+ a case study, an essay, a long landing page — one per section. A bar marks
116
+ the section being read and glides to the next as the page scrolls; hovering
117
+ or focusing a dot names it in one tooltip that travels between dots
118
+ (`TooltipGroup`). Picking a dot jumps there with the page's own
119
+ `scroll-behavior` (instant under reduced motion), and the bar goes straight
120
+ to the pick rather than stepping through every section in between. Pass
121
+ `root` when the content scrolls inside an element: only that element moves,
122
+ never the page around it. Links are real `#id` anchors, so it works without
123
+ script. Promoted from carldecaire.com's case-study rail.
124
+ - **New: `useActiveSection(ids, { offset, root })`.** The scroll spy behind it,
125
+ exported for a contents list you draw yourself. The current section is the
126
+ last whose top has crossed the reading line; at the end of the scroll the
127
+ last one in view wins, so a short closing section is still reachable.
128
+ - **`ApprovalBlock` asks the way agent products do.** It wraps the call's own
129
+ `ToolCall` — tool name and exact input, open on arrival — that turns to
130
+ Allowed or Denied once answered, and offers Deny / Always allow / Allow once.
131
+ The details table and the "for the rest of this chat" checkbox are gone;
132
+ `onApprove({ remember })` is unchanged, with `remember` now true for Always
133
+ allow. The example is an agent asking to run a command.
134
+
3
135
  ## 0.16.0
4
136
 
5
137
  Five pieces of the calm, product-chrome look that AI and B2B tools share:
package/README.md CHANGED
@@ -69,7 +69,7 @@ pnpm add @sableui/react
69
69
  pnpm exec sable init # or: npx sable init — once the package is installed
70
70
  ```
71
71
 
72
- `init` reads your package manager from the lockfile and your framework from the project. It installs what is missing (React, Tailwind and its build plugin), puts the CSS imports below in order, adds React dedupe and the Tailwind plugin to a Vite config, loads the fonts, mounts `IconProvider` in a Vite starter's entry, installs the agent skill (`sable setup --agent`), then runs `sable doctor`. Anything it cannot change safely — an unfamiliar config, a Next.js layout — it prints as the exact snippet and the file it belongs in. Vite and Next.js (App Router) get every step; other setups get the packages and the stylesheet. `--dry-run` shows every change first; `--no-agent` skips the skill. Already set up? `sable doctor --fix` applies the same fixes to whatever doctor flags.
72
+ `init` reads your package manager from the lockfile and your framework from the project. It installs what is missing (React, Tailwind and its build plugin), puts the CSS imports below in order, adds React dedupe and the Tailwind plugin to a Vite config, loads the fonts, mounts `IconProvider` in a Vite starter's entry, installs the agent skill and its block in `AGENTS.md` / `CLAUDE.md` (`sable setup --agent`), offers the Claude Code Stop hook when you run it in a terminal, then runs `sable doctor`. Anything it cannot change safely — an unfamiliar config, a Next.js layout — it prints as the exact snippet and the file it belongs in. Vite and Next.js (App Router) get every step; other setups get the packages and the stylesheet. `--dry-run` shows every change first; `--no-agent` skips the skill. Already set up? `sable doctor --fix` applies the same fixes to whatever doctor flags.
73
73
 
74
74
  The steps below are what `init` does, for doing it by hand or checking its work.
75
75
 
@@ -213,6 +213,11 @@ In a monorepo where Sable belongs to one workspace package, run setup in that
213
213
  package with `--root` naming the repository root (for example
214
214
  `pnpm --filter web exec sable setup --agent --root ../..`); the entries go
215
215
  there and resolve Sable through the package.
216
+ Setup also adds a short managed block to the project's `AGENTS.md` and
217
+ `CLAUDE.md`, the instructions an agent reads on every task, so a UI request
218
+ goes through the skill even when it doesn't mention Sable. Only the text between
219
+ its `<!-- sable:begin -->` and `<!-- sable:end -->` markers is Sable's;
220
+ `--no-instructions` skips it.
216
221
  Each entry resolves `skills/sable/SKILL.md` from the installed package, so package
217
222
  upgrades also update the workflow. Other agents can read that file directly.
218
223
  Use `$sable build <idea>`, `inspire <references>`, `audit <target>`, or
@@ -225,9 +230,11 @@ server: `claude mcp add --transport http sable https://www.sableui.com/api/mcp`
225
230
  and `docs`, give the CLI's answers from the published package, at the version
226
231
  the agent passes or the latest. Once Sable is installed, the CLI is the default.
227
232
 
228
- Upgrade with `pnpm update @sableui/react --latest`, then
229
- `pnpm exec sable setup --agent --update`. Setup only replaces managed files
230
- that have not been locally edited; `--dry-run` checks without writing. Keep
233
+ Upgrade with `pnpm exec sable upgrade`: it updates the package, then runs the
234
+ new version's `init`, which refreshes the skill entries and the instructions
235
+ block and checks the wiring. From 0.17 or earlier, which has no `upgrade`, run
236
+ `pnpm update @sableui/react --latest` and then `pnpm exec sable init` once.
237
+ Setup only replaces managed files that have not been locally edited; `--dry-run` checks without writing. Keep
231
238
  project preferences in project instructions. Package installation alone does not
232
239
  change agent settings. See [building-with-sable.md](./llms/tasks/building-with-sable.md),
233
240
  [creating-a-preset.md](./llms/tasks/creating-a-preset.md), and
package/bin/sable.mjs CHANGED
@@ -36,13 +36,15 @@
36
36
 
37
37
  import { spawnSync } from "node:child_process"
38
38
  import { appendFileSync, existsSync, readdirSync, readFileSync, statSync } from "node:fs"
39
+ import { createRequire } from "node:module"
39
40
  import { dirname, join, relative, resolve } from "node:path"
40
41
  import { fileURLToPath } from "node:url"
41
42
  import { skeletonize } from "../scripts/skeleton.mjs"
42
43
  import { claudeStopHook, installAgentSkill, installClaudeStopHook } from "../scripts/agent-setup.mjs"
44
+ import { adoptionReport, swapLines } from "../scripts/adoption.mjs"
43
45
  import { patchChecks } from "../scripts/patches.mjs"
44
46
  import { sourcesCheck } from "../scripts/sources.mjs"
45
- import { detectFramework, detectPackageManager, fixWiring, fontsCheck, providerSnippet, tailwindBuildCheck } from "../scripts/wiring.mjs"
47
+ import { detectFramework, detectPackageManager, fixWiring, fontsCheck, installCommand, providerSnippet, tailwindBuildCheck } from "../scripts/wiring.mjs"
46
48
 
47
49
  const HERE = dirname(fileURLToPath(import.meta.url))
48
50
  const PKG_ROOT = join(HERE, "..")
@@ -119,7 +121,7 @@ const COMMANDS = [
119
121
  },
120
122
  {
121
123
  name: "setup",
122
- summary: "Install package-backed skill entry files for coding agents, preserving local edits.",
124
+ summary: "Install package-backed skill entry files for coding agents, plus a short managed block in AGENTS.md / CLAUDE.md that sends every UI request through the skill, preserving local edits.",
123
125
  arg: null,
124
126
  flags: [
125
127
  { name: "--agent", value: null, describe: "Install skill entry files. Required." },
@@ -128,6 +130,7 @@ const COMMANDS = [
128
130
  { name: "--update", value: null, describe: "Refresh unchanged managed entries after a package upgrade." },
129
131
  { name: "--dry-run", value: null, describe: "Check and report the destination without writing files." },
130
132
  { name: "--hook", value: null, describe: "Also add a Claude Code Stop hook that runs `sable check` to .claude/settings.json (Claude targets only). Without it, setup prints the hook and writes nothing there." },
133
+ { name: "--no-instructions", value: null, describe: "Skip the managed Sable block in AGENTS.md / CLAUDE.md. Without that block an agent uses the skill only when a request names it." },
131
134
  ],
132
135
  returns: ["setup.agent"],
133
136
  },
@@ -249,7 +252,7 @@ const COMMANDS = [
249
252
  },
250
253
  {
251
254
  name: "doctor",
252
- summary: "Check this project's Sable wiring. Exits 1 on any failure.",
255
+ summary: "Check this project's Sable wiring, and warn when its UI barely uses Sable (adoption). Exits 1 on any wiring failure.",
253
256
  arg: null,
254
257
  flags: [
255
258
  { name: "--fix", value: null, describe: "Apply the fixes that need no judgment (imports in order, @source, missing packages, Vite dedupe and Tailwind plugin, fonts, IconProvider in a starter entry), print the rest, then check again." },
@@ -265,10 +268,26 @@ const COMMANDS = [
265
268
  flags: [
266
269
  { name: "--dry-run", value: null, describe: "Report every change without writing or installing anything." },
267
270
  { name: "--no-agent", value: null, describe: "Skip the agent skill (sable setup --agent)." },
271
+ { name: "--no-instructions", value: null, describe: "Install the skill but skip its managed block in AGENTS.md / CLAUDE.md." },
272
+ { name: "--hook", value: null, describe: "Also add the Claude Code Stop hook that runs `sable check` before a task can end. In a terminal, init asks; elsewhere it only prints the hook." },
273
+ { name: "--no-hook", value: null, describe: "Don't ask about the Stop hook." },
268
274
  { name: "--no-install", value: null, describe: "Print install commands instead of running them." },
269
275
  ],
270
276
  returns: ["init"],
271
277
  },
278
+ {
279
+ name: "upgrade",
280
+ summary: "Update @sableui/react to the latest release, then run the new version's init: skill entries and the AGENTS.md / CLAUDE.md block refreshed, wiring checked, adoption reported. Safe on an app that is already set up.",
281
+ arg: null,
282
+ flags: [
283
+ { name: "--dry-run", value: null, describe: "Print the update command and preview init's changes without installing or writing anything." },
284
+ { name: "--no-install", value: null, describe: "Skip the package update (you updated it yourself); just run the installed version's init." },
285
+ { name: "--hook", value: null, describe: "Passed to init: add the Claude Code Stop hook." },
286
+ { name: "--no-hook", value: null, describe: "Passed to init: don't ask about the Stop hook." },
287
+ { name: "--no-instructions", value: null, describe: "Passed to init: skip the AGENTS.md / CLAUDE.md block." },
288
+ ],
289
+ returns: ["init"],
290
+ },
272
291
  {
273
292
  name: "grade",
274
293
  summary: "Static contract checks and evidence review. Original compositions carry no penalty; no automatic taste score.",
@@ -358,6 +377,7 @@ const ERROR_CODES = [
358
377
  { code: "ERR_PRESET_CONTRAST", when: "A label on a brand or status fill (primary, sidebar-primary, highlight, destructive, success, tone-warning, tone-info) misses 4.5:1 or APCA Lc 60. Apply the suggested pair, or record an `exceptions` entry with a reason." },
359
378
  { code: "ERR_AGENT_SETUP", when: "The packaged skill is missing, the destination is linked, or existing local edits would be overwritten." },
360
379
  { code: "ERR_INIT", when: "sable init ran outside an app root (no package.json)." },
380
+ { code: "ERR_UPGRADE", when: "sable upgrade ran where @sableui/react is not a dependency, or the package manager's update failed." },
361
381
  { code: "ERR_UNKNOWN_COMMAND", when: "The first argument is not a command." },
362
382
  { code: "ERR_MISSING_ARGUMENT", when: "A command that requires an argument got none." },
363
383
  { code: "ERR_UNKNOWN_COMPONENT", when: "No component by that name. `suggestions` carries near matches." },
@@ -1471,6 +1491,21 @@ function doctorReport() {
1471
1491
  )
1472
1492
  }
1473
1493
 
1494
+ /* Adoption: wiring that holds says nothing about whether the app composes
1495
+ * Sable. An agent that never loaded the skill wires the stylesheet, uses the
1496
+ * tokens, and writes its own Button and Modal. A warning, never a failure:
1497
+ * check lists the swaps, and moving working screens is the user's call. */
1498
+ if (pkg && { ...pkg.dependencies, ...pkg.devDependencies }["@sableui/react"]) {
1499
+ const files = src
1500
+ .filter((f) => /\.(tsx|jsx)$/.test(f.path) && !/\.(test|spec|stories)\./.test(f.path))
1501
+ .map((f) => ({ rel: relative(CWD, f.path).replaceAll("\\", "/"), text: f.text }))
1502
+ const adoption = adoptionReport({ files, components: COMPONENTS, deps: { ...pkg.dependencies, ...pkg.devDependencies } })
1503
+ if (adoption.level === "none" || adoption.level === "low")
1504
+ add("warn", "adoption", `${adoption.summary} ${adoption.recommendation}`, "sable check src lists each swap; then ask your agent to /sable audit (or $sable audit) the app's use of Sable.")
1505
+ else if (adoption.level === "sable") add("pass", "adoption", adoption.summary)
1506
+ else if (adoption.level === "fresh") add("info", "adoption", adoption.summary)
1507
+ }
1508
+
1474
1509
  /* Tailwind in the build, and the theme's fonts: both fail as a page that
1475
1510
  * renders, just not as Sable — unstyled, or in system fonts. */
1476
1511
  if (pkg) {
@@ -1567,7 +1602,7 @@ function cmdDoctorFix() {
1567
1602
  * app still has to mount. Each step is the code its own command runs, so init
1568
1603
  * can never wire an app differently from what doctor checks.
1569
1604
  */
1570
- function cmdInit() {
1605
+ async function cmdInit() {
1571
1606
  if (!existsSync(join(CWD, "package.json")))
1572
1607
  fail("ERR_INIT", `No package.json in ${CWD}.`, ["Run sable init from your app's root.", "Starting fresh: pnpm create vite@latest my-app --template react-ts, then pnpm add @sableui/react && pnpm exec sable init"])
1573
1608
  const dryRun = flags.has("dry-run")
@@ -1582,26 +1617,57 @@ function cmdInit() {
1582
1617
  try {
1583
1618
  // update: an unchanged managed entry from an older Sable is refreshed;
1584
1619
  // one with local edits still refuses, and is reported for the user.
1585
- const setup = installAgentSkill({ root: CWD, project: CWD, packageRoot: PKG_ROOT, target: "all", update: true, dryRun })
1586
- agent = { status: dryRun ? "would-apply" : "applied", destinations: setup.destinations, next: setup.next }
1620
+ const setup = installAgentSkill({ root: CWD, project: CWD, packageRoot: PKG_ROOT, target: "all", update: true, dryRun, instructions: !flags.has("no-instructions") })
1621
+ agent = { status: dryRun ? "would-apply" : "applied", destinations: setup.destinations, instructions: setup.instructions, instructionsChanged: setup.instructionsChanged, next: setup.next }
1587
1622
  } catch (error) {
1588
1623
  agent = { status: "manual", detail: error.message, fix: "sable setup --agent --dry-run" }
1589
1624
  }
1590
1625
 
1626
+ /* The Stop hook changes how Claude Code behaves, so init never adds it on its
1627
+ * own. A person at a terminal is asked; an agent running init (no TTY) gets
1628
+ * the one-line offer in the output, and --hook adds it either way. */
1629
+ let hook = null
1630
+ if (agent.status === "applied" || agent.status === "would-apply") {
1631
+ let want = flags.has("hook")
1632
+ const interactive = process.stdin.isTTY && process.stdout.isTTY && !JSON_OUT && !dryRun
1633
+ if (!want && !flags.has("no-hook") && interactive) {
1634
+ const { createInterface } = await import("node:readline/promises")
1635
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
1636
+ const answer = await rl.question("Add a Claude Code Stop hook, so Claude can't end a task while `sable check` fails? [Y/n] ")
1637
+ rl.close()
1638
+ want = !/^\s*n/i.test(answer)
1639
+ }
1640
+ try {
1641
+ hook = want ? installClaudeStopHook({ root: CWD, project: CWD, dryRun }) : claudeStopHook({ root: CWD, project: CWD })
1642
+ } catch (error) {
1643
+ hook = { installed: false, error: error.message }
1644
+ }
1645
+ }
1646
+
1591
1647
  const after = dryRun ? before : doctorReport()
1592
1648
  verifyFixes(fixes, after)
1593
1649
  const mounted = after.checks.find((c) => c.name === "icons")?.status === "pass"
1594
1650
  const providers = { ...providerSnippet(framework), mounted }
1595
- const data = { framework: { id: framework.id, label: framework.label }, packageManager, dryRun, fixes, agent, providers, doctor: after }
1651
+ // An app that is wired but built mostly without Sable needs the audit path
1652
+ // next, not a new build: `build` would start the product over.
1653
+ const lowAdoption = after.checks.some((c) => c.name === "adoption" && c.status === "warn")
1654
+ const data = { framework: { id: framework.id, label: framework.label }, packageManager, dryRun, fixes, agent, stopHook: hook, providers, doctor: after, next: lowAdoption ? EXISTING_APP_NEXT : (agent.next ?? null) }
1596
1655
 
1597
1656
  if (JSON_OUT) process.stdout.write(`${JSON.stringify({ type: "init", data }, null, DENSE ? 0 : 1)}\n`)
1598
1657
  else {
1599
1658
  const agentText = {
1600
- applied: () => `Agent skill: installed at ${agent.destinations.join(", ")}.`,
1601
- "would-apply": () => `Agent skill: would install at ${agent.destinations.join(", ")}.`,
1659
+ applied: () => `Agent skill: installed at ${agent.destinations.join(", ")}.\n${instructionsText({ ...agent, dryRun })}`,
1660
+ "would-apply": () => `Agent skill: would install at ${agent.destinations.join(", ")}.\n${instructionsText({ ...agent, dryRun })}`,
1602
1661
  skipped: () => "Agent skill: skipped (--no-agent).",
1603
1662
  manual: () => `Agent skill: not installed — ${agent.detail}\n → ${agent.fix}`,
1604
1663
  }[agent.status]()
1664
+ const hookText = !hook
1665
+ ? ""
1666
+ : hook.error
1667
+ ? `Stop hook: not added — ${hook.error}`
1668
+ : hook.installed
1669
+ ? `Stop hook: ${dryRun ? "would add to" : hook.alreadyPresent ? "already in" : "added to"} ${hook.settingsFile}. Claude can't end a task while sable check fails.`
1670
+ : `Stop hook (optional): sable init --hook, or sable setup --agent --hook, makes Claude Code run sable check before it can end a task.`
1605
1671
  const iconsFix = fixes.find((f) => f.check === "icons")
1606
1672
  const providerText = mounted
1607
1673
  ? "Providers: <IconProvider> is mounted. MotionProvider and SableThemeProvider are optional: sable provider --list."
@@ -1617,13 +1683,14 @@ function cmdInit() {
1617
1683
  "",
1618
1684
  fixes.length ? `${dryRun ? "Would fix:" : "Fixes:"}\n${fixLines(fixes)}` : "Nothing to fix.\n",
1619
1685
  agentText,
1686
+ ...(hookText ? [hookText] : []),
1620
1687
  "",
1621
1688
  providerText,
1622
1689
  "",
1623
1690
  `Doctor:\n${doctorLines(after.checks)}`,
1624
1691
  after.failed
1625
1692
  ? `[sable init] ✗ ${after.failed} failure(s) ${dryRun ? "now" : "left; the steps marked ! need you"}. Re-check with sable doctor.`
1626
- : `[sable init] ✓ wiring holds. Next: ${agent.next ?? "sable docs building-with-sable"}`,
1693
+ : `[sable init] ✓ wiring holds. Next: ${lowAdoption ? EXISTING_APP_NEXT : (agent.next ?? "sable docs building-with-sable")}`,
1627
1694
  "",
1628
1695
  ].join("\n"),
1629
1696
  )
@@ -1632,6 +1699,54 @@ function cmdInit() {
1632
1699
  process.exit(after.failed ? 1 : 0)
1633
1700
  }
1634
1701
 
1702
+ const EXISTING_APP_NEXT = "most of this app's UI isn't built from Sable yet. Tell your agent \"move this app onto Sable\": it lists what to swap and asks before changing anything."
1703
+
1704
+ /* ── upgrade ────────────────────────────────────────────────────────────────── */
1705
+
1706
+ /**
1707
+ * The whole update in one command: the package, then the NEW version's init.
1708
+ * This process is the old CLI, so after installing it hands over to the
1709
+ * freshly installed bin rather than running its own init: the skill entries,
1710
+ * the instructions block and the checks must be the new release's.
1711
+ */
1712
+ function cmdUpgrade() {
1713
+ let pkg
1714
+ try {
1715
+ pkg = JSON.parse(readFileSync(join(CWD, "package.json"), "utf8"))
1716
+ } catch {
1717
+ fail("ERR_UPGRADE", `No package.json in ${CWD}.`, ["Run sable upgrade from your app's root."])
1718
+ }
1719
+ const spec = pkg.dependencies?.["@sableui/react"] ?? pkg.devDependencies?.["@sableui/react"]
1720
+ if (!spec) fail("ERR_UPGRADE", "@sableui/react is not a dependency of this project.", ["pnpm add @sableui/react, then pnpm exec sable init"])
1721
+ const pm = detectPackageManager(CWD)
1722
+ const dryRun = flags.has("dry-run")
1723
+ // A tarball, link or workspace copy is not something "latest" can replace.
1724
+ const local = /^(?:file:|link:|workspace:|portal:|\.{0,2}\/)/.test(String(spec))
1725
+ const install = installCommand(pm, ["@sableui/react@latest"], { dev: !pkg.dependencies?.["@sableui/react"] })
1726
+ const say = (line) => { if (!JSON_OUT) process.stdout.write(`${line}\n`) }
1727
+
1728
+ if (flags.has("no-install")) say("sable upgrade — skipping the package update (--no-install).")
1729
+ else if (local) say(`sable upgrade — @sableui/react comes from ${spec}, which "latest" can't replace. Update that copy yourself; running the installed version's init.`)
1730
+ else if (dryRun) say(`sable upgrade — would run: ${install.join(" ")}`)
1731
+ else {
1732
+ say(`sable upgrade — ${install.join(" ")}`)
1733
+ const r = spawnSync(install[0], install.slice(1), { cwd: CWD, stdio: JSON_OUT ? "pipe" : "inherit", shell: process.platform === "win32" })
1734
+ if (r.status !== 0) fail("ERR_UPGRADE", `${install.join(" ")} exited ${r.status}.`, ["Fix the install, then run sable upgrade --no-install."])
1735
+ }
1736
+
1737
+ // The installed package's own CLI, resolved the way the skill resolves it.
1738
+ let bin = join(PKG_ROOT, "bin", "sable.mjs")
1739
+ try {
1740
+ const registry = createRequire(join(CWD, "package.json")).resolve("@sableui/react/registry.json")
1741
+ const installed = join(dirname(registry), "bin", "sable.mjs")
1742
+ if (existsSync(installed)) bin = installed
1743
+ } catch { /* not resolvable from here (a workspace copy): this CLI's init is the installed one */ }
1744
+ const passed = ["dry-run", "hook", "no-hook", "no-instructions", "json"].filter((f) => flags.has(f)).map((f) => `--${f}`)
1745
+ const r = spawnSync(process.execPath, [bin, "init", ...passed], { cwd: CWD, stdio: "inherit" })
1746
+ logInvocation(r.status ?? 1)
1747
+ process.exit(r.status ?? 1)
1748
+ }
1749
+
1635
1750
  function doctorLines(checks, indent = " ") {
1636
1751
  const mark = { pass: "✓", warn: "!", fail: "✗", info: "·" }
1637
1752
  return checks
@@ -1751,7 +1866,8 @@ function cmdCheck() {
1751
1866
  details: doctorFailures.map((c) => `${c.name}: ${c.detail}${c.fix ? ` → ${c.fix}` : ""}`),
1752
1867
  // Warnings and notes don't fail the check, but they are things to act on,
1753
1868
  // and a passing check that printed nothing else hid them from the agent.
1754
- notes: doctor.checks.filter((c) => c.status === "warn" || c.status === "info").map((c) => `${c.name}: ${c.detail}${c.fix ? ` → ${c.fix}` : ""}`),
1869
+ // Adoption is reported once, from grade, with its swaps.
1870
+ notes: doctor.checks.filter((c) => (c.status === "warn" || c.status === "info") && c.name !== "adoption").map((c) => `${c.name}: ${c.detail}${c.fix ? ` → ${c.fix}` : ""}`),
1755
1871
  data: doctor,
1756
1872
  })
1757
1873
 
@@ -1807,14 +1923,20 @@ function cmdCheck() {
1807
1923
  /* App-owned surfaces, from grade. Never a failure: the input to the reuse
1808
1924
  * inventory (finish step 6), which otherwise gets written from memory. */
1809
1925
  const inventory = grade && grade.type !== "error" ? (grade.inventory ?? null) : null
1926
+ /* Adoption, from grade. Never a failure either: wiring can hold while the app
1927
+ * composes almost none of Sable, and the move onto it is the user's call. */
1928
+ const adoption = grade && grade.type !== "error" ? (grade.adoption ?? null) : null
1929
+ const lowAdoption = adoption && (adoption.level === "none" || adoption.level === "low")
1810
1930
 
1811
1931
  const failed = checks.filter((c) => c.status !== "pass").map((c) => c.name)
1812
1932
  // A build that still scans all of Sable ships every component's classes;
1813
1933
  // trimming it is a finish step, because sources are only final once the app is.
1814
- const manual = doctor.checks.some((c) => c.name === "css:size")
1815
- ? [...MANUAL_REVIEW, { step: "Ship only the CSS you use", how: "sable sources --write src/sable-sources.css, then @import it in place of the whole-package @source (about 32 KB of CSS gzipped down to what the app imports). Re-run it whenever you import something new from Sable; sable check fails while it is stale." }]
1816
- : MANUAL_REVIEW
1817
- const data = { paths, passed: !failed.length, failed, checks, inventory, manualReview: manual }
1934
+ const manual = [
1935
+ ...(lowAdoption ? [{ step: "Ask about moving onto Sable", how: `${adoption.recommendation} The swaps are listed under Adoption above; sable docs auditing-an-app ("Adoption") covers how to put the choice to the user.` }] : []),
1936
+ ...MANUAL_REVIEW,
1937
+ ...(doctor.checks.some((c) => c.name === "css:size") ? [{ step: "Ship only the CSS you use", how: "sable sources --write src/sable-sources.css, then @import it in place of the whole-package @source (about 32 KB of CSS gzipped down to what the app imports). Re-run it whenever you import something new from Sable; sable check fails while it is stale." }] : []),
1938
+ ]
1939
+ const data = { paths, passed: !failed.length, failed, checks, inventory, adoption, manualReview: manual }
1818
1940
  const MARK = { pass: "✓", fail: "✗", error: "✗" }
1819
1941
  const MAX_DETAILS = 12
1820
1942
  const summaryText = () =>
@@ -1840,6 +1962,16 @@ function cmdCheck() {
1840
1962
  "",
1841
1963
  ].join("\n")
1842
1964
  }
1965
+ const adoptionText = () => {
1966
+ if (!adoption || adoption.level === "empty" || (adoption.level === "sable" && !adoption.swaps.length)) return ""
1967
+ return [
1968
+ "",
1969
+ `Adoption — ${adoption.level}. ${adoption.summary}`,
1970
+ ` ${adoption.recommendation}`,
1971
+ ...swapLines(adoption, MAX_DETAILS),
1972
+ "",
1973
+ ].join("\n")
1974
+ }
1843
1975
  const manualText = () =>
1844
1976
  [
1845
1977
  "",
@@ -1869,7 +2001,7 @@ function cmdCheck() {
1869
2001
  }
1870
2002
 
1871
2003
  if (JSON_OUT) process.stdout.write(`${JSON.stringify({ type: "check", data }, null, DENSE ? 0 : 1)}\n`)
1872
- else process.stdout.write(`${summaryText()}\n${inventoryText()}${manualText()}\n`)
2004
+ else process.stdout.write(`${summaryText()}\n${adoptionText()}${inventoryText()}${manualText()}\n`)
1873
2005
  logInvocation(failed.length ? 1 : 0, failed.length ? "ERR_CHECK_FAILED" : undefined)
1874
2006
  process.exit(failed.length ? 1 : 0)
1875
2007
  }
@@ -1899,6 +2031,15 @@ function cmdDesignTool(script) {
1899
2031
  passThrough(runScript(join(PKG_ROOT, "scripts", script)))
1900
2032
  }
1901
2033
 
2034
+ /** What setup did to the project instructions, in one line. */
2035
+ function instructionsText(data) {
2036
+ if (!data.instructions.length) return "Project instructions: skipped (--no-instructions). Agents use the skill only when a request names it."
2037
+ const changed = data.instructionsChanged
2038
+ const files = data.instructions.join(", ")
2039
+ if (!changed.length) return `Project instructions: ${files} already carry the Sable block.`
2040
+ return `Project instructions: ${data.dryRun ? "would add" : "added"} the Sable block to ${changed.join(", ")}, so every UI request goes through the skill. Edit outside its markers; --no-instructions skips it.`
2041
+ }
2042
+
1902
2043
  function cmdSetup() {
1903
2044
  if (!flags.has("agent") || rest.length) fail("ERR_BAD_ARGUMENT", "Use sable setup --agent from your project root.", ["sable setup --agent --dry-run"])
1904
2045
  const root = flags.get("root")
@@ -1908,7 +2049,7 @@ function cmdSetup() {
1908
2049
  if (flags.has("hook") && !forClaude)
1909
2050
  fail("ERR_BAD_ARGUMENT", "--hook installs a Claude Code Stop hook; use it with --target claude or all.", ["sable setup --agent --target claude --hook"])
1910
2051
  try {
1911
- const options = { root: root ? resolve(CWD, root) : CWD, project: CWD, packageRoot: PKG_ROOT, target, update: flags.has("update"), dryRun: flags.has("dry-run") }
2052
+ const options = { root: root ? resolve(CWD, root) : CWD, project: CWD, packageRoot: PKG_ROOT, target, update: flags.has("update"), dryRun: flags.has("dry-run"), instructions: !flags.has("no-instructions") }
1912
2053
  // Preflight the settings file too, so a refusal there writes nothing anywhere.
1913
2054
  if (flags.has("hook")) installClaudeStopHook({ ...options, dryRun: true })
1914
2055
  const data = installAgentSkill(options)
@@ -1922,7 +2063,7 @@ function cmdSetup() {
1922
2063
  : hook.installed
1923
2064
  ? `\n${data.dryRun ? "Would add" : hook.alreadyPresent ? "Already present:" : "Added"} a Claude Code Stop hook to ${hook.settingsFile}: Claude cannot end a task while \`sable check\` fails.`
1924
2065
  : `\nOptional: block Claude Code from ending a task while \`sable check\` fails. Add this to ${hook.settingsFile} (merge into an existing "hooks" key), or rerun with --hook to add it:\n\n${JSON.stringify(hook.settings, null, 2)}\n\nThe hook runs: ${hook.command}`
1925
- ok("setup.agent", data, `${data.dryRun ? "Would install" : "Installed"} Sable skill entries at ${data.destinations.map((d) => relative(CWD, join(data.root, d)).replaceAll("\\", "/")).join(", ")}.\nNext: ${data.next}${hookText}`)
2066
+ ok("setup.agent", data, `${data.dryRun ? "Would install" : "Installed"} Sable skill entries at ${data.destinations.map((d) => relative(CWD, join(data.root, d)).replaceAll("\\", "/")).join(", ")}.\n${instructionsText(data)}\nNext: ${data.next}${hookText}`)
1926
2067
  } catch (error) {
1927
2068
  fail("ERR_AGENT_SETUP", error.message, ["sable setup --agent --dry-run"])
1928
2069
  }
@@ -1961,6 +2102,7 @@ const HANDLERS = {
1961
2102
  docs: cmdDocs,
1962
2103
  doctor: cmdDoctor,
1963
2104
  init: cmdInit,
2105
+ upgrade: cmdUpgrade,
1964
2106
  grade: cmdGrade,
1965
2107
  learn: cmdLearn,
1966
2108
  atlas: () => cmdDesignTool("design-atlas.mjs"),
@@ -0,0 +1,25 @@
1
+ /** One day of activity. Only `date` and `value` are required. */
2
+ interface ActivityDay {
3
+ /** The calendar day, "YYYY-MM-DD". Read as a day, never as an instant. */
4
+ date: string;
5
+ /** The measure for that day. A day missing from the data counts as 0. */
6
+ value: number;
7
+ /** 0…levels-1. Overrides the computed level — GitHub's API sends its own. */
8
+ level?: number;
9
+ /** A key into the heatmap's `categories`, for multi-source cells. */
10
+ category?: string;
11
+ }
12
+ /** Days as a list, or as a map of date to value (GitLab's and Mantine's shape). */
13
+ type ActivityData = ActivityDay[] | Record<string, number>;
14
+ /**
15
+ * Several sources' days merged into one list with categories: a day active in
16
+ * one source takes that source's key, a day active in more than one takes
17
+ * `both`. Values sum. For "GitHub, Codex, or both on that day".
18
+ *
19
+ * @example mergeActivity({ github: githubDays, codex: codexDays })
20
+ */
21
+ declare function mergeActivity(sources: Record<string, ActivityData>, { both }?: {
22
+ both?: string;
23
+ }): ActivityDay[];
24
+
25
+ export { type ActivityDay as A, type ActivityData as a, mergeActivity as m };
@@ -0,0 +1,41 @@
1
+ import { A as ActivityDay } from '../activity-DT8VK9fx.js';
2
+
3
+ /**
4
+ * GitHub contribution data → ActivityHeatmap days. Pure transforms: Sable never
5
+ * fetches. GitHub's GraphQL API needs a token, so call it from your server and
6
+ * pass the result through; the docs page shows a route handler that does.
7
+ */
8
+ /** The `contributionLevel` enum GitHub's GraphQL API returns per day. */
9
+ type GitHubContributionLevel = "NONE" | "FIRST_QUARTILE" | "SECOND_QUARTILE" | "THIRD_QUARTILE" | "FOURTH_QUARTILE";
10
+ /** The slice of `user.contributionsCollection.contributionCalendar` the transform reads. */
11
+ interface GitHubContributionCalendar {
12
+ weeks: {
13
+ contributionDays: {
14
+ date: string;
15
+ contributionCount: number;
16
+ contributionLevel?: GitHubContributionLevel;
17
+ }[];
18
+ }[];
19
+ }
20
+ /**
21
+ * From GraphQL `contributionsCollection { contributionCalendar { weeks { contributionDays
22
+ * { date contributionCount contributionLevel } } } }`. GitHub's own quartile levels pass
23
+ * through as 0–4, so the grid matches the profile page exactly (use the default 5 levels).
24
+ */
25
+ declare function fromGitHubGraphQL(calendar: GitHubContributionCalendar): ActivityDay[];
26
+ /** The response of the community github-contributions-api (`/v4/{user}`). */
27
+ interface GitHubContributionsApiResponse {
28
+ contributions: {
29
+ date: string;
30
+ count: number;
31
+ level?: number;
32
+ }[];
33
+ }
34
+ /**
35
+ * From the community github-contributions-api (`https://github-contributions-api.jogruber.de/v4/{user}`).
36
+ * It is a third-party scraper of the profile page — cached for an hour and rate
37
+ * limited — so treat it as a convenience for demos, not a dependency.
38
+ */
39
+ declare function fromGitHubContributionsApi(json: GitHubContributionsApiResponse): ActivityDay[];
40
+
41
+ export { type GitHubContributionCalendar, type GitHubContributionLevel, type GitHubContributionsApiResponse, fromGitHubContributionsApi, fromGitHubGraphQL };