Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
448 changes: 448 additions & 0 deletions i18n.js

Large diffs are not rendered by default.

266 changes: 163 additions & 103 deletions index.html

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,26 @@

applyTheme(readStoredThemeChoice(), false);

function menuLabel(key, fallback) {
// i18n.js (loaded before main.js) owns the dictionaries; fall back to the
// English literal if it failed to load so the button never ends up
// without an accessible name.
var lang = root.dataset.lang;
return window.PortfolioI18n ? window.PortfolioI18n.translate(lang, key) : fallback;
}

function closeMenu() {
if (!menuToggle || !sectionNavigation) return;
menuToggle.setAttribute("aria-expanded", "false");
menuToggle.setAttribute("aria-label", "Open section navigation");
menuToggle.setAttribute("aria-label", menuLabel("topbar.menuOpen", "Open section navigation"));
sectionNavigation.classList.remove("is-open");
}

function toggleMenu() {
if (!menuToggle || !sectionNavigation) return;
var open = menuToggle.getAttribute("aria-expanded") !== "true";
menuToggle.setAttribute("aria-expanded", String(open));
menuToggle.setAttribute("aria-label", open ? "Close section navigation" : "Open section navigation");
menuToggle.setAttribute("aria-label", menuLabel(open ? "topbar.menuClose" : "topbar.menuOpen", open ? "Close section navigation" : "Open section navigation"));
sectionNavigation.classList.toggle("is-open", open);
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"private": true,
"type": "module",
"scripts": {
"test": "node --check main.js && node --check effect-skins.js && node --test"
"test": "node --check main.js && node --check effect-skins.js && node --check i18n.js && node --test"
}
}
2 changes: 1 addition & 1 deletion profile/projects.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"star_history_start": "2026-03-01",
"merged_upstream_prs": 38,
"starred_projects": 16,
"_note": "stars_earned includes 5 stars on forks; the graph uses a 2026-03-01 opening balance plus GitHub star events and ends at 99 original-repository stars"
"_note": "stars_earned includes 5 stars on forks; the daily history (stars-history.json) tracks only original-repository events (GitHub has no reliable per-day fork-star signal), so the rendered chart adds fork_stars as a constant offset to every point — the line's endpoint always equals stars_earned, matching the 'all repositories' copy next to it"
},

