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/dist/index.js ADDED
@@ -0,0 +1,1098 @@
1
+ 'use client';
2
+ import { _ as weekdayLabels, a as fromGitHub, b as themeVars, c as generateSampleData, d as computeStats, f as cropCalendar, g as pluralize, h as makeLevelFn, i as fromDates, l as sampleProfiles, m as formatNumber, n as fetchGitHubContributions, o as fromGitLab, p as formatDate, r as fetchGitLabContributions, s as mergeSources, t as GITHUB_CONTRIBUTIONS_QUERY, u as buildCalendar, v as resolveTheme, x as themes, y as themeNames } from "./data-oHgoFFM5.js";
3
+ import { Fragment, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
+ // src/core.tsx
6
+ /**
7
+ * useLayoutEffect en el navegador (mide antes de pintar) y useEffect en el
8
+ * servidor, donde los efectos no corren y React avisaría por cada componente.
9
+ */
10
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
11
+ /** `:focus-visible` no existe en navegadores viejos (Safari < 15.4) ni en algunos entornos de test. */
12
+ function isFocusVisible(el) {
13
+ try {
14
+ return el.matches(":focus-visible");
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+ const indexOf = (target) => {
20
+ const el = target?.closest?.("[data-cg-i]");
21
+ return el ? Number(el.getAttribute("data-cg-i")) : null;
22
+ };
23
+ /**
24
+ * Hover + navegación con teclado usando delegación de eventos: las celdas solo
25
+ * llevan `data-cg-i`, así que no se crean cientos de handlers. Flechas izquierda/derecha
26
+ * avanzan `stepX` elementos y arriba/abajo `stepY`.
27
+ */
28
+ function useGraphInteraction(opts) {
29
+ const { rootRef, count, stepX, stepY, onActivate } = opts;
30
+ const [state, setState] = useState(null);
31
+ const [pos, setPos] = useState(null);
32
+ const index = state && state.index < count ? state.index : null;
33
+ useIsomorphicLayoutEffect(() => {
34
+ const root = rootRef.current;
35
+ if (!root) return;
36
+ const el = index == null ? null : root.querySelector(`[data-cg-i="${index}"]`);
37
+ root.querySelectorAll("[data-active]").forEach((n) => n !== el && n.removeAttribute("data-active"));
38
+ el?.setAttribute("data-active", "");
39
+ });
40
+ useIsomorphicLayoutEffect(() => {
41
+ const root = rootRef.current;
42
+ const el = index == null || !root ? null : root.querySelector(`[data-cg-i="${index}"]`);
43
+ if (!root || !el) {
44
+ setPos(null);
45
+ return;
46
+ }
47
+ const r = root.getBoundingClientRect();
48
+ const e = el.getBoundingClientRect();
49
+ setPos({
50
+ x: e.left + e.width / 2 - r.left,
51
+ y: e.top - r.top
52
+ });
53
+ }, [
54
+ rootRef,
55
+ index,
56
+ count
57
+ ]);
58
+ const onKeyDown = useCallback((e) => {
59
+ const cur = index ?? count - 1;
60
+ let next = null;
61
+ switch (e.key) {
62
+ case "ArrowRight":
63
+ next = cur + stepX;
64
+ break;
65
+ case "ArrowLeft":
66
+ next = cur - stepX;
67
+ break;
68
+ case "ArrowDown":
69
+ next = stepY ? cur + stepY : null;
70
+ break;
71
+ case "ArrowUp":
72
+ next = stepY ? cur - stepY : null;
73
+ break;
74
+ case "Home":
75
+ next = 0;
76
+ break;
77
+ case "End":
78
+ next = count - 1;
79
+ break;
80
+ case "Escape":
81
+ setState(null);
82
+ return;
83
+ case "Enter":
84
+ case " ":
85
+ if (index != null) {
86
+ e.preventDefault();
87
+ onActivate?.(index);
88
+ }
89
+ return;
90
+ default: return;
91
+ }
92
+ if (next == null || count === 0) return;
93
+ e.preventDefault();
94
+ setState({
95
+ index: Math.max(0, Math.min(count - 1, next)),
96
+ via: "keyboard"
97
+ });
98
+ }, [
99
+ index,
100
+ count,
101
+ stepX,
102
+ stepY,
103
+ onActivate
104
+ ]);
105
+ return {
106
+ rootRef,
107
+ active: index,
108
+ via: state?.via ?? null,
109
+ pos,
110
+ handlers: {
111
+ tabIndex: 0,
112
+ onKeyDown,
113
+ onPointerOver: (e) => {
114
+ const i = indexOf(e.target);
115
+ if (i != null && i !== index) setState({
116
+ index: i,
117
+ via: "pointer"
118
+ });
119
+ },
120
+ onPointerLeave: () => setState((s) => s?.via === "pointer" ? null : s),
121
+ onClick: (e) => {
122
+ const i = indexOf(e.target);
123
+ if (i != null) onActivate?.(i);
124
+ },
125
+ onFocus: (e) => {
126
+ if (e.target === e.currentTarget && isFocusVisible(e.currentTarget)) setState((s) => s ?? {
127
+ index: count - 1,
128
+ via: "keyboard"
129
+ });
130
+ },
131
+ onBlur: () => setState((s) => s?.via === "keyboard" ? null : s)
132
+ }
133
+ };
134
+ }
135
+ /** Reduce las semanas visibles al ancho del contenedor (en vez de hacer scroll). */
136
+ function useFitWeeks(ref, requested, pitch, reserved, enabled) {
137
+ const [fit, setFit] = useState(requested);
138
+ useIsomorphicLayoutEffect(() => {
139
+ const el = ref.current;
140
+ if (!enabled || !el || typeof ResizeObserver === "undefined") {
141
+ setFit(requested);
142
+ return;
143
+ }
144
+ const update = () => {
145
+ const w = el.clientWidth;
146
+ if (w > 0) setFit(Math.max(4, Math.min(requested, Math.floor((w - reserved) / pitch))));
147
+ };
148
+ update();
149
+ const ro = new ResizeObserver(update);
150
+ ro.observe(el);
151
+ return () => ro.disconnect();
152
+ }, [
153
+ ref,
154
+ requested,
155
+ pitch,
156
+ reserved,
157
+ enabled
158
+ ]);
159
+ return enabled ? fit : requested;
160
+ }
161
+ /** Monta calendario + interacción. Es la base de todas las variantes por día. */
162
+ function useCalendarGraph(props, opts = {}) {
163
+ const rootRef = useRef(null);
164
+ const requested = props.weeks ?? 53;
165
+ const weeks = useFitWeeks(rootRef, requested, opts.fit?.pitch ?? 1, opts.fit?.reserved ?? 0, opts.fit?.enabled ?? false);
166
+ const endKey = props.endDate instanceof Date ? props.endDate.getTime() : props.endDate;
167
+ const scaleKey = typeof props.scale === "string" ? props.scale : props.scale?.join(",");
168
+ const full = useMemo(() => buildCalendar(props.data, {
169
+ weeks: requested,
170
+ endDate: props.endDate,
171
+ weekStart: props.weekStart,
172
+ scale: props.scale,
173
+ locale: props.locale
174
+ }), [
175
+ props.data,
176
+ requested,
177
+ endKey,
178
+ props.weekStart,
179
+ scaleKey,
180
+ props.locale
181
+ ]);
182
+ const calendar = useMemo(() => cropCalendar(full, weeks, props.locale), [
183
+ full,
184
+ weeks,
185
+ props.locale
186
+ ]);
187
+ const { onDayClick } = props;
188
+ const onActivate = useCallback((i) => onDayClick?.(calendar.days[i]), [onDayClick, calendar]);
189
+ const interaction = useGraphInteraction({
190
+ rootRef,
191
+ count: calendar.days.length,
192
+ stepX: opts.stepX ?? 7,
193
+ stepY: opts.stepY ?? 1,
194
+ onActivate
195
+ });
196
+ /** `periodTotal` es el total del periodo pedido aunque se muestren menos semanas. */
197
+ return {
198
+ calendar,
199
+ periodTotal: full.total,
200
+ interaction,
201
+ theme: resolveTheme(props.theme)
202
+ };
203
+ }
204
+ function describeDay(day, props) {
205
+ const n = day.count;
206
+ return {
207
+ title: `${formatNumber(n, props.locale)} ${pluralize(n, props.unit)}`,
208
+ subtitle: formatDate(day.date, props.locale)
209
+ };
210
+ }
211
+ /** Texto del periodo según las semanas pedidas (no las recortadas por ancho). */
212
+ function periodText(props) {
213
+ const weeks = props.weeks ?? 53;
214
+ const p = props.periodLabel;
215
+ if (typeof p === "function") return p(weeks);
216
+ if (typeof p === "string") return p;
217
+ return weeks >= 52 ? "in the last year" : `in the last ${weeks} weeks`;
218
+ }
219
+ function defaultTitle(total, props) {
220
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
221
+ /* @__PURE__ */ jsx("strong", { children: formatNumber(total, props.locale) }),
222
+ " ",
223
+ pluralize(total, props.unit),
224
+ " ",
225
+ periodText(props)
226
+ ] });
227
+ }
228
+ const cellDelay = (w) => ({ "--cg-w": w });
229
+ /** Id estable y válido para usar en `url(#...)` dentro de SVG. */
230
+ function useSvgId(prefix) {
231
+ return `${prefix}-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
232
+ }
233
+ function Legend({ labels = ["Less", "More"], swatch }) {
234
+ return /* @__PURE__ */ jsxs("span", {
235
+ className: "cg-legend",
236
+ "aria-hidden": "true",
237
+ children: [
238
+ /* @__PURE__ */ jsx("span", { children: labels[0] }),
239
+ [
240
+ 0,
241
+ 1,
242
+ 2,
243
+ 3,
244
+ 4
245
+ ].map((l) => swatch ? /* @__PURE__ */ jsx("span", { children: swatch(l) }, l) : /* @__PURE__ */ jsx("span", {
246
+ className: "cg-swatch",
247
+ style: { background: `var(--cg-l${l})` }
248
+ }, l)),
249
+ /* @__PURE__ */ jsx("span", { children: labels[1] })
250
+ ]
251
+ });
252
+ }
253
+ /**
254
+ * Contenedor común: tema, modo claro/oscuro, encabezado, leyenda, tooltip
255
+ * y región aria-live para anunciar el día activo al navegar con teclado.
256
+ */
257
+ function GraphShell({ props, interaction, variant, describe, summary, title, headerEnd, footerStart, legendSwatch, glow, surface, children }) {
258
+ const theme = resolveTheme(props.theme);
259
+ const { active, pos, via } = interaction;
260
+ const desc = active != null ? describe(active) : null;
261
+ const tipRef = useRef(null);
262
+ const [shift, setShift] = useState(0);
263
+ useIsomorphicLayoutEffect(() => {
264
+ const tip = tipRef.current;
265
+ const root = interaction.rootRef.current;
266
+ if (!tip || !root || !pos) return;
267
+ const half = tip.offsetWidth / 2;
268
+ const w = root.clientWidth;
269
+ setShift(pos.x - half < 0 ? half - pos.x : pos.x + half > w ? w - half - pos.x : 0);
270
+ }, [
271
+ pos,
272
+ desc?.title,
273
+ desc?.subtitle,
274
+ interaction.rootRef
275
+ ]);
276
+ const showHeader = props.showTotal !== false || headerEnd;
277
+ return /* @__PURE__ */ jsxs("div", {
278
+ ref: interaction.rootRef,
279
+ className: [
280
+ "cg-root",
281
+ `cg-${variant}`,
282
+ props.className
283
+ ].filter(Boolean).join(" "),
284
+ style: {
285
+ ...themeVars(theme),
286
+ ...props.style
287
+ },
288
+ "data-mode": props.colorMode ?? "auto",
289
+ "data-animate": props.animate ? "" : void 0,
290
+ "data-glow": glow ? "" : void 0,
291
+ "data-surface": surface ? "" : void 0,
292
+ "data-has-active": active != null ? "" : void 0,
293
+ role: "group",
294
+ "aria-label": props["aria-label"] ?? `${summary}. Use arrow keys to explore.`,
295
+ ...interaction.handlers,
296
+ children: [
297
+ showHeader && /* @__PURE__ */ jsxs("div", {
298
+ className: "cg-header",
299
+ children: [props.showTotal !== false ? /* @__PURE__ */ jsx("span", {
300
+ className: "cg-title",
301
+ children: props.title ?? title
302
+ }) : /* @__PURE__ */ jsx("span", {}), headerEnd]
303
+ }),
304
+ /* @__PURE__ */ jsx("div", {
305
+ className: "cg-body",
306
+ children
307
+ }),
308
+ (props.showLegend !== false || footerStart) && /* @__PURE__ */ jsxs("div", {
309
+ className: "cg-footer",
310
+ children: [/* @__PURE__ */ jsx("span", { children: footerStart }), props.showLegend !== false && /* @__PURE__ */ jsx(Legend, {
311
+ labels: props.legendLabels,
312
+ swatch: legendSwatch
313
+ })]
314
+ }),
315
+ desc && pos && /* @__PURE__ */ jsxs("div", {
316
+ ref: tipRef,
317
+ className: "cg-tooltip",
318
+ style: {
319
+ left: pos.x + shift,
320
+ top: pos.y,
321
+ "--cg-arrow": `${-shift}px`
322
+ },
323
+ "aria-hidden": "true",
324
+ children: [/* @__PURE__ */ jsx("strong", { children: desc.title }), desc.subtitle && /* @__PURE__ */ jsxs("span", { children: [" · ", desc.subtitle] })]
325
+ }),
326
+ /* @__PURE__ */ jsx("span", {
327
+ className: "cg-sr-only",
328
+ "aria-live": "polite",
329
+ children: via === "keyboard" && desc ? `${desc.title}, ${desc.subtitle ?? ""}` : ""
330
+ })
331
+ ]
332
+ });
333
+ }
334
+ function summaryOf(calendar, props) {
335
+ return `${formatNumber(calendar.total, props.locale)} ${pluralize(calendar.total, props.unit)} from ${formatDate(calendar.start, props.locale)} to ${formatDate(calendar.end, props.locale)}`;
336
+ }
337
+ // src/variants/GridGraph.tsx
338
+ function useGrid(props, shape) {
339
+ const { cellSize = 11, gap = 3, radius = 2, showWeekdays = true, showMonths = true, fit = "crop" } = props;
340
+ const pitch = cellSize + gap;
341
+ const left = showWeekdays ? 30 : 0;
342
+ const top = showMonths ? 15 : 0;
343
+ const { calendar, periodTotal, interaction } = useCalendarGraph(props, { fit: {
344
+ pitch,
345
+ reserved: left - gap,
346
+ enabled: fit === "crop"
347
+ } });
348
+ const glow = props.glow ?? false;
349
+ const filterId = useSvgId("cg-glow");
350
+ const weekdays = useMemo(() => weekdayLabels(props.locale, props.weekStart ?? 0), [props.locale, props.weekStart]);
351
+ const width = left + calendar.weeks.length * pitch - gap;
352
+ const height = top + 7 * pitch - gap;
353
+ const svg = useMemo(() => /* @__PURE__ */ jsxs("svg", {
354
+ width: fit === "scale" ? "100%" : width,
355
+ height: fit === "scale" ? void 0 : height,
356
+ viewBox: `0 0 ${width} ${height}`,
357
+ "aria-hidden": "true",
358
+ children: [
359
+ glow && /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("filter", {
360
+ id: filterId,
361
+ x: "-50%",
362
+ y: "-50%",
363
+ width: "200%",
364
+ height: "200%",
365
+ children: [/* @__PURE__ */ jsx("feGaussianBlur", {
366
+ stdDeviation: cellSize / 4,
367
+ result: "b"
368
+ }), /* @__PURE__ */ jsxs("feMerge", { children: [/* @__PURE__ */ jsx("feMergeNode", { in: "b" }), /* @__PURE__ */ jsx("feMergeNode", { in: "SourceGraphic" })] })]
369
+ }) }),
370
+ showMonths && calendar.months.map((m) => /* @__PURE__ */ jsx("text", {
371
+ className: "cg-label",
372
+ x: left + m.weekIndex * pitch,
373
+ y: 9,
374
+ children: m.label
375
+ }, `${m.year}-${m.month}`)),
376
+ showWeekdays && [
377
+ 1,
378
+ 3,
379
+ 5
380
+ ].map((r) => /* @__PURE__ */ jsx("text", {
381
+ className: "cg-label",
382
+ x: 0,
383
+ y: top + r * pitch + cellSize - 2,
384
+ children: weekdays[r]
385
+ }, r)),
386
+ /* @__PURE__ */ jsx("g", { children: calendar.days.map((d) => {
387
+ const { kind, attrs } = shape.cell({
388
+ x: left + d.weekIndex * pitch,
389
+ y: top + d.dayIndex * pitch,
390
+ size: cellSize,
391
+ level: d.level,
392
+ radius
393
+ });
394
+ return /* @__PURE__ */ jsx(kind, {
395
+ ...attrs,
396
+ className: `cg-cell${glow && d.level >= 3 ? " cg-glow" : ""}`,
397
+ "data-cg-i": d.index,
398
+ "data-level": d.level,
399
+ fill: `var(--cg-l${d.level})`,
400
+ style: cellDelay(d.weekIndex)
401
+ }, d.date);
402
+ }) })
403
+ ]
404
+ }), [
405
+ calendar,
406
+ width,
407
+ height,
408
+ fit,
409
+ glow,
410
+ filterId,
411
+ cellSize,
412
+ showMonths,
413
+ showWeekdays,
414
+ left,
415
+ top,
416
+ pitch,
417
+ radius,
418
+ weekdays,
419
+ shape
420
+ ]);
421
+ return /* @__PURE__ */ jsx(GraphShell, {
422
+ props,
423
+ interaction,
424
+ variant: shape.variant,
425
+ describe: (i) => describeDay(calendar.days[i], props),
426
+ summary: summaryOf(calendar, props),
427
+ title: defaultTitle(periodTotal, props),
428
+ glow,
429
+ legendSwatch: shape.variant === "dots" ? (l) => /* @__PURE__ */ jsx("span", {
430
+ className: "cg-swatch",
431
+ style: {
432
+ background: `var(--cg-l${l})`,
433
+ borderRadius: "50%",
434
+ transform: `scale(${.55 + l * .11})`
435
+ }
436
+ }) : void 0,
437
+ children: /* @__PURE__ */ jsx("div", {
438
+ style: glow ? { "--cg-glow-filter": `url(#${filterId})` } : void 0,
439
+ children: svg
440
+ })
441
+ });
442
+ }
443
+ const classicShape = {
444
+ variant: "classic",
445
+ cell: ({ x, y, size, radius }) => ({
446
+ kind: "rect",
447
+ attrs: {
448
+ x,
449
+ y,
450
+ width: size,
451
+ height: size,
452
+ rx: radius,
453
+ ry: radius
454
+ }
455
+ })
456
+ };
457
+ const dotsShape = {
458
+ variant: "dots",
459
+ cell: ({ x, y, size, level }) => ({
460
+ kind: "circle",
461
+ attrs: {
462
+ cx: x + size / 2,
463
+ cy: y + size / 2,
464
+ r: size / 2 * (level === 0 ? .42 : .55 + level * .115)
465
+ }
466
+ })
467
+ };
468
+ /** The classic weeks × days grid. */
469
+ function ClassicGraph(props) {
470
+ return useGrid(props, classicShape);
471
+ }
472
+ /** Circles whose size also encodes intensity, so it still reads in monochrome. */
473
+ function DotsGraph(props) {
474
+ return useGrid(props, dotsShape);
475
+ }
476
+ // src/isometric.ts
477
+ const r = (v) => Math.round(v * 100) / 100;
478
+ /**
479
+ * Geometría de la vista isométrica, compartida por `IsometricGraph` y por
480
+ * `renderIsometricSvg`: así el componente y el SVG estático se ven igual.
481
+ * Las baldosas vienen ordenadas de atrás hacia adelante (orden de pintado).
482
+ */
483
+ function isometricLayout(calendar, { tileSize = 12, maxHeight = 48, heightBy = "count" } = {}) {
484
+ const tw = tileSize;
485
+ const th = tileSize / 2;
486
+ const W = calendar.weeks.length;
487
+ const width = (W + 7) * (tw / 2);
488
+ const height = (W + 7) * (th / 2) + maxHeight + 2;
489
+ const ox = 7 * (tw / 2);
490
+ const max = Math.max(1, calendar.max);
491
+ return {
492
+ width,
493
+ height,
494
+ tiles: [...calendar.days].sort((a, b) => a.weekIndex + a.dayIndex - (b.weekIndex + b.dayIndex) || a.weekIndex - b.weekIndex).map((day) => {
495
+ const X = ox + (day.weekIndex - day.dayIndex) * (tw / 2);
496
+ const Y = maxHeight + (day.weekIndex + day.dayIndex) * (th / 2);
497
+ const h = day.count === 0 ? 0 : heightBy === "level" ? day.level / 4 * maxHeight : Math.max(3, day.count / max * maxHeight);
498
+ const p = (x, y) => `${r(x)},${r(y)}`;
499
+ return {
500
+ day,
501
+ height: h,
502
+ top: [
503
+ p(X, Y - h),
504
+ p(X + tw / 2, Y + th / 2 - h),
505
+ p(X, Y + th - h),
506
+ p(X - tw / 2, Y + th / 2 - h)
507
+ ].join(" "),
508
+ left: [
509
+ p(X - tw / 2, Y + th / 2 - h),
510
+ p(X, Y + th - h),
511
+ p(X, Y + th),
512
+ p(X - tw / 2, Y + th / 2)
513
+ ].join(" "),
514
+ right: [
515
+ p(X, Y + th - h),
516
+ p(X + tw / 2, Y + th / 2 - h),
517
+ p(X + tw / 2, Y + th / 2),
518
+ p(X, Y + th)
519
+ ].join(" ")
520
+ };
521
+ })
522
+ };
523
+ }
524
+ /** Porcentaje de color de la cara izquierda y derecha (el resto se mezcla con negro). */
525
+ const ISO_SHADES = {
526
+ left: 78,
527
+ right: 60
528
+ };
529
+ // src/variants/IsometricGraph.tsx
530
+ const shade = (level, pct) => `color-mix(in oklab, var(--cg-l${level}) ${pct}%, var(--cg-mix))`;
531
+ /** Isometric columns whose height shows each day's activity. */
532
+ function IsometricGraph(props) {
533
+ const { tileSize = 12, maxHeight = 48, heightBy = "count", showStats = true, statLabels = [
534
+ "longest streak",
535
+ "current streak",
536
+ "best day"
537
+ ] } = props;
538
+ const { calendar, periodTotal, interaction } = useCalendarGraph(props);
539
+ const stats = useMemo(() => computeStats(calendar.days), [calendar]);
540
+ const svg = useMemo(() => {
541
+ const { width, height, tiles } = isometricLayout(calendar, {
542
+ tileSize,
543
+ maxHeight,
544
+ heightBy
545
+ });
546
+ return /* @__PURE__ */ jsx("svg", {
547
+ width,
548
+ height,
549
+ viewBox: `0 0 ${width} ${height}`,
550
+ "aria-hidden": "true",
551
+ children: tiles.map(({ day: d, height: h, top, left, right }) => /* @__PURE__ */ jsxs("g", {
552
+ className: "cg-cell cg-rise",
553
+ "data-cg-i": d.index,
554
+ style: cellDelay(d.weekIndex),
555
+ children: [
556
+ h > 0 && /* @__PURE__ */ jsx("polygon", {
557
+ points: left,
558
+ style: { fill: shade(d.level, ISO_SHADES.left) }
559
+ }),
560
+ h > 0 && /* @__PURE__ */ jsx("polygon", {
561
+ points: right,
562
+ style: { fill: shade(d.level, ISO_SHADES.right) }
563
+ }),
564
+ /* @__PURE__ */ jsx("polygon", {
565
+ points: top,
566
+ style: { fill: `var(--cg-l${d.level})` }
567
+ })
568
+ ]
569
+ }, d.date))
570
+ });
571
+ }, [
572
+ calendar,
573
+ tileSize,
574
+ maxHeight,
575
+ heightBy
576
+ ]);
577
+ const n = (v) => formatNumber(v, props.locale);
578
+ return /* @__PURE__ */ jsx(GraphShell, {
579
+ props,
580
+ interaction,
581
+ variant: "isometric",
582
+ describe: (i) => describeDay(calendar.days[i], props),
583
+ summary: summaryOf(calendar, props),
584
+ title: defaultTitle(periodTotal, props),
585
+ headerEnd: showStats && /* @__PURE__ */ jsxs("span", {
586
+ className: "cg-stats",
587
+ children: [
588
+ /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("b", { children: n(stats.longestStreak) }), statLabels[0]] }),
589
+ /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("b", { children: n(stats.currentStreak) }), statLabels[1]] }),
590
+ /* @__PURE__ */ jsxs("span", { children: [
591
+ /* @__PURE__ */ jsx("b", { children: n(stats.best?.count ?? 0) }),
592
+ statLabels[2],
593
+ stats.best && ` · ${formatDate(stats.best.date, props.locale, {
594
+ month: "short",
595
+ day: "numeric"
596
+ })}`
597
+ ] })
598
+ ]
599
+ }),
600
+ children: svg
601
+ });
602
+ }
603
+ // src/variants/RadialGraph.tsx
604
+ function sector(cx, cy, r0, r1, a0, a1) {
605
+ const p = (r, a) => `${(cx + r * Math.cos(a)).toFixed(2)} ${(cy + r * Math.sin(a)).toFixed(2)}`;
606
+ return `M${p(r1, a0)}A${r1} ${r1} 0 0 1 ${p(r1, a1)}L${p(r0, a1)}A${r0} ${r0} 0 0 0 ${p(r0, a0)}Z`;
607
+ }
608
+ /** The year as a ring: each week is a slice and each weekday a band. */
609
+ function RadialGraph(props) {
610
+ const { size = 320, innerRadius = .38, showMonths = true } = props;
611
+ const { calendar, interaction } = useCalendarGraph(props);
612
+ const pad = showMonths ? 22 : 4;
613
+ const R = size / 2;
614
+ const outer = R - pad;
615
+ const inner = outer * innerRadius;
616
+ const ring = (outer - inner) / 7;
617
+ const W = calendar.weeks.length;
618
+ const slice = Math.PI * 2 / W;
619
+ const start = -Math.PI / 2;
620
+ const gapA = Math.min(.012, slice * .12);
621
+ const svg = useMemo(() => /* @__PURE__ */ jsxs("svg", {
622
+ width: size,
623
+ height: size,
624
+ viewBox: `0 0 ${size} ${size}`,
625
+ "aria-hidden": "true",
626
+ children: [
627
+ calendar.days.map((d) => {
628
+ const a0 = start + d.weekIndex * slice + gapA;
629
+ const a1 = start + (d.weekIndex + 1) * slice - gapA;
630
+ const r0 = inner + d.dayIndex * ring + .6;
631
+ const r1 = inner + (d.dayIndex + 1) * ring - .6;
632
+ return /* @__PURE__ */ jsx("path", {
633
+ d: sector(R, R, r0, r1, a0, a1),
634
+ className: "cg-cell",
635
+ "data-cg-i": d.index,
636
+ fill: `var(--cg-l${d.level})`,
637
+ style: cellDelay(d.weekIndex)
638
+ }, d.date);
639
+ }),
640
+ showMonths && calendar.months.map((m) => {
641
+ const a = start + (m.weekIndex + .5) * slice;
642
+ return /* @__PURE__ */ jsx("text", {
643
+ className: "cg-label",
644
+ x: R + (outer + 11) * Math.cos(a),
645
+ y: R + (outer + 11) * Math.sin(a),
646
+ textAnchor: "middle",
647
+ dominantBaseline: "middle",
648
+ children: m.label
649
+ }, `${m.year}-${m.month}`);
650
+ }),
651
+ /* @__PURE__ */ jsx("text", {
652
+ x: R,
653
+ y: R - 4,
654
+ textAnchor: "middle",
655
+ style: {
656
+ fill: "var(--cg-text)",
657
+ fontSize: size / 11,
658
+ fontWeight: 650
659
+ },
660
+ children: formatNumber(calendar.total, props.locale)
661
+ }),
662
+ /* @__PURE__ */ jsx("text", {
663
+ x: R,
664
+ y: R + size / 18,
665
+ textAnchor: "middle",
666
+ className: "cg-label",
667
+ style: { fontSize: 11 },
668
+ children: pluralize(calendar.total, props.unit)
669
+ })
670
+ ]
671
+ }), [
672
+ calendar,
673
+ size,
674
+ R,
675
+ inner,
676
+ ring,
677
+ outer,
678
+ slice,
679
+ start,
680
+ gapA,
681
+ showMonths,
682
+ props.locale,
683
+ props.unit
684
+ ]);
685
+ return /* @__PURE__ */ jsx(GraphShell, {
686
+ props: {
687
+ ...props,
688
+ showTotal: props.showTotal ?? false
689
+ },
690
+ interaction,
691
+ variant: "radial",
692
+ describe: (i) => describeDay(calendar.days[i], props),
693
+ summary: summaryOf(calendar, props),
694
+ children: /* @__PURE__ */ jsx("div", {
695
+ style: {
696
+ display: "flex",
697
+ justifyContent: "center"
698
+ },
699
+ children: svg
700
+ })
701
+ });
702
+ }
703
+ // src/variants/SkylineGraph.tsx
704
+ /** One building per week: your year as a city skyline. */
705
+ function SkylineGraph(props) {
706
+ const { height = 110, barWidth = 9, gap = 3, windows = true, fit = "crop" } = props;
707
+ const pitch = barWidth + gap;
708
+ const rootRef = useRef(null);
709
+ const weeksN = useFitWeeks(rootRef, props.weeks ?? 53, pitch, -gap, fit === "crop");
710
+ const endKey = props.endDate instanceof Date ? props.endDate.getTime() : props.endDate;
711
+ const requested = props.weeks ?? 53;
712
+ const full = useMemo(() => buildCalendar(props.data, {
713
+ weeks: requested,
714
+ endDate: props.endDate,
715
+ weekStart: props.weekStart,
716
+ locale: props.locale
717
+ }), [
718
+ props.data,
719
+ requested,
720
+ endKey,
721
+ props.weekStart,
722
+ props.locale
723
+ ]);
724
+ const calendar = useMemo(() => cropCalendar(full, weeksN, props.locale), [
725
+ full,
726
+ weeksN,
727
+ props.locale
728
+ ]);
729
+ const allWeeks = useMemo(() => {
730
+ const raw = full.weeks.map((w, index) => {
731
+ const days = w.filter((d) => d !== null);
732
+ return {
733
+ index,
734
+ days,
735
+ total: days.reduce((s, d) => s + d.count, 0),
736
+ activeDays: days.filter((d) => d.count > 0).length,
737
+ level: 0
738
+ };
739
+ });
740
+ const levelOf = makeLevelFn(raw.map((w) => w.total), props.scale);
741
+ raw.forEach((w) => w.level = levelOf(w.total));
742
+ return raw;
743
+ }, [full, typeof props.scale === "string" ? props.scale : props.scale?.join(",")]);
744
+ const maxWeek = Math.max(1, ...allWeeks.map((w) => w.total));
745
+ const weeks = useMemo(() => allWeeks.slice(allWeeks.length - calendar.weeks.length).map((w, index) => ({
746
+ ...w,
747
+ index,
748
+ days: calendar.weeks[index].filter((d) => d !== null)
749
+ })), [allWeeks, calendar]);
750
+ const { onWeekClick } = props;
751
+ const onActivate = useCallback((i) => onWeekClick?.(weeks[i]), [onWeekClick, weeks]);
752
+ const interaction = useGraphInteraction({
753
+ rootRef,
754
+ count: weeks.length,
755
+ stepX: 1,
756
+ stepY: 0,
757
+ onActivate
758
+ });
759
+ const labelH = 14;
760
+ const minH = windows ? 30 : 3;
761
+ const width = weeks.length * pitch - gap;
762
+ const svgH = height + labelH;
763
+ const svg = useMemo(() => /* @__PURE__ */ jsxs("svg", {
764
+ width: fit === "scale" ? "100%" : width,
765
+ height: fit === "scale" ? void 0 : svgH,
766
+ viewBox: `0 0 ${width} ${svgH}`,
767
+ "aria-hidden": "true",
768
+ children: [
769
+ /* @__PURE__ */ jsx("line", {
770
+ x1: 0,
771
+ x2: width,
772
+ y1: height + .5,
773
+ y2: height + .5,
774
+ stroke: "var(--cg-border)"
775
+ }),
776
+ weeks.map((w) => {
777
+ const h = w.total === 0 ? minH * .8 : minH + w.total / maxWeek * (height - minH);
778
+ const x = w.index * pitch;
779
+ const y = height - h;
780
+ const ww = Math.max(2, barWidth - 4);
781
+ return /* @__PURE__ */ jsxs("g", {
782
+ className: "cg-cell cg-rise",
783
+ "data-cg-i": w.index,
784
+ style: cellDelay(w.index),
785
+ children: [/* @__PURE__ */ jsx("rect", {
786
+ x,
787
+ y,
788
+ width: barWidth,
789
+ height: h,
790
+ rx: 1.5,
791
+ fill: `var(--cg-l${w.level})`
792
+ }), windows && w.days.map((d) => /* @__PURE__ */ jsx("rect", {
793
+ x: x + (barWidth - ww) / 2,
794
+ y: y + 3 + d.dayIndex * 3.6,
795
+ width: ww,
796
+ height: 2.2,
797
+ rx: .5,
798
+ fill: d.count > 0 ? "#fff6c8" : "currentColor",
799
+ opacity: d.count > 0 ? .92 : .12,
800
+ style: { color: "var(--cg-mix)" }
801
+ }, d.date))]
802
+ }, w.index);
803
+ }),
804
+ calendar.months.map((m) => /* @__PURE__ */ jsx("text", {
805
+ className: "cg-label",
806
+ x: m.weekIndex * pitch,
807
+ y: svgH - 2,
808
+ children: m.label
809
+ }, `${m.year}-${m.month}`))
810
+ ]
811
+ }), [
812
+ weeks,
813
+ calendar.months,
814
+ width,
815
+ svgH,
816
+ height,
817
+ pitch,
818
+ barWidth,
819
+ maxWeek,
820
+ minH,
821
+ windows,
822
+ fit
823
+ ]);
824
+ const describe = (i) => {
825
+ const w = weeks[i];
826
+ return {
827
+ title: `${formatNumber(w.total, props.locale)} ${pluralize(w.total, props.unit)}`,
828
+ subtitle: w.days[0] ? `${formatDate(w.days[0].date, props.locale, {
829
+ month: "short",
830
+ day: "numeric",
831
+ year: "numeric"
832
+ })} · ${w.activeDays}/7` : void 0
833
+ };
834
+ };
835
+ return /* @__PURE__ */ jsx(GraphShell, {
836
+ props,
837
+ interaction,
838
+ variant: "skyline",
839
+ describe,
840
+ summary: summaryOf(calendar, props),
841
+ title: defaultTitle(full.total, props),
842
+ children: svg
843
+ });
844
+ }
845
+ // src/variants/TerminalGraph.tsx
846
+ const terminalCharsets = {
847
+ blocks: [
848
+ "··",
849
+ "░░",
850
+ "▒▒",
851
+ "▓▓",
852
+ "██"
853
+ ],
854
+ squares: [
855
+ "□ ",
856
+ "▪ ",
857
+ "■ ",
858
+ "■ ",
859
+ "■ "
860
+ ],
861
+ ascii: [
862
+ ". ",
863
+ ": ",
864
+ "+ ",
865
+ "# ",
866
+ "@ "
867
+ ],
868
+ dots: [
869
+ "· ",
870
+ "• ",
871
+ "• ",
872
+ "● ",
873
+ "● "
874
+ ]
875
+ };
876
+ const CHAR_W = 7.22;
877
+ const MIN_FONT = 6;
878
+ /** Real monospace text, for portfolios with a terminal look. */
879
+ function TerminalGraph(props) {
880
+ const { charset = "blocks", command = "git log --since='1 year ago' | heatmap", prompt = "~ $", colored = true, fit = "crop" } = props;
881
+ const chars = terminalCharsets[charset];
882
+ const { calendar, periodTotal, interaction } = useCalendarGraph(props, { fit: (() => {
883
+ const k = fit === "scale" ? MIN_FONT / 12 : 1;
884
+ return {
885
+ pitch: CHAR_W * 2 * k,
886
+ reserved: CHAR_W * 5 * k + 34,
887
+ enabled: true
888
+ };
889
+ })() });
890
+ const weekdays = useMemo(() => weekdayLabels(props.locale, props.weekStart ?? 0), [props.locale, props.weekStart]);
891
+ const [fontSize, setFontSize] = useState(12);
892
+ const { rootRef } = interaction;
893
+ const cols = 5 + calendar.weeks.length * 2;
894
+ useIsomorphicLayoutEffect(() => {
895
+ const el = rootRef.current;
896
+ if (fit !== "scale" || !el || typeof ResizeObserver === "undefined") {
897
+ setFontSize(12);
898
+ return;
899
+ }
900
+ const update = () => {
901
+ const avail = el.clientWidth - 34;
902
+ if (avail > 0) setFontSize(Math.max(MIN_FONT, Math.min(12, avail / cols * (12 / CHAR_W))));
903
+ };
904
+ update();
905
+ const ro = new ResizeObserver(update);
906
+ ro.observe(el);
907
+ return () => ro.disconnect();
908
+ }, [
909
+ fit,
910
+ cols,
911
+ rootRef
912
+ ]);
913
+ const color = (l) => colored ? `var(--cg-l${l === 0 ? 2 : l})` : "var(--cg-text)";
914
+ const op = (l) => l === 0 ? .35 : colored ? 1 : .4 + l * .15;
915
+ const body = useMemo(() => {
916
+ const cols = calendar.weeks.length * 2;
917
+ const header = Array.from({ length: cols }, () => " ");
918
+ for (const m of calendar.months) {
919
+ const lbl = m.label.replace(".", "").slice(0, 3);
920
+ for (let i = 0; i < lbl.length && m.weekIndex * 2 + i < cols; i++) header[m.weekIndex * 2 + i] = lbl[i];
921
+ }
922
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
923
+ " ",
924
+ header.join(""),
925
+ "\n",
926
+ Array.from({ length: 7 }, (_, r) => /* @__PURE__ */ jsxs(Fragment, { children: [
927
+ (r % 2 === 1 ? weekdays[r].replace(".", "").slice(0, 3) : "").padEnd(5),
928
+ calendar.weeks.map((w, wi) => {
929
+ const d = w[r];
930
+ if (!d) return /* @__PURE__ */ jsx(Fragment, { children: " " }, wi);
931
+ return /* @__PURE__ */ jsx("span", {
932
+ className: "cg-cell",
933
+ "data-cg-i": d.index,
934
+ style: {
935
+ ...cellDelay(wi),
936
+ color: color(d.level),
937
+ opacity: op(d.level)
938
+ },
939
+ children: chars[d.level]
940
+ }, d.date);
941
+ }),
942
+ "\n"
943
+ ] }, r))
944
+ ] });
945
+ }, [
946
+ calendar,
947
+ weekdays,
948
+ chars,
949
+ colored
950
+ ]);
951
+ const [less, more] = props.legendLabels ?? ["less", "more"];
952
+ return /* @__PURE__ */ jsx(GraphShell, {
953
+ props: {
954
+ ...props,
955
+ showTotal: false,
956
+ showLegend: false
957
+ },
958
+ interaction,
959
+ variant: "terminal",
960
+ surface: true,
961
+ describe: (i) => describeDay(calendar.days[i], props),
962
+ summary: summaryOf(calendar, props),
963
+ children: /* @__PURE__ */ jsxs("pre", {
964
+ className: "cg-term",
965
+ style: fontSize !== 12 ? { fontSize } : void 0,
966
+ children: [
967
+ /* @__PURE__ */ jsxs("span", {
968
+ className: "cg-prompt cg-wrap",
969
+ children: [
970
+ /* @__PURE__ */ jsx("b", { children: prompt }),
971
+ " ",
972
+ command
973
+ ]
974
+ }),
975
+ "\n\n",
976
+ body,
977
+ "\n",
978
+ (props.showTotal !== false || props.showLegend !== false) && /* @__PURE__ */ jsxs("span", {
979
+ className: "cg-foot",
980
+ children: [props.showTotal !== false && /* @__PURE__ */ jsx("span", {
981
+ className: "cg-prompt",
982
+ children: props.title ?? `${formatNumber(periodTotal, props.locale)} ${pluralize(periodTotal, props.unit)} ${periodText(props)}`
983
+ }), props.showLegend !== false && /* @__PURE__ */ jsxs("span", { children: [
984
+ less,
985
+ " ",
986
+ [
987
+ 0,
988
+ 1,
989
+ 2,
990
+ 3,
991
+ 4
992
+ ].map((l) => /* @__PURE__ */ jsx("span", {
993
+ style: {
994
+ color: color(l),
995
+ opacity: op(l)
996
+ },
997
+ children: chars[l]
998
+ }, l)),
999
+ more
1000
+ ] })]
1001
+ }),
1002
+ /* @__PURE__ */ jsxs("span", {
1003
+ className: "cg-prompt",
1004
+ children: [
1005
+ /* @__PURE__ */ jsx("b", { children: prompt }),
1006
+ " ",
1007
+ /* @__PURE__ */ jsx("span", { className: "cg-caret" })
1008
+ ]
1009
+ })
1010
+ ]
1011
+ })
1012
+ });
1013
+ }
1014
+ // src/variants/CompactGraph.tsx
1015
+ /** A single row with the last N days, for headers, cards and footers. */
1016
+ function CompactGraph(props) {
1017
+ const { days: n = 30, cellSize = 10, gap = 3, radius = 2, showStreak = true } = props;
1018
+ const rootRef = useRef(null);
1019
+ const endKey = props.endDate instanceof Date ? props.endDate.getTime() : props.endDate;
1020
+ const scaleKey = typeof props.scale === "string" ? props.scale : props.scale?.join(",");
1021
+ const days = useMemo(() => {
1022
+ const slice = buildCalendar(props.data, {
1023
+ weeks: Math.ceil(n / 7) + 1,
1024
+ endDate: props.endDate
1025
+ }).days.slice(-n);
1026
+ const levelOf = makeLevelFn(slice.map((d) => d.count), props.scale);
1027
+ return slice.map((d, i) => ({
1028
+ ...d,
1029
+ index: i,
1030
+ weekIndex: i,
1031
+ level: levelOf(d.count)
1032
+ }));
1033
+ }, [
1034
+ props.data,
1035
+ n,
1036
+ endKey,
1037
+ scaleKey
1038
+ ]);
1039
+ const stats = useMemo(() => computeStats(days), [days]);
1040
+ const { onDayClick } = props;
1041
+ const onActivate = useCallback((i) => onDayClick?.(days[i]), [onDayClick, days]);
1042
+ const interaction = useGraphInteraction({
1043
+ rootRef,
1044
+ count: days.length,
1045
+ stepX: 1,
1046
+ stepY: 0,
1047
+ onActivate
1048
+ });
1049
+ const pitch = cellSize + gap;
1050
+ const width = days.length * pitch - gap;
1051
+ const svg = useMemo(() => /* @__PURE__ */ jsx("svg", {
1052
+ width,
1053
+ height: cellSize,
1054
+ viewBox: `0 0 ${width} ${cellSize}`,
1055
+ "aria-hidden": "true",
1056
+ children: days.map((d) => /* @__PURE__ */ jsx("rect", {
1057
+ className: "cg-cell",
1058
+ "data-cg-i": d.index,
1059
+ x: d.index * pitch,
1060
+ y: 0,
1061
+ width: cellSize,
1062
+ height: cellSize,
1063
+ rx: radius,
1064
+ fill: `var(--cg-l${d.level})`,
1065
+ style: cellDelay(d.index / 3)
1066
+ }, d.date))
1067
+ }), [
1068
+ days,
1069
+ width,
1070
+ cellSize,
1071
+ pitch,
1072
+ radius
1073
+ ]);
1074
+ const streak = props.streakLabel ?? ((s) => `${s} day streak`);
1075
+ return /* @__PURE__ */ jsx(GraphShell, {
1076
+ props: {
1077
+ ...props,
1078
+ showLegend: props.showLegend ?? false
1079
+ },
1080
+ interaction,
1081
+ variant: "compact",
1082
+ describe: (i) => describeDay(days[i], props),
1083
+ summary: `${stats.total} ${pluralize(stats.total, props.unit)} in the last ${n} days`,
1084
+ title: /* @__PURE__ */ jsxs(Fragment$1, { children: [
1085
+ /* @__PURE__ */ jsx("strong", { children: formatNumber(stats.total, props.locale) }),
1086
+ " ",
1087
+ pluralize(stats.total, props.unit),
1088
+ " ",
1089
+ (props.rangeLabel ?? ((d) => `in the last ${d} days`))(n)
1090
+ ] }),
1091
+ headerEnd: showStreak && stats.currentStreak > 0 && /* @__PURE__ */ jsx("span", {
1092
+ style: { color: "var(--cg-muted)" },
1093
+ children: streak(stats.currentStreak)
1094
+ }),
1095
+ children: svg
1096
+ });
1097
+ }
1098
+ export { ClassicGraph, CompactGraph, DotsGraph, GITHUB_CONTRIBUTIONS_QUERY, IsometricGraph, RadialGraph, SkylineGraph, TerminalGraph, buildCalendar, computeStats, cropCalendar, fetchGitHubContributions, fetchGitLabContributions, formatDate, fromDates, fromGitHub, fromGitLab, generateSampleData, makeLevelFn, mergeSources, resolveTheme, sampleProfiles, terminalCharsets, themeNames, themeVars, themes };