# Design Guidelines Visual conventions for ghstats SVG cards. All cards share a single frame shape so they stack cleanly in a README. ## Card frame | Property | Value | | --- | --- | | Width × Height | **500 × 220** | | Corner radius | 8 px | | Stroke | `theme.Stroke` at `theme.StrokeOpacity` | | Fill | `theme.Background` | | Font family | `'Segoe UI', Ubuntu, Sans-Serif` | | Title | 18 px, weight 600, `theme.Title`, anchored at `(25, 35)` | 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 | Value | | --- | --- | | First row baseline (y) | 70 | | Row spacing | 24 px | | Icon scale | `14/16 = 0.875` from 16×16 Octicon viewBox | | Icon color | `theme.Muted` | | Right-aligned value anchor (stats) | `x = 475`, `text-anchor="end"` | | Value font weight | 600 | | Value color | `theme.Accent` | Cap rows at what fits: 7 for profile, 7 for stats (commits row splits into lifetime + last-year). ## Donut cards (language breakdowns) | Metric | Value | | --- | --- | | Donut centre | `(380, 120)` | | Outer radius | 70 | | Inner radius | 38 | | Top-N entries shown | 6 (overflow collapses into "Other") | | Slice stroke | `theme.Background`, 1.5 px (gap between slices) | | Legend origin | `(30, 70)` | | Legend row height | 22 px | | Swatch size | 12 × 12 | 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 ∈ [50, 475]`, `y ∈ [60, 170]` (110 tall) | | Bars | 24 bars, 2 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 ∈ [35, 465]`, `y ∈ [60, 180]` (120 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 `…`. - 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. ## 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 500 px width, that's a card design problem — fix the layout, not the data.