Skip to content

Tier 4 refactor: router chrome, design system, unit tests#68

Merged
felipebalbi merged 8 commits into
OpenDevicePartnership:mainfrom
felipebalbi:tier4-refactor
May 15, 2026
Merged

Tier 4 refactor: router chrome, design system, unit tests#68
felipebalbi merged 8 commits into
OpenDevicePartnership:mainfrom
felipebalbi:tier4-refactor

Conversation

@felipebalbi
Copy link
Copy Markdown
Contributor

Depends on #67.

Tier 4 -- structural refactor

This stack lands the deepest readability/maintainability work in the
series: a router-level page chrome, an externalized D3 graph, a shared
Section shell, host-side unit tests, a logo-as-home navigation
fix, and a small design-system module wired into eight components.

Commits

  • t24 -- cargo fmt + cargo clippy gates in CI.
  • t22 -- Externalize the repo_view D3 graph script and
    module so the component file is layout-only.
  • t20 -- Extract <Section> (background + horizontal padding
    ladder + padded flag) and migrate every page section onto it.
  • t21 -- Move static content (projects, teams, partners, graph
    JSON) into src/data and data/. Fixes a script-load race
    for the project graph by making index.html <script defer>
    the d3 + repo_graph.js bundle.
  • t23 -- Hoist the <Header> + <Footer> chrome into a
    router-level <SiteShell> parent route so individual pages stop
    hand-pairing them.
  • t27 -- The logo is now the home link; the redundant Home nav
    button is gone; /home is dropped in favor of the canonical
    / route.
  • t26 -- Host-side cargo test sweep (36 tests across the
    data layer, Section, SiteShell, and the ui primitives) and
    a matching CI job.
  • t19 -- src/components/ui/ design-system primitives
    (Heading, Text, Mono, Link, Button,
    Container, Stack, Grid) plus a team_hero.rs
    shared component, migrated into eight call sites. Dedupes three
    triplicated team-page heroes and three project-row blocks.

Discussion item (t25): CSR Leptos vs static SSG

The repo is a marketing site with no client-side interactivity beyond
the D3 graph and the mobile nav toggle. The current Leptos CSR build
costs the user a wasm download and a JS bootstrap before any content
paints, which hurts first-contentful-paint and SEO.

Worth discussing as a separate effort: regenerate the same content
with a static-site generator (zola, mdbook, astro, or even a thin
leptos_axum SSR) so the HTML is served as-is. The design system, data
modules, and component breakdown from this stack would carry over
directly. Not in scope for this PR.

felipebalbi and others added 8 commits May 15, 2026 10:41
Two new `required` checks alongside the existing
`line-endings` / `leptosfmt --check` / `trunk build` jobs:

  * `rustfmt` -- `cargo fmt --all -- --check` on stable.
    Catches the kind of trailing-newline / whitespace drift that
    `leptosfmt` doesn't see (`leptosfmt` only formats the
    contents of `view! { ... }` macro invocations; everything
    else is left to `rustfmt`). Also fixes one such offender:
    `image_button.rs` was missing the trailing newline that
    `rustfmt` requires after the closing brace.

  * `clippy` -- `cargo clippy --target wasm32-unknown-unknown
    --all-targets --no-deps -- -D warnings` on nightly with the
    `Swatinem/rust-cache` action keyed separately from the
    build job's cache so clippy and trunk don't fight over
    `target/`. `--no-deps` keeps the noise floor down (we only
    own our own crate); `-D warnings` makes any new lint
    failure the PR's problem rather than a slow drift.

Both jobs run on `pull_request` to `main` and `push` to
`main` like the rest of the suite.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The repository-dependency graph used on the three project pages was a
single `repo_view.rs` file mixing four very different things:

  * ~230 lines of inline D3 v7 JavaScript embedded in a Rust
    `format!()` string with doubled braces everywhere -- impossible
    to lint, format, or read with JS tooling.
  * ~50 lines of CSS embedded in another Rust string and injected
    into `<head>` on every component mount.
  * The Rust component itself, plus an unused `D3` `JsValue`
    extern, redundant `use leptos::*;` / `use leptos::html::*;`
    glob imports, and two `signal()` pairs whose setters were
    never used.
  * A per-mount `<script>` injection that re-loaded the d3 CDN on
    every navigation between project pages.

