contrib-graphs 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Francis Miguel Castillo Cruz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,53 @@
1
- # Temporary Holding Version
1
+ # contrib-graphs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ![Isometric contribution graph](https://raw.githubusercontent.com/ingfranciscastillo/contrib-graphs/main/docs/images/isometric-light.svg)
4
+
5
+ Contribution graph components for React: GitHub-style activity heatmaps in seven designs (Classic, Dots, Isometric, Terminal, Radial, Skyline, Compact), with themes, light/dark mode, keyboard navigation and screen reader support.
6
+
7
+ ```bash
8
+ npm install contrib-graphs
9
+ ```
10
+
11
+ ```tsx
12
+ import { ClassicGraph } from 'contrib-graphs'
13
+ import 'contrib-graphs/styles.css'
14
+
15
+ const data = [
16
+ { date: '2026-11-10', count: 4 },
17
+ { date: '2026-11-11', count: 9 },
18
+ ]
19
+
20
+ export function Activity() {
21
+ return <ClassicGraph data={data} theme="github" animate />
22
+ }
23
+ ```
24
+
25
+ Every component takes the same `{ date, count }[]` data. Adapters for GitHub and GitLab (`fromGitHub`, `fromGitLab`, `fetchGitHubContributions`, `fetchGitLabContributions`) and generic sources (`fromDates`, `mergeSources`) are included.
26
+
27
+ Requires React 18 or later.
28
+
29
+ ## Next.js, Astro and server rendering
30
+
31
+ - `contrib-graphs` exports the components and starts with `"use client"`, so you can render them straight from a Next.js Server Component.
32
+ - `contrib-graphs/data` exports everything else (calendar helpers, themes, adapters, sample data) with no React dependency. Use it on the server, for example `fetchGitHubContributions` in a Server Component or an Astro page.
33
+ - In Astro (with `@astrojs/react`), a graph without a `client:*` directive ships as static HTML with no JavaScript; add `client:visible` for tooltips and keyboard navigation. Functions can't be passed to islands, so wrap the graph in a React component to use `onDayClick`.
34
+ - When rendering on the server, pass `endDate` (for real data, the last date in it): the server and the browser may be on different days. Dates and numbers default to the `en` locale on both sides.
35
+
36
+ ```tsx
37
+ // app/page.tsx (Next.js)
38
+ import { ClassicGraph } from 'contrib-graphs'
39
+ import { fetchGitHubContributions } from 'contrib-graphs/data'
40
+
41
+ export default async function Page() {
42
+ const data = await fetchGitHubContributions('your-username', process.env.GITHUB_TOKEN!)
43
+ return <ClassicGraph data={data} endDate={data.at(-1)?.date} />
44
+ }
45
+ ```
46
+
47
+ Tested with Next.js 16 (React 19) and Astro 6.
48
+
49
+ Live gallery, playground and full API reference: [ingfranciscastillo.github.io/contrib-graphs](https://ingfranciscastillo.github.io/contrib-graphs/). Source and more designs: [github.com/ingfranciscastillo/contrib-graphs](https://github.com/ingfranciscastillo/contrib-graphs).
50
+
51
+ ## License
52
+
53
+ MIT
@@ -0,0 +1,344 @@
1
+
2
+ import { CSSProperties, ReactNode } from "react";
3
+ /** Colors of levels 0 to 4. */
4
+ type LevelColors = readonly [string, string, string, string, string];
5
+ /** Colors for one color mode. */
6
+ interface ThemePalette {
7
+ /** Colors of levels 0 to 4. */
8
+ levels: LevelColors;
9
+ text: string;
10
+ muted: string;
11
+ /** Background of variants with their own surface, such as Terminal. */
12
+ surface: string;
13
+ border: string;
14
+ }
15
+ /** A theme: a name plus a light and a dark palette. */
16
+ interface GraphTheme {
17
+ name: string;
18
+ light: ThemePalette;
19
+ dark: ThemePalette;
20
+ }
21
+ /** Built-in themes: `github`, `gitlab`, `frost`, `ember`, `violet` and `mono`. */
22
+ declare const themes: {
23
+ github: {
24
+ name: string;
25
+ light: {
26
+ levels: [string, string, string, string, string];
27
+ text: string;
28
+ muted: string;
29
+ surface: string;
30
+ border: string;
31
+ };
32
+ dark: {
33
+ levels: [string, string, string, string, string];
34
+ text: string;
35
+ muted: string;
36
+ surface: string;
37
+ border: string;
38
+ };
39
+ };
40
+ gitlab: {
41
+ name: string;
42
+ light: {
43
+ levels: [string, string, string, string, string];
44
+ text: string;
45
+ muted: string;
46
+ surface: string;
47
+ border: string;
48
+ };
49
+ dark: {
50
+ surface: string;
51
+ border: string;
52
+ levels: [string, string, string, string, string];
53
+ text: string;
54
+ muted: string;
55
+ };
56
+ };
57
+ frost: {
58
+ name: string;
59
+ light: {
60
+ levels: [string, string, string, string, string];
61
+ text: string;
62
+ muted: string;
63
+ surface: string;
64
+ border: string;
65
+ };
66
+ dark: {
67
+ surface: string;
68
+ border: string;
69
+ text: string;
70
+ muted: string;
71
+ levels: [string, string, string, string, string];
72
+ };
73
+ };
74
+ ember: {
75
+ name: string;
76
+ light: {
77
+ levels: [string, string, string, string, string];
78
+ text: string;
79
+ muted: string;
80
+ surface: string;
81
+ border: string;
82
+ };
83
+ dark: {
84
+ surface: string;
85
+ border: string;
86
+ levels: [string, string, string, string, string];
87
+ text: string;
88
+ muted: string;
89
+ };
90
+ };
91
+ violet: {
92
+ name: string;
93
+ light: {
94
+ levels: [string, string, string, string, string];
95
+ text: string;
96
+ muted: string;
97
+ surface: string;
98
+ border: string;
99
+ };
100
+ dark: {
101
+ surface: string;
102
+ border: string;
103
+ levels: [string, string, string, string, string];
104
+ text: string;
105
+ muted: string;
106
+ };
107
+ };
108
+ mono: {
109
+ name: string;
110
+ light: {
111
+ levels: [string, string, string, string, string];
112
+ text: string;
113
+ muted: string;
114
+ surface: string;
115
+ border: string;
116
+ };
117
+ dark: {
118
+ surface: string;
119
+ border: string;
120
+ levels: [string, string, string, string, string];
121
+ text: string;
122
+ muted: string;
123
+ };
124
+ };
125
+ };
126
+ /** Name of a built-in theme. */
127
+ type ThemeName = keyof typeof themes;
128
+ /** Names of the built-in themes. */
129
+ declare const themeNames: ThemeName[];
130
+ /** Returns the theme object for a preset name or a custom theme; unknown names fall back to `github`. */
131
+ declare function resolveTheme(theme: ThemeName | GraphTheme | undefined): GraphTheme;
132
+ /** Turns a theme into CSS variables (`--cg-l0-light`, `--cg-l0-dark`…); the stylesheet picks light or dark. */
133
+ declare function themeVars(theme: GraphTheme): CSSProperties;
134
+ /** One day of activity. `date` uses the ISO format `YYYY-MM-DD`. */
135
+ interface ContributionDay {
136
+ date: string;
137
+ count: number;
138
+ }
139
+ /** Intensity level from 0 (no activity) to 4 (busiest). */
140
+ type Level = 0 | 1 | 2 | 3 | 4;
141
+ /**
142
+ * How each count maps to a level from 0 to 4.
143
+ * - `quantile`: by quartiles of the active days (like GitHub)
144
+ * - `linear`: proportional to the maximum
145
+ * - `[a, b, c, d]`: minimum count for levels 1, 2, 3 and 4
146
+ */
147
+ type LevelScale = 'quantile' | 'linear' | readonly [number, number, number, number];
148
+ /** `auto` follows `prefers-color-scheme`, or `data-theme="light|dark"` on `<html>`. */
149
+ type ColorMode = 'auto' | 'light' | 'dark';
150
+ /** A day as laid out in the calendar, with its level and position. */
151
+ interface CalendarDay extends ContributionDay {
152
+ level: Level;
153
+ /** Flat index in `calendar.days`. */
154
+ index: number;
155
+ /** Column (week) in the calendar. */
156
+ weekIndex: number;
157
+ /** Row from 0 to 6, relative to `weekStart`. */
158
+ dayIndex: number;
159
+ /** Actual day of the week, 0 = Sunday. */
160
+ weekday: number;
161
+ }
162
+ /** A month label and the week column where it starts. */
163
+ interface MonthLabel {
164
+ label: string;
165
+ weekIndex: number;
166
+ month: number;
167
+ year: number;
168
+ }
169
+ /** The result of `buildCalendar`: a weeks × 7 grid plus totals and month labels. */
170
+ interface Calendar {
171
+ weeks: (CalendarDay | null)[][];
172
+ days: CalendarDay[];
173
+ months: MonthLabel[];
174
+ total: number;
175
+ max: number;
176
+ start: string;
177
+ end: string;
178
+ levelOf: (count: number) => Level;
179
+ }
180
+ /** Props shared by every graph. */
181
+ interface BaseGraphProps {
182
+ /** Days of activity. Missing days count as 0 and duplicate dates are added up. */
183
+ data: ContributionDay[];
184
+ /**
185
+ * A preset name or your own theme object.
186
+ * @default 'github'
187
+ */
188
+ theme?: ThemeName | GraphTheme;
189
+ /**
190
+ * `auto` follows `prefers-color-scheme`, or `data-theme="light|dark"` on `<html>`.
191
+ * @default 'auto'
192
+ */
193
+ colorMode?: ColorMode;
194
+ /**
195
+ * Number of weeks to show.
196
+ * @default 53
197
+ */
198
+ weeks?: number;
199
+ /**
200
+ * Last day of the calendar. When rendering on the server, always pass it:
201
+ * the server and the browser may be on different days.
202
+ * @default today
203
+ */
204
+ endDate?: string | Date;
205
+ /**
206
+ * First day of each column: 0 = Sunday, 1 = Monday.
207
+ * @default 0
208
+ */
209
+ weekStart?: 0 | 1;
210
+ /**
211
+ * How counts map to the five color levels.
212
+ * @default 'quantile'
213
+ */
214
+ scale?: LevelScale;
215
+ /**
216
+ * Locale for dates and numbers. Fixed by default so server and client HTML match.
217
+ * @default 'en'
218
+ */
219
+ locale?: string;
220
+ /**
221
+ * Staggered entry animation. Turned off with `prefers-reduced-motion`.
222
+ * @default false
223
+ */
224
+ animate?: boolean;
225
+ /**
226
+ * Show the Less–More legend.
227
+ * @default true
228
+ */
229
+ showLegend?: boolean;
230
+ /**
231
+ * Show the header with the total.
232
+ * @default true
233
+ */
234
+ showTotal?: boolean;
235
+ /**
236
+ * Replaces the whole header text.
237
+ * @default '{total} contributions in the last year'
238
+ */
239
+ title?: ReactNode;
240
+ /**
241
+ * Singular and plural of the unit being counted.
242
+ * @default ['contribution', 'contributions']
243
+ */
244
+ unit?: readonly [string, string];
245
+ /**
246
+ * Text after the total in the header. Can be a function of the weeks shown.
247
+ * @default 'in the last year' (52+ weeks), otherwise 'in the last {weeks} weeks'
248
+ */
249
+ periodLabel?: string | ((weeks: number) => string);
250
+ /**
251
+ * Labels at both ends of the legend.
252
+ * @default ['Less', 'More']
253
+ */
254
+ legendLabels?: readonly [string, string];
255
+ /** Called when a day is clicked, or with Enter or Space while it is focused. */
256
+ onDayClick?: (day: CalendarDay) => void;
257
+ /** Extra class name for the root element. */
258
+ className?: string;
259
+ /** Inline styles for the root element. Theme CSS variables can be overridden here. */
260
+ style?: CSSProperties;
261
+ /**
262
+ * Accessible name of the graph.
263
+ * @default a summary with the total and the date range
264
+ */
265
+ 'aria-label'?: string;
266
+ }
267
+ /** Returns a function that maps a count to a level from 0 to 4 for the given counts and scale. */
268
+ declare function makeLevelFn(counts: readonly number[], scale?: LevelScale): (n: number) => Level;
269
+ /** Options of `buildCalendar`. They match the graph props of the same name. */
270
+ interface CalendarOptions {
271
+ weeks?: number;
272
+ endDate?: string | Date;
273
+ weekStart?: 0 | 1;
274
+ scale?: LevelScale;
275
+ locale?: string;
276
+ }
277
+ /** Lays out days as a weeks × 7 grid with levels, totals and month labels. Dates are handled in UTC. */
278
+ declare function buildCalendar(data: readonly ContributionDay[], opts?: CalendarOptions): Calendar;
279
+ /** Keeps the last `n` weeks. Levels come from the full calendar, so cropping doesn't change colors. */
280
+ declare function cropCalendar(cal: Calendar, n: number, locale?: string): Calendar;
281
+ /** Totals and streaks of a list of days. */
282
+ interface GraphStats {
283
+ total: number;
284
+ activeDays: number;
285
+ best: CalendarDay | null;
286
+ longestStreak: number;
287
+ currentStreak: number;
288
+ }
289
+ /** Total, active days, best day, longest streak and current streak. A current streak isn't broken by a quiet today. */
290
+ declare function computeStats(days: readonly CalendarDay[]): GraphStats;
291
+ /** Formats an ISO date with `Intl`, in UTC so the day never shifts. */
292
+ declare function formatDate(iso: string, locale?: string, opts?: Intl.DateTimeFormatOptions): string;
293
+ /** Shape of the generated activity. */
294
+ type SampleProfile = 'steady' | 'burst' | 'weekend' | 'sparse';
295
+ /** Available sample profiles with a short English label. */
296
+ declare const sampleProfiles: Record<SampleProfile, string>;
297
+ /** Deterministic sample data: the same seed always gives the same days. Useful for demos and tests. */
298
+ declare function generateSampleData({ seed, profile, days, endDate }?: {
299
+ seed?: number;
300
+ profile?: SampleProfile;
301
+ days?: number;
302
+ endDate?: Date;
303
+ }): ContributionDay[];
304
+ /**
305
+ * GitHub GraphQL query for the contribution calendar. It needs a token even
306
+ * for public data, so run it on the server and never send the token to the browser.
307
+ */
308
+ declare const GITHUB_CONTRIBUTIONS_QUERY = "\n query ($login: String!, $from: DateTime, $to: DateTime) {\n user(login: $login) {\n contributionsCollection(from: $from, to: $to) {\n contributionCalendar {\n totalContributions\n weeks { contributionDays { date contributionCount } }\n }\n }\n }\n }\n";
309
+ /** Shape of the GraphQL response read by `fromGitHub`. */
310
+ interface GitHubCalendarResponse {
311
+ data?: {
312
+ user?: {
313
+ contributionsCollection?: {
314
+ contributionCalendar?: {
315
+ weeks: {
316
+ contributionDays: {
317
+ date: string;
318
+ contributionCount: number;
319
+ }[];
320
+ }[];
321
+ };
322
+ };
323
+ } | null;
324
+ };
325
+ }
326
+ /** Converts a GitHub GraphQL response into `{ date, count }` days. */
327
+ declare function fromGitHub(res: GitHubCalendarResponse): ContributionDay[];
328
+ /**
329
+ * Fetches the last year of contributions from GitHub's GraphQL API. Server only (Node 18+, Deno, Bun, edge runtimes).
330
+ * A classic token with `read:user` includes private contributions.
331
+ */
332
+ declare function fetchGitHubContributions(login: string, token: string): Promise<ContributionDay[]>;
333
+ /** Converts GitLab's `{ "2026-03-01": 4 }` calendar into `{ date, count }` days. */
334
+ declare function fromGitLab(json: Record<string, number>): ContributionDay[];
335
+ /**
336
+ * Fetches a GitLab user's calendar. The endpoint is public but sends no CORS headers,
337
+ * so call it from the server or at build time. Pass `baseUrl` for self-hosted instances.
338
+ */
339
+ declare function fetchGitLabContributions(username: string, baseUrl?: string): Promise<ContributionDay[]>;
340
+ /** Counts occurrences per day: blog posts, workouts, local commits… Strings are sliced to the date; `Date` objects use the local date. */
341
+ declare function fromDates(dates: readonly (string | Date)[]): ContributionDay[];
342
+ /** Adds several sources day by day (for example GitHub plus GitLab), sorted by date. */
343
+ declare function mergeSources(...sources: readonly ContributionDay[][]): ContributionDay[];
344
+ export { ThemePalette as A, ContributionDay as C, GraphTheme as D, MonthLabel as E, themeNames as M, themeVars as N, LevelColors as O, themes as P, ColorMode as S, LevelScale as T, formatDate as _, fromDates as a, Calendar as b, mergeSources as c, sampleProfiles as d, CalendarOptions as f, cropCalendar as g, computeStats as h, fetchGitLabContributions as i, resolveTheme as j, ThemeName as k, SampleProfile as l, buildCalendar as m, GitHubCalendarResponse as n, fromGitHub as o, GraphStats as p, fetchGitHubContributions as r, fromGitLab as s, GITHUB_CONTRIBUTIONS_QUERY as t, generateSampleData as u, makeLevelFn as v, Level as w, CalendarDay as x, BaseGraphProps as y };