# 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 `` 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 (, UTC±N.NN)` | | Hover | `HH:00 — N commits` 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](https://primer.style/octicons/) 16×16 set. - Stored as raw `` strings in `internal/card/icons.go`. - Rendered inside `` (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 `` from Octicons and appending to `icons.go`. Keep them to the same 16×16 viewBox so the existing scale math applies. ## Fit-the-frame invariant (MUST hold before release) Every card MUST render entirely inside the 340 × 200 frame **for every profile the card can encounter**, not just the author's. That means: | Thing that varies | Worst case to design for | | --- | --- | | Star counts, commit counts, streak lengths, active days | 10-digit formatted integer (e.g. `1,234,567,890`) | | Repo / language / company / location names | 40+ char strings with CJK / emoji | | Number of active years | 20+ years (contribution history can start in 2008) | | Contribution calendar weeks | 53 — not 52 — when the window spans a year transition | Concrete rules this implies: - **Reserve columns.** When a card has `N` equal-width columns, treat `340 / N` as the hard limit for each column's widest element. No centered text can be wider than its column. - **Right-anchored values** (stats rows, top-starred bars) must leave a safety margin. Right edge ≤ `width − 6`; do not place an icon to the right of a right-anchored number (they collide on multi-digit values). - **Long strings get truncated, not wrapped.** Use `truncateName` (top-starred) or pick a layout that allows clipping via `text-overflow` semantics. Never let a wide string push a later element off-screen. - **Variable-count grids** (heatmap 7×N, by-year N bars) must compute cell size from the container width, not the other way round. Don't hardcode a cell size that only works for the author's profile. - **Month / year tick labels** within `~20 px` of the right edge must be skipped (they read past the frame otherwise). ### Review checklist (before merging any card change) Render the **dracula** theme against at least three profiles or synthetic fixtures: 1. **Tiny**: a brand-new account with 0 commits, 0 stars, 1 repo. 2. **Typical**: the author's profile (`tiennm99` via `demo/` regeneration). 3. **Adversarial**: seven-digit commit counts, 40-char repo / company / location names, 20 active years, 53-week span. A small synthetic `*.json` fixture is fine; it doesn't need a token. Then open every affected SVG at 1× and 2× zoom and verify: - [ ] No ``, ``, ``, ``, or path coordinate exceeds `x=340` or `y=200`, or falls below `x=0` / `y=0`. - [ ] No two elements overlap in a way that makes either unreadable. - [ ] Right-anchored numbers don't collide with icons, swatches, or bars. - [ ] Peak-vs-dim highlighting still reads at a glance (dracula `Background → Accent` contrast is fine; light themes like `github` or `nord_bright` need a separate check). The `demo//` gallery auto-regenerates on every push to `main`; use the last CI run as the dracula reference, and stress-test the adversarial case locally before pushing. ## Accessibility - Contrast is the theme author's responsibility — we don't validate at runtime. - Tooltips (``) 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.