14 Commits
Author SHA1 Message Date
tiennm99 3fc8a59de6 chore: rename the project to ghglance
The Marketplace rejects ghstats as an action name, so the repo, Go module,
binary, and action share the name ghglance instead. The demo workflow keeps
reading the existing GHSTATS_TOKEN repo secret.
2026-10-03 13:38:15 +07:00
tiennm99 92387eddf5 feat(card): configurable start of week (#25)
* feat(card): configurable start of week for heatmap + weekday cards

New -start-of-week CLI flag (and start_of_week action input) rotates the
contribution-heatmap rows and the productive-weekday bars so users whose
calendars start on Monday (or any other day) get matching output. Default
stays Sunday to preserve existing renders.

* docs: note start-of-week in design-guidelines, codebase-summary, deployment-guide

- design-guidelines: heatmap row order + productive-weekday bar order now derive from Profile.WeekStart
- codebase-summary: list new weekday_start_test.go cases + TestParseWeekday
- deployment-guide: mention start_of_week as an optional action input in the workflow template
2026-04-21 20:57:21 +07:00
tiennm99 0e0e4fc500 fix(card): productive titles render at 15 px for every timezone (#24)
Two cuts together keep the productive-time and productive-weekday
titles at the same 15 px the rest of the gallery uses:

1. utcOffsetLabel is now compact. Integer offsets drop the '.00'
   padding ("UTC+7" vs the old "UTC+7.00") and non-integer offsets use
   the colon form ("UTC+5:30", "UTC+5:45"). Saves 3 chars for the
   common integer case, so "Commits by Hour (last year, UTC+7)" lands
   at 34 chars — inside the 15 px budget.

2. weekdayTitle no longer embeds the UTC label. Day-of-week aggregates
   by whole days; clock precision isn't informative there, and
   dropping it shortens the title to 30 chars so the full 15 px lands
   for every timezone.

Quarter-hour-zone users (Kathmandu UTC+5:45) see the hour title drop
to 14 px — 37 chars still exceeds the budget — but that's a rare
case and only 1 px off.

TestUTCOffsetLabel updated to the new format. TestFitTitleFontSize
pinned the new titles.
2026-04-19 11:49:40 +07:00
tiennm99 70bcbd43f2 feat(card): stack heatmap into two halves; unify font-size vocabulary (#23)
## Heatmap — two halves, 8x8 cells

The single-row 53-week layout could never hold square cells larger than
4x4 inside a 340 px card — cramped. Split the year in half (ceil(weeks/2)
on top, floor on bottom) and each half is ~27 weeks wide, freeing the
cells to be 8x8 (4x the area) while keeping comfortable left (30 px)
and right (~67 px) gutters. Grid:

  topPadA = 45, half = 7*9 - 1 = 62 tall
  halfGap = 13
  topPadB = 120, same 62 tall
  grid bottom at y = 182, 18 px frame margin

Year still reads top-to-bottom left-to-right, just with one extra line
break at the midpoint. Dropped the separate Less/More legend — at 8x8
the intensity gradient is self-explanatory, and the removal buys the
vertical space the new layout needs.

Refactored into a helper `renderHeatmapHalf` so the two halves share a
single code path (labels + month markers + cells).

## Font-size vocabulary

Four named constants in svg.go: fontBody=12, fontLabel=11, fontAxis=10,
fontBigNum=28. Every card's existing literals already sit on this
ladder except the heatmap, which was using 9 for weekday/month labels.
Heatmap now routes through fontAxis so the whole gallery shares one
size scale.

Other cards' literals weren't rewritten to reference the constants
(pure churn for no behavior change); the constants give future cards
the vocabulary and the design-guidelines the schema.
2026-04-19 11:39:50 +07:00
tiennm99 d59ae71116 fix(card): heatmap cells back to 4x4 squares with side gutters (#22)
The 4x12 rectangular stretch from v1.2.3 read as "weird". Requirement
is square cells + comfortable side padding. Given the 53-week hard
constraint, those two requirements together pick the cell size:

  53 * (size + gap) + leftPad + rightPad = 340

Candidates audited:
  6 x 6, gap 1 → 371 wide, overflows the frame
  5 x 5, gap 1 → 318 wide, only 11 px of total side padding ("very close")
  5 x 5, gap 0 → 265 wide, cells touch (need a stroke to fake a gap)
  4 x 4, gap 1 → 265 wide, 30 + 45 px gutters, real 1 px gaps ✓
  3 x 3, gap 2 → 265 wide, cells become pinhead-sized

4 x 4 with a 1 px gap is the largest square that keeps breathing room
on both sides without any rendering trickery. Grid is 35 px tall;
there's leftover vertical space on the card but short beats "stretched
horizontal bands" visually. Bump topPad from 62 → 70 to offset the
grid slightly down from the title and reduce that apparent emptiness.

Legend swatches also revert to the same cellSize so the Less/More
bar matches the grid visually again.
2026-04-19 11:23:48 +07:00
tiennm99 7d586eb461 fix(card): make heatmap cells 3× taller — width was tight, height had room (#21)
Cells were 4 × 4 with 1 px gap: the grid used 35 of 140 available
vertical pixels (25 %) and ~80 px of dead space below. Widening the
cells isn't an option — 53 weeks already consume every horizontal
pixel past the weekday-label gutter. So stretch vertically instead:

  cellW = 4 (unchanged)  cellH = 12  cellGap = 1

Grid footprint is now 265 × 91 px inside a 340 × 200 frame, with a 32
px gap below the grid for the legend. Each weekday reads as a distinct
horizontal band instead of a cramped postage-stamp row.

Legend keeps 8 × 8 SQUARE swatches so the "Less ▢▢▢▢▢ More" row is
still recognisable as an intensity legend rather than a stretched echo
of the data cells. Label baseline offset switches from `cellSize - 1`
to `cellH - 3` so "Mon"/"Wed"/"Fri" sit visually centred in the taller
rows.
2026-04-19 11:14:42 +07:00
tiennm99 1e58d4c13a fix(card): donut legend caps at 7 rows (Other inclusive); add heatmap gutter (#20)
Two follow-ups:

1. "Top 7 languages" counts rows the reader sees, not "7 named + Other".
   Revert collapseOther to the original semantic: when the input has more
   than n entries, keep in[:n-1] named and fold the tail into a single
   "Other" row — 7 rows total with topN=7. Input with ≤7 real languages
   still passes through unchanged. Test and adversarial-profile updated
   to pin the cap (6 named + Other, Shell/Kotlin/Java collapse).

2. Contributions heatmap grid was touching the card right edge
   (leftPad 22 + 53*6 = 340 exactly). Shrink cells from 5×5 to 4×4 and
   bump leftPad to 30, giving 45 px of right gutter. Grid no longer
   reads as bleeding into the frame border; weekday label column also
   gets a few more pixels of breathing room on the left.
2026-04-19 11:00:02 +07:00
tiennm99 3dd5e85806 fix(card): show 7 *named* languages + Other, not 6 + Other (#19)
With topN=7 the previous collapseOther kept only the first 6 entries
and added "Other" as the 7th row. A user expecting to see 7 actual
languages in the legend saw six named languages plus "Other" — the
exact complaint just raised about the profile repo's donut.

Flip the semantic: the "top N" slots are reserved for real languages,
and "Other" is an extra row when (and only when) there's a non-zero
tail past the Nth entry. Topologically that means up to 8 legend
rows — still fits the card frame (row 8 text baseline at y=195, card
height 200).

- TestDonutTopSevenPlusOther pins the new contract with a 9-language
  input.
- adversarialProfile in TestCardsFitFrame bumped to 9 languages so
  the stress test exercises the 8-row legend geometry.
- design-guidelines: the donut row re-reads "Up to 7 named languages,
  plus an 'Other' row when the tail is non-zero (8 rows max)".
2026-04-19 10:46:10 +07:00
tiennm99 88397e80a3 docs: resync after card fixes — title auto-fit, truncate, abbreviated ticks (#15)
Several recent code changes hadn't propagated to the docs:

- design-guidelines
  * Card frame title row: document the 11–15 px auto-shrink (not a flat
    15 px anymore).
  * Donut Top-N: already 7 (updated earlier).
  * Bar-chart section renamed to cover weekday + by-year too; document
    the peak-vs-dim highlight convention and the niceTicks yMax ≥ max
    invariant.
  * Add Heatmap / Stat-column (streak) / List (top-starred) card
    sections — they were missing entirely.
  * Rewrite "Text overflow" from "we don't truncate" to the current
    truth (truncate helper, formatTick abbreviations).
  * Replace dangling `truncateName` reference with `truncate`.

- code-standards
  * SVG output standards: call out truncate, formatTick abbreviation,
    header auto-fit so the card-review gate reflects what the renderers
    actually do.

- codebase-summary
  * Layout tree: svg.go / axis.go comments list the helpers they now
    contain; productive.go notes the weekday histogram.
  * demo/ tree shows the index vs per-theme split.
  * Data-flow diagram includes Weekday in the productive pass.
  * Test coverage row lists TestCardsFitFrame / TestFitTitleFontSize /
    TestNiceTicksCoversMax — the new invariant guards.

- system-architecture
  * Shared primitives list adds renderWeekday and renderHeatmap; donut
    blurb updated to "top 7".
  * New "Chart-geometry invariants" block documents niceTicks ceiling,
    formatTick abbreviation, header auto-fit.

- project-roadmap
  * Phase 7.5 bullet updated to describe the index + per-theme demo
    split (was single-README TOC).
2026-04-19 10:22:03 +07:00
tiennm99 d1034e6883 feat(card): show top 7 languages on donut cards instead of top 5 (#14)
Legend at x=20-155 fits 7 rows (y=55 to y=175); donut at cx=250, cy=110
is unaffected. Update the design-guidelines table to match. Stress test
still passes — the legend column never leaves its left-side gutter.
2026-04-19 10:13:07 +07:00
tiennm99 93880f7ddd fix(card): eliminate frame overflows and add release-gate standard (#8)
Three cards overflowed the 340×200 frame for realistic profile data:

- contributions-heatmap: the classic case — 53 weeks at 9px cellSize + 2px
  gap pushed the grid out to x≈611. Shrink to cellSize=5, cellGap=1 so
  leftPad(22) + 53*6 = 340 (exact fit). Drop month labels within 20 px of
  the right edge so "Dec"/"Apr" can't stick past the frame.
- streak: the third column rendered "N / M" at font-size 28, centered at
  x=282. For 4+ digit totals (e.g. 584 / 3031) the text extended to x≈347.
  Refactor to show the active-days integer by itself in the big slot and
  push "of N total (P%)" into the small detail line that the other two
  columns already use.
- top-starred-repos: the per-row star icon sat at x=306 while the right-
  anchored number ended at x=334, so 5+ digit star counts collided with
  the icon. Drop the icon (card title already says "Top Starred Repos"),
  emit the count as "N ★", right-anchor at x=334 with a 6 px safety gap.

Add a new TestCardsFitFrame stress test that renders every card against an
adversarial profile (10-digit counts, 40-char names, 20 active years,
53-week span) and asserts every positional attribute stays inside the
frame. This is the automated half of the new "fit-the-frame invariant"
added to docs/design-guidelines.md + a pre-release review checklist in
docs/code-standards.md.

Bug reports will still surface text-overflow cases that the coordinate
check can't see (a text-anchor="middle" element has a single x attribute
but renders outward), so the docs also spell out the human-review step:
render dracula against tiny/typical/adversarial fixtures before release.
2026-04-19 09:27:17 +07:00
tiennm99 e5b6daae5a docs: resync with current state across all project docs
- design-guidelines: every dimension was stale — card frame 340x200
  (was 500x220), corner radius 6, title 15px at (20,30), row y0/dy,
  donut centre (250,110) r=55/30, topN=5, legend y0=55 dy=20, bar chart
  area [35,325]x[45,155], area chart [28,312]x[45,150], icon scale 0.75.
- code-standards: FetchContributionsAllTime signature now ctx-first,
  viewbox 500x220 → 340x200.
- codebase-summary: test coverage lists main_test.go +
  TestDonutSingleSlice/Empty; filename convention says plain kebab-case
  (no numeric prefix).
- project-overview-pdr: forks/private defaults now on, not off.
- project-roadmap: add Phase 7 (Marketplace polish — resize, numeric-
  prefix drop, v1 floating tag, rename-rollback). Renumber planned
  phases 8-11. Fix "hard width 500 px" limitation.
- deployment-guide: document update-major-tag job; note
  Marketplace listing name is `ghstats-cards`.
2026-04-19 00:09:00 +07:00
tiennm99 8cc31d3d97 docs: refresh for context plumbing, rate-limit handling, phase 6
- Fetcher signatures across codebase-summary and system-architecture
  now show the ctx-first arguments and document the rate-limit retry
  loop in Client.query.
- Attribution pseudo-code hoists the per-repo total out of the commit
  loop to match the current implementation (I6).
- Failure-modes table enumerates primary rate-limit retry, per-year
  nil-user warn, and -timeout / Ctrl-C cancellation.
- design-guidelines notes the single-slice donut special case.
- deployment-guide's release section documents the new test gate and
  the SHA-pinned Docker/GHA actions; troubleshooting adds the
  rate-limit-reset-too-long error. Rate-limit section describes the
  sleep-and-retry policy and -timeout flag.
- project-roadmap records Phase 6 (code-review remediation) as done,
  renumbers later planned phases, links the new review report.
2026-04-18 22:55:32 +07:00
tiennm99 8b3a8e1932 docs: add project documentation set
Seven canonical docs under docs/ per the project structure convention:

- project-overview-pdr.md   users, non-goals, requirements
- codebase-summary.md       directory layout, module responsibilities
- system-architecture.md    runtime phases, GraphQL flow, SVG primitives
- code-standards.md         YAGNI/KISS/DRY, Go conventions, commit rules
- design-guidelines.md      frame dimensions, theme roles, per-card specs
- deployment-guide.md       Action/binary/Docker paths, release process
- project-roadmap.md        done phases (0-5), planned phases (6-9)

All files under the 800-line cap. Each leans on tables; grammar
sacrificed for concision per project rules.
2026-04-18 22:10:09 +07:00