Split it apart:

  * `public/repo_graph.js` -- the D3 rendering code as a normal
    JavaScript module. `leptosfmt` / `rustfmt` no longer have to
    pretend they understand it; editor tooling and CI linters see real
    JS. Exposes `window.__odpRenderGraph()` and uses
    `window.__odpGraphData` for the per-page payload. Trunk copies
    the file via `rel="copy-file"`.
  * `style/repo_graph.css` -- the styles, loaded once from
    `index.html` via `rel="css"`. No more runtime `<style>`
    injection.
  * `src/components/repo_view.rs` -- ~95 lines, all Rust. On mount it
    publishes `window.__odpGraphData` via a one-shot inline script
    (which removes itself), then ensures both `d3.v7.min.js` and
    `repo_graph.js` are loaded exactly once for the lifetime of the
    page (`getElementById` guards). Subsequent route changes between
    the three project pages just call `__odpRenderGraph()` with the
    new data.

Drive-by fixes the cleanup made obvious:

  * Dropped the unused `D3` `#[wasm_bindgen]` extern.
  * Dropped `use leptos::*;` and `use leptos::html::*;` (both
    redundant with `leptos::prelude::*`).
  * Replaced two unused `(_, set_*)` `signal()` pairs with a
    single `Effect::new` that no longer needs an explicit
    initialised flag.
  * Scoped the `.node` / `.link` / `.column-headers` rules
    under `.repository-graph` in the new CSS file so they cannot
    leak into other pages.

Behaviour preserved: the graph still renders identically, with the
same zoom controls, drag, header columns, and click-to-open-URL.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Six call sites across landing_page, projects_component, partners_grid,
and community_teams repeated the same long horizontal-padding ladder
(`px-4 sm:px-8 md:px-16 lg:px-32`) plus a `background_*` surface
class. Some had drifted to slightly different mobile padding
(`px-6`, `px-4 py-6`) which made the codebase look intentional when
it was just inconsistency.

Add `components/section.rs` exporting:

* `Surface` -- enum mapping to the existing `.background_*`
  utility classes (Primary/Secondary/Tertiary/Quaternary).
* `<Section surface=... class=...>` -- emits `<section>` with the
  surface class, the standard horizontal ladder, and any extra
  utilities (typically vertical padding).

Convert the six sites. Vertical padding moves to the `class` prop
(`py-20`, `py-32`, `pb-32`, `py-6`, `py-8 md:py-20 lg:py-32`).
`project_introduction.rs` keeps its own bare `<section>` because it
intentionally has no padding (composed inside pages with their own).

No visual change at the dominant breakpoints; only side effect is that
`community_teams` and `projects_component` mobile padding is now
the canonical 16/32/64/128px ladder instead of an off-by-one 24px.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Page components were carrying large static payloads inline:

* Team rosters (~10 members each across patina/ec/ec-services and the
  steering committee) lived in `create_team()` helpers next to the
  layout code.
* The partner list was a private `const PARTNERS` inside
  `partners_grid.rs`.
* Per-project marketing copy ("title", "summary", "what", "why") was
  inlined as multi-line `r#""#` strings inside each project page.
* The repository dependency graph payloads were ~7 KB JSON literals
  embedded as `r#"[...]"#` raw strings inside the project pages.

Move them into dedicated locations so future contributors can edit a
team roster or a partner without touching layout code:

* `src/data/teams.rs`    -- `steering_committee()`, `patina_team()`,
  `ec_team()`, `ec_services_team()`.
* `src/data/partners.rs` -- `PartnerInfo` struct + `PARTNERS` slice.
* `src/data/projects.rs` -- `ProjectCopy` struct + `PATINA`,
  `EMBEDDED_CONTROLLER`, `EC_SERVICES` constants. Graph node/link
  JSON is pulled in via `include_str!` from `data/graphs/*.json`.
* `data/graphs/{patina,ec,ec_services}_{nodes,links}.json` (six new
  files) -- the actual graph payloads as standalone JSON.

The page components shrink dramatically (~80% in some cases) and the
project pages are now nearly identical: layout + props pulled from
`crate::data::projects::*`. No new dependencies; `include_str!` is
the cheapest possible extraction.