"contributions": [
Expand Down
1 change: 1 addition & 0 deletions profile/sync/build_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"index.html",
"styles.css",
"main.js",
"i18n.js",
"effect-skins.js",
"favicon.svg",
"og.png",
Expand Down
33 changes: 23 additions & 10 deletions profile/sync/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,27 @@
DEFAULT_OUT = Path(".build/profile")


def chart_data(history: dict) -> dict:
"""Prepare a compact, responsive SVG line-chart from daily cumulative data."""
def chart_data(history: dict, fork_stars: int = 0) -> dict:
"""Prepare a compact, responsive SVG line-chart from daily cumulative data.

``history["entries"]`` tracks only original (non-fork) repositories —
GitHub's stargazer-events API has no reliable per-day signal for fork
stars, so they can't be plotted as their own history (see
stars_history.py's "Original axisrow repositories; forks excluded"
scope). The page's copy next to the chart nonetheless claims "all
repositories" and its stats/current-total swatch shows stats_earned
(original + fork stars) — so ``fork_stars`` is added as a constant
offset to every plotted point, making the line's endpoint equal
stats_earned and match what the visible/accessible chart text asserts.
It does not imply the offset accrued gradually; it has always been
"current" fork stars, added uniformly across the whole series.
"""
entries = history["entries"]
width, height = 960, 340
left, right, top, bottom = 54, 24, 22, 42
plot_width, plot_height = width - left - right, height - top - bottom
maximum = max(entry["total"] for entry in entries)
totals = [entry["total"] + fork_stars for entry in entries]
maximum = max(totals)
ceiling = max(10, ((maximum + 9) // 10) * 10)

def x(index: int) -> float:
Expand All @@ -57,7 +71,7 @@ def x(index: int) -> float:
def y(value: int) -> float:
return top + plot_height * (1 - value / ceiling)

points = " ".join(f"{x(i):.1f},{y(entry['total']):.1f}" for i, entry in enumerate(entries))
points = " ".join(f"{x(i):.1f},{y(total):.1f}" for i, total in enumerate(totals))
ticks = [0, ceiling // 2, ceiling]
month_labels = []
for index, entry in enumerate(entries):
Expand All @@ -67,8 +81,8 @@ def y(value: int) -> float:
return {
"points": points,
"end_x": f"{x(len(entries) - 1):.1f}",
"end_y": f"{y(entries[-1]['total']):.1f}",
"latest_total": entries[-1]["total"],
"end_y": f"{y(totals[-1]):.1f}",
"latest_total": totals[-1],
"latest_date": entries[-1]["date"],
"ticks": [{"value": tick, "y": f"{y(tick):.1f}"} for tick in ticks],
"months": month_labels,
Expand Down Expand Up @@ -128,11 +142,10 @@ def load_history(cfg: dict) -> dict | None:
if not history_path.exists():
return None
history = json.loads(history_path.read_text())
fork_stars = int(cfg["stats"]["fork_stars"])
cfg["stats"] = dict(cfg["stats"])
cfg["stats"]["stars_earned"] = (
history["entries"][-1]["total"] + int(cfg["stats"]["fork_stars"])
)
return {**history, "chart": chart_data(history)}
cfg["stats"]["stars_earned"] = history["entries"][-1]["total"] + fork_stars
return {**history, "chart": chart_data(history, fork_stars)}


def main() -> int:
Expand Down
18 changes: 9 additions & 9 deletions profile/sync/templates/projects.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@
<canvas id="projects-plasma" class="effect-canvas" data-effect="plasma"></canvas>
</div>
<div class="projects-field-copy veil-panel">
<p class="eyebrow"><span>02</span> Selected Work</p>
<h2 class="section-title">Systems with measurable output.</h2>
<p class="section-note">Original repositories with the most stars — see all at <a href="https://github.com/{{ handle }}?tab=repositories" target="_blank" rel="noopener">github.com/{{ handle }}</a>.</p>
<p class="eyebrow"><span>02</span> <span data-i18n="projects.eyebrow">Selected Work</span></p>
<h2 class="section-title" data-i18n="projects.title">Systems with measurable output</h2>
<p class="section-note"><span data-i18n="projects.notePrefix">Own repositories with the most stars — see all at </span><a href="https://github.com/{{ handle }}?tab=repositories" target="_blank" rel="noopener">github.com/{{ handle }}</a>.</p>
<div class="field-caption">
<span>Selected systems</span>
<strong>Automation that ships.</strong>
<small>Agents · integrations · data</small>
<span data-i18n="projects.captionLabel">Selected systems</span>
<strong data-i18n="projects.captionStrong">Automation that ships</strong>
<small data-i18n="projects.captionSmall">Agents · integrations · data</small>
</div>
</div>
</div>
<div class="project-surface veil-panel">
{% for group, repos in projects.items() %} <h3 class="group-title">{{ group }}</h3>
{% for group, repos in projects.items() %} <h3 class="group-title" data-i18n="projects.group.{{ group | replace(' & ', '-') | replace(' / ', '-') | replace(' ', '-') | lower }}">{{ group }}</h3>
<div class="cards project-cards">
{% for name in repos %} <a class="card reveal" href="https://github.com/{{ handle }}/{{ name }}" target="_blank" rel="noopener">
<div class="card-head"><span class="card-name">{{ name }}</span>{% if stars[name] %}<span class="card-star">★ {{ stars[name] }}</span>{% endif %}</div>
<p class="card-desc">{{ descriptions[name] }}</p>
<span class="card-link">View repository ↗</span>
<p class="card-desc" data-i18n="projects.desc.{{ name }}">{{ descriptions[name] }}</p>
<span class="card-link" data-i18n="projects.viewRepository">View repository ↗</span>
</a>
{% endfor %} </div>
{% endfor %} </div>
Expand Down
14 changes: 7 additions & 7 deletions profile/sync/templates/stars.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@
<canvas id="stars-starfield" class="effect-canvas" data-effect="starfield"></canvas>
</div>
<div class="stars-field-copy veil-panel">
<p class="eyebrow"><span>01</span> Momentum</p>
<h2 class="section-title">Stars over time.</h2>
<p class="section-note">Daily cumulative stars on original GitHub repositories. The line starts with an opening balance on {{ star_history.start_date }}; this chart excludes the {{ stats.fork_stars }} stars earned on maintained forks.</p>
<div class="stars-current"><strong>{{ star_history.chart.latest_total }}★</strong><span>{{ star_history.chart.latest_date }} · original repositories</span></div>
<p class="eyebrow"><span>01</span> <span data-i18n="stars.eyebrow">Momentum</span></p>
<h2 class="section-title" data-i18n="stars.title">Stars over time</h2>
<p class="section-note"><span data-i18n="stars.sectionNote">Cumulative GitHub stars across all repositories since </span>{{ star_history.start_date }}.</p>
<div class="stars-current"><strong>{{ stats.stars_earned }}★</strong><span>{{ star_history.chart.latest_date }} · <span data-i18n="stars.currentLabel">all repositories</span></span></div>
</div>
<div class="stars-chart">
<svg viewBox="0 0 {{ star_history.chart.width }} {{ star_history.chart.height }}" role="img" aria-labelledby="stars-chart-title stars-chart-desc">
<title id="stars-chart-title">Cumulative GitHub stars since {{ star_history.start_date }}</title>
<desc id="stars-chart-desc">The chart ends at {{ star_history.chart.latest_total }} stars on {{ star_history.chart.latest_date }}.</desc>
<title id="stars-chart-title" data-i18n-meta="stars.chartTitle" data-i18n-vars='{"startDate":"{{ star_history.start_date }}"}'>Cumulative GitHub stars since {{ star_history.start_date }}</title>
<desc id="stars-chart-desc" data-i18n-meta="stars.chartDesc" data-i18n-vars='{"count":"{{ stats.stars_earned }}","endDate":"{{ star_history.chart.latest_date }}"}'>The chart ends at {{ stats.stars_earned }} stars on {{ star_history.chart.latest_date }}.</desc>
{% for tick in star_history.chart.ticks %} <line class="stars-grid" x1="{{ star_history.chart.left }}" x2="{{ star_history.chart.width - star_history.chart.right }}" y1="{{ tick.y }}" y2="{{ tick.y }}" />
<text class="stars-y-label" x="44" y="{{ tick.y }}">{{ tick.value }}</text>
{% endfor %} <polyline class="stars-line" points="{{ star_history.chart.points }}" />
<circle class="stars-end" cx="{{ star_history.chart.end_x }}" cy="{{ star_history.chart.end_y }}" r="5" />
{% for month in star_history.chart.months %} {# x-label baseline sits 24px below the plot's bottom margin so month names clear the axis. #}
<text class="stars-x-label" x="{{ month.x }}" y="{{ star_history.chart.height - star_history.chart.bottom + 24 }}">{{ month.label }}</text>
<text class="stars-x-label" x="{{ month.x }}" y="{{ star_history.chart.height - star_history.chart.bottom + 24 }}" data-i18n="month.{{ month.label }}">{{ month.label }}</text>
{% endfor %} </svg>
</div>
</div>
Expand Down
21 changes: 21 additions & 0 deletions profile/tests/test_build_pages.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
import re
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
Expand All @@ -16,6 +17,11 @@

REPO = Path(__file__).resolve().parents[2]

# Matches a same-origin <script src="name.js?v=..."> reference — deliberately
# excludes absolute/protocol-relative URLs (http:, https:, //) so a CDN script
# tag never gets mistaken for a local file the artifact must ship.
_LOCAL_SCRIPT_SRC_RE = re.compile(r'<script\s+src="([^":/][^"]*?)(?:\?[^"]*)?"')


def _seed_site_root(temp: Path) -> Path:
"""Make a temp directory behave as the real site root.
Expand Down Expand Up @@ -63,6 +69,21 @@ def artifact_files(canonical: Path) -> set[str]:
if path.is_file()
}

def test_every_local_script_index_html_references_is_in_public_files(self) -> None:
# Regression guard for the bug this test was added alongside: index.html
# gained a new <script src="i18n.js"> without PUBLIC_FILES being updated
# to match, so the deployed Pages artifact silently omitted i18n.js —
# production requested a nonexistent script. This asserts the inverse
# direction (every referenced local script is shipped) so a future
# <script> addition can't repeat that gap.
html = (REPO / "index.html").read_text()
referenced = {match.group(1) for match in _LOCAL_SCRIPT_SRC_RE.finditer(html)}
self.assertTrue(referenced, "expected to find at least one local <script> tag")
self.assertTrue(
referenced.issubset(set(PUBLIC_FILES)),
f"index.html references local scripts not in PUBLIC_FILES: {referenced - set(PUBLIC_FILES)}",
)

def test_artifact_is_allowlisted_and_source_is_unchanged(self) -> None:
with TemporaryDirectory() as tmp:
site_root = _seed_site_root(Path(tmp))
Expand Down
39 changes: 39 additions & 0 deletions profile/tests/test_generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,40 @@ def test_latest_total_and_end_point_match_last_entry(self) -> None:
# end_x is the x-projection of the last index: left + plot_width.
self.assertEqual(chart["end_x"], "936.0")

def test_fork_stars_offsets_every_plotted_point_and_the_endpoint(self) -> None:
# fork_stars has no daily history of its own (GitHub gives no reliable
# per-day signal for fork stargazers), so it's added as a constant
# offset to every entry's total — the chart's endpoint (latest_total)
# must equal stats_earned (original total + fork_stars), matching the
# "all repositories" copy rendered next to the chart.
entries = [
{"date": "2026-03-01", "gained": 0, "total": 0},
{"date": "2026-03-02", "gained": 7, "total": 7},
]
chart = chart_data(self._history(entries), fork_stars=5)
self.assertEqual(chart["latest_total"], 12)
self.assertEqual(len(chart["points"].split()), 2)
# A fix that only patched the endpoint (e.g. adding fork_stars to
# latest_total alone, without threading it through every plotted
# point) would leave ceiling/scale computed off the un-offset totals
# and the first point's y unaffected. With the offset applied to
# every entry, ceiling must be sized off the offset maximum (12 → 20,
# not 7 → 10) and the first point (total 0 + 5 = 5) must sit above
# the axis baseline (y < the y of an unoffset 0).
no_offset_chart = chart_data(self._history(entries))
self.assertEqual(chart["ticks"][-1]["value"], 20)
self.assertEqual(no_offset_chart["ticks"][-1]["value"], 10)
first_point_y = float(chart["points"].split()[0].split(",")[1])
baseline_y = float(chart["ticks"][0]["y"]) # y for value=0 on this chart's scale
self.assertLess(first_point_y, baseline_y)

def test_zero_fork_stars_is_the_default_and_matches_prior_behavior(self) -> None:
entries = [{"date": "2026-03-01", "gained": 0, "total": 9}]
with_default = chart_data(self._history(entries))
with_explicit_zero = chart_data(self._history(entries), fork_stars=0)
self.assertEqual(with_default, with_explicit_zero)
self.assertEqual(with_default["latest_total"], 9)


class LoadHistoryTests(unittest.TestCase):
"""load_history resolves its file as ROOT.parent/data/stars-history.json,
Expand Down Expand Up @@ -146,6 +180,11 @@ def test_recomputes_stars_earned_from_last_entry_plus_fork_stars(self) -> None:
assert result is not None
self.assertIn("chart", result)
self.assertEqual(result["entries"], entries)
# The chart's own endpoint must match stats_earned — the page's copy
# next to the chart claims "all repositories", so the plotted line
# has to end where that number says it ends, not at the
# forks-excluded raw entry total.
self.assertEqual(result["chart"]["latest_total"], 104)

def test_does_not_mutate_caller_stats_dict(self) -> None:
entries = [{"date": "2026-03-01", "gained": 0, "total": 50}]
Expand Down
4 changes: 2 additions & 2 deletions profile/tests/test_site_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ def test_generated_sections_keep_the_new_visual_order_numbers(self) -> None:
project_root = Path(__file__).resolve().parents[1]
projects = (project_root / "sync/templates/projects.html.j2").read_text()
stars = (project_root / "sync/templates/stars.html.j2").read_text()
self.assertIn('<span>01</span> Momentum', stars)
self.assertIn('<span>02</span> Selected Work', projects)
self.assertIn('<span>01</span> <span data-i18n="stars.eyebrow">Momentum</span>', stars)
self.assertIn('<span>02</span> <span data-i18n="projects.eyebrow">Selected Work</span>', projects)

def test_generated_sections_keep_their_effect_canvases(self) -> None:
# The bot rewrites everything between the PROFILE markers from these
Expand Down
62 changes: 61 additions & 1 deletion styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ body {
transition: background-color 360ms ease, color 360ms ease;
}

/* index.html ships English text inline; the <head> bootstrap script adds
.lang-loading to <html> only when it resolves a non-English language, and
i18n.js (loaded at the end of body) removes it synchronously right after
translating — this window hides the flash of English rather than the
whole first render. */
.lang-loading body {
visibility: hidden;
}

a {
color: inherit;
text-decoration: none;
Expand Down Expand Up @@ -293,6 +302,57 @@ a {
outline-offset: 2px;
}

/* Same collapsed-select technique as .theme-select above: a two-letter icon
sits over a fully transparent, full-size <select> so the control keeps
native keyboard/screen-reader semantics while matching the round
icon-button style shared with the theme toggle and GitHub link. */
.lang-select-label {
position: relative;
display: inline-flex;
padding: 0;
overflow: hidden;
}

.lang-select-icon {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-family: "IBM Plex Mono", monospace;
letter-spacing: 0.02em;
line-height: 1;
pointer-events: none;
}

.lang-select {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
padding: 0;
border: 0;
background: transparent;
color: transparent;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}

.lang-select option {
color: var(--ink);
background: var(--surface-solid);
}

.lang-select:focus-visible {
outline: none;
}

.lang-select-label:has(.lang-select:focus-visible) {
outline: 2px solid var(--accent);
outline-offset: 2px;
}

main {
position: relative;
}
Expand Down Expand Up @@ -335,7 +395,7 @@ main {
text-transform: uppercase;
}

.availability span {
.availability span:first-child {
width: 7px;
height: 7px;
flex: 0 0 auto;
Expand Down
Loading