- 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`.
4.6 KiB
Design Guidelines
Visual conventions for ghstats SVG cards. All cards share a single frame shape so they stack cleanly in a README — two cards sit side-by-side inside GitHub's ~816 px content column.
Card frame
| Property | Value |
|---|---|
| Width × Height | 340 × 200 (matches github-profile-summary-cards) |
| Corner radius | 6 px |
| Stroke | theme.Stroke at theme.StrokeOpacity |
| Fill | theme.Background |
| Font family | 'Segoe UI', Ubuntu, Sans-Serif |
| Title | 15 px, weight 600, theme.Title, anchored at (20, 30) |
Generated by header(width, height, bg, stroke, strokeOpacity, titleColor, title) in internal/card/svg.go.
Theme role mapping
| Theme field | Used for |
|---|---|
Title |
Card title text |
Text |
Primary content (values, names) |
Background |
Card fill + stroke around donut slices to separate colors |
Stroke + StrokeOpacity |
Card outline |
Muted |
Axis lines, axis labels, icons, legend metadata |
Accent |
Bars, area fills, stat values, fallback slice color |
Cards MUST NOT hardcode colors outside these fields. If a new visual needs a shade, pick the closest existing field — don't extend the schema without a strong reason.
Row-based cards (profile, stats)
Single-column rows of icon + label or icon + label + value.
| Metric | Profile | Stats |
|---|---|---|
| First row baseline (y) | 60 | 55 |
| Row spacing | 20 px | 20 px |
| Row x padding | 20 | 20 |
| Icon scale | 12/16 = 0.75 from 16×16 Octicon viewBox |
same |
| Icon color | theme.Muted |
same |
| Value font | 12 px, theme.Text |
12 px, weight 600, theme.Accent, right-anchored at x = 320 |
Cap rows at what fits: up to 7 rows per card. Stats splits commits into lifetime + last-year rows.
Donut cards (language breakdowns)
| Metric | Value |
|---|---|
| Donut centre | (250, 110) |
| Outer radius | 55 |
| Inner radius | 30 |
| Top-N entries shown | 5 (overflow collapses into "Other") |
| Slice stroke | theme.Background, 1.5 px (gap between slices) |
| Legend origin | (20, 55) |
| Legend row height | 20 px |
| Swatch size | 10 × 10 |
| Legend font | 11 px |
Language colors come from linguist via GraphQL (repo.languages.edges[].node.color). Missing colors fall back to theme.Accent.
When there's exactly one slice (one language at 100%), the renderer emits two concentric <circle> elements instead of a pie arc, because SVG's A command from point P back to the same P draws nothing. Regression guarded by TestDonutSingleSlice.
Bar-chart cards (productive time)
| Metric | Value |
|---|---|
| Chart area | x ∈ [35, 325], y ∈ [45, 155] (110 tall) |
| Bars | 24 bars, 1 px gap |
| Bar fill | theme.Accent |
| Y-axis ticks | niceTicks(max, 5) — 1/2/5 × 10^k ladder |
| X-axis labels | Hours 0, 6, 12, 18, 23 |
| Axis caption | "hour of day" bottom-center |
| Title format | Commits by Hour (<window>, UTC±N.NN) |
| Hover | <title>HH:00 — N commits</title> inside each bar |
Area-chart cards (contributions)
| Metric | Value |
|---|---|
| Chart area | x ∈ [28, 312], y ∈ [45, 150] (105 tall) |
| Curve | Catmull-Rom → cubic Bezier (tension 0.5) |
| Fill | theme.Accent at 25% opacity |
| Stroke | theme.Accent, 2 px |
| Y-axis | Both sides, mirrored, same tick values |
| X-axis labels | mm/yy, stride-thinned to ~6 labels regardless of bucket count |
| Axis caption | "mm/yy" bottom-center |
| First/last labels | Always printed (pinned endpoints) |
Missing months in the [first, last] range are inserted as zero-count rows to keep the curve time-continuous.
Icons
- Sourced from Primer Octicons 16×16 set.
- Stored as raw
<path d="…"/>strings ininternal/card/icons.go. - Rendered inside
<g transform="translate(x,y) scale(0.75)" fill="muted">…</g>(scaled to fit the 12 px box). - Used:
iconRepos,iconCompany,iconLocation,iconClock,iconLink,iconPeople,iconStar,iconCommit,iconPR,iconIssue,iconReview.
Add new icons by copying the <path> from Octicons and appending to icons.go. Keep them to the same 16×16 viewBox so the existing scale math applies.
Accessibility
- Contrast is the theme author's responsibility — we don't validate at runtime.
- Tooltips (
<title>) on productive-time bars let screen readers announce counts. - No motion, no
<animate>elements — profile READMEs render statically.
Text overflow
- Long strings (bio, repo names) are not truncated; they're XML-escaped and printed as-is.
- If a card looks crowded at 340 px width, that's a card design problem — fix the layout, not the data.