Fix a load-order race uncovered by this work: the original
`RepositoryGraph` injected d3 + `repo_graph.js` from a Leptos
`Effect` on the first mount and then immediately called the (not
yet defined) renderer. On the very first navigation to a project
page the chained network downloads hadn't completed by the time the
component published its data, so the graph stayed blank until the
user reloaded. The new wiring loads both scripts as `<script defer>`
tags from `index.html` so they finish executing before the Wasm
bundle boots, the Effect just publishes data + calls the renderer,
and `repo_graph.js` re-tries on `requestAnimationFrame` if its
SVG host (or d3) is not yet present. `RepositoryGraph` drops ~70
lines of script-injection plumbing as a result.

Add t26-unit-tests to the backlog: a tier-end sweep to add
wasm-bindgen-test coverage to applicable components, with the
script-load race above as the highest-priority regression test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the per-page <PageLayout> wrapper with two sibling
<ParentRoute>s in the router that render Header, <Outlet/>, and
Footer inside a shared ErrorBoundary. SiteShell uses overflow-x-
hidden, SiteShellScrollable uses overflow-x-auto. Pages now contain
only their content, with no Header/Footer/ErrorBoundary boilerplate.

Announcements keeps its own chrome (custom Header background and
hand-rolled layout) and remains a top-level route.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wrap the ODP logo in a router <A href="/"> so clicking the logo
returns to the landing page. Remove the now-redundant Home button
from both the desktop and mobile navs. Drop the /home route — the
canonical home URL is /.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add host-side unit tests (runnable via plain `cargo test`) for the
data and presentation pieces that were previously protected only by
manual click-through:

* `src/data/projects.rs` -- parse each repository graph JSON,
  reject empty graphs, enforce unique node ids, and verify that
  every link's source/target points at an existing node id. This is
  exactly the invariant the D3 renderer assumes; the script-load
  race fixed alongside t21 was a browser-runtime issue but at least
  these tests now guarantee the payload itself is well-formed once
  the renderer does run. Also assert each project's required copy
  and asset fields are populated.
* `src/data/teams.rs` -- every roster non-empty, every member has
  a non-empty first/last/github username, and the github_url and
  image_url derive from the username (the easy place for a typo to
  hide).
* `src/data/partners.rs` -- non-empty list, https urls, logos
  under `/images/partners/`.
* `src/components/section.rs` -- factor the class composition into
  a pure `section_class` helper and pin its behaviour:
  `Surface` -> brand class mapping, the padded/unpadded ladder
  toggle, and the empty-extra regression.
* `src/components/site_shell.rs` -- pin the two shell class
  strings so a refactor cannot accidentally drop `overflow-x-hidden`
  on the default shell.

Wire everything in:

* `serde_json` becomes a dev-dependency (host-side JSON parsing).
* `.github/workflows/ci.yaml` gains a `cargo test` job.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The new ui module shipped without users in the previous commit. This
commit migrates eight components to consume the Heading/Text/Mono
primitives so the design tokens are actually load-bearing, and dedupes
three triplicated blocks along the way.

New shared components:

* team_hero.rs -- the "Meet the team" hero used by every per-team
  page. team_ec, team_patina, and team_ec_services now contain just
  <TeamHero ... /> + <TeamGrid ... />, dropping ~50 lines of
  identical layout from each.

* projects_component.rs grew an internal ProjectRow + ProjectLink
  pair that collapses three near-identical "image | title + tagline +
  description + two doc links" rows into a data-driven list.

* community_teams.rs grew an internal TeamCard for the three 320x350
  call-to-action cards.

Migrated to <Heading>/<Text>/<Mono>:

* landing_page.rs
* projects_component.rs
* project_introduction.rs (kept the inner_html slot as a raw <p>
  since the value is rendered HTML, not children)
* partners_grid.rs (kept H1 visual size for parity)
* documentation_training.rs
* team_grid.rs
* community_teams.rs
* team_ec.rs / team_patina.rs / team_ec_services.rs (via TeamHero)

Drive-by responsive cleanups in community_teams.rs:

* fixed items-left -> items-start (items-left is not a real class)
* gap: 175px -> gap-16 md:gap-24 lg:gap-[175px] so tablets don't get
  crushed
* width: 320px cards -> w-full md:w-80 so they don't overflow on
  phones

The ui module still keeps #![allow(dead_code, unused_imports)] for
the primitives (Container, Stack, Grid, Button, IconButton, Link)
that haven't reached call sites yet -- those will land in follow-up
work.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@felipebalbi felipebalbi merged commit fedb49e into OpenDevicePartnership:main May 15, 2026
6 checks passed
@felipebalbi felipebalbi deleted the tier4-refactor branch May 15, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant