Skip to content

fix(web): gallery anchors select their filter; site links all resolve - #695

Open
DemchaAV wants to merge 12 commits into
developfrom
fix/web-showcase-navigation
Open

DemchaAV wants to merge 12 commits into
developfrom
fix/web-showcase-navigation

Conversation

@DemchaAV

Copy link
Copy Markdown
Owner

Why

The showcase renders a category section only while its filter shows it. After choosing Features, the Templates menu link — like every #<category>-section anchor in the menu and the sitemap — changed the address and moved nothing, because #templates-section was not in the DOM.

The page also pointed at things the site does not publish, and nothing checked them:

  • HIGHLIGHT_IDS named cv-sidebar-portrait and cv-monogram-sidebar, whose cards are …-v2. The featured strip skips an unknown id without a sign, so it showed six of its eight tiles.
  • The no-JavaScript index linked to invoice.pdf, proposal.pdf and features/themes/invoice-custom-theme.pdf; none of them ships.
  • The structured data said JVM 21+ while every module compiles with maven.compiler.release 17, the preset card counted 16 CV presets where 26 ship, and the template-authoring buttons linked the develop docs from the released site.

GitHub Pages serves web/ as committed, so each of these reached visitors directly.

What changed

  • web/examples.js — section anchors. An anchor naming the gallery (#showcase) or a category section selects that filter first, then aligns the gallery heading below the sticky header, which on a phone is taller than the section's top padding.

    • Click: the view is rebuilt only when it changes, so an expanded Show all group stays open. The search is kept unless it hides the named section. pushState records the anchor, and focus moves to #showcase-title, as it would for a link the browser follows.
    • Back / Forward: a gallery anchor re-selects its filter. An entry with no anchor shows the whole gallery, at the position the browser restores.
    • Opened at an anchor: the page jumps to the gallery once the manifest renders, except on a reload or history traversal, where the browser restores the reader's position.
    • Filter pills: replaceState the matching anchor, so a reload reopens the same view.
  • web/examples.js — other fixes. The two featured ids gain -v2. A failed manifest load tells visitors to refresh or browse the examples on GitHub, instead of telling them to run ShowcaseSync.

  • web/index.html.

    • The dead fallback links now point at invoice-modern-v2.pdf and proposal-modern-v2.pdf; the custom-theme entry and a duplicate cover-letter entry are removed.
    • JSON-LD says JVM 17+.
    • The preset card says 26 CV presets and create(): ten presets have no create(BrandTheme).
    • The authoring buttons link main, and the footer links the 2.0 migration guide instead of the 1.6 → 1.7 one.
  • ShowcaseSiteGuardTest (core, documentation guards), added to the CI guard job and to the CONTRIBUTING list. web/** matches no path filter, so the always-running guard job is where a site-only pull request gets checked. A release cut's verify also runs it over the catalogue the cut has just regenerated. It fails when:

    • a featured id is not a card in examples.json;
    • a card's pdf, screenshot or pptx is not a file under web/showcase/;
    • index.html, sitemap.xml or robots.txt links to a site file that does not exist. This covers relative href/src and any https://demchaav.github.io/GraphCompose/… address;
    • a #<category>-section anchor or a filter pill names a category the manifest lacks, or another anchor names no element in index.html.

    Every list the guard reads must be non-empty, so a page whose shape moved fails instead of passing unchecked.

  • StrictJsonReader and StrictJsonReaderTest (core test scope). The core test classpath has no JSON library. The reader refuses what Response.json() refuses: raw control characters in strings, +1, 01, trailing commas and non-JSON whitespace. A manifest the page could not load therefore fails the guard instead of reading as a shorter catalogue.

  • web/README.md documents three things:

    • what a release changes in web/: the five version places cut-release.ps1 rewrites on a final release (four of them held by VersionConsistencyGuardTest), the GH_BASE flip and catalogue sync, and -PostReleaseOnly;
    • the checks above;
    • that the Pages deploy on a push to main does not wait for the Maven Central deployment, which is published by hand.
  • CHANGELOG.md opens ## v2.4.1 — Planned with a Documentation entry.

Verification

  • Build and tests. ./mvnw -B -ntp clean verify -pl :graph-compose-core,:graph-compose-render-pdf,:graph-compose-render-docx,:graph-compose-render-pptx,:graph-compose-templates,:graph-compose-testing,:graph-compose,:graph-compose-bundle,:graph-compose-qa,:graph-compose-coverage -am gives BUILD SUCCESS in 3 min: 3,188 tests, 0 failures, 2 skipped.

    • Core ran 773 tests, including every guard the CI guard job names (CiGuardListGuardTest resolves the new name).
    • 9 of them are new: ShowcaseSiteGuardTest has 4 checks over the committed web/ files and 3 over parser inputs, and StrictJsonReaderTest adds 2.
  • Sabotage. Each case patched the real file, ran ShowcaseSiteGuardTest and restored the file. Every case went red and named the broken item:

    • Renaming a featured id, a card preview, an index.html link and a sitemap PDF gave three failures, listing table-advancedX, receipt-modernX.png, weekly-scheduleX.pdf and business-reportX.pdf.
    • Renaming the HIGHLIGHT_IDS constant made the guard refuse to read a list that moved.
    • A { appended to examples.json, or a form feed inside one of its strings, gave not valid JSON.
    • Pointing the sitemap at another host made both page checks report that they found nothing to check.
    • #feature-section in the sitemap and a flagshipsX pill were each named as landing nowhere.
    • A card pdf set to index.html was reported as not a file under showcase/.
  • Browser, serving web/ locally at 1280 px and 375 px:

    • After Features, the Templates menu link shows the Templates section with its heading 16 px below the header. Before the change, only features-section stayed in the DOM and the page did not move.
    • index.html#features-section opens on the Features view. Back and Forward re-select the filter, and Back to the entry without an anchor shows All.
    • A menu click leaves focus on #showcase-title.
    • An expanded Show all group and a matching search survive a click on the view already shown. A search that hides the named section is cleared.
    • A reload does not jump.
    • The featured strip shows 8 tiles, against 6 before.
    • With examples.json missing, the error names GitHub.
    • No console errors.
  • Not run: a real Tab key press (the preview pane was not drawing, so it took no key input) and the browser's own scroll restoration on reload.

Lane: site and test. Covers web/, one core documentation guard with its JSON reader, and the CI guard list; no library code, no public API.

A category section exists only while its filter renders it, so once another
filter was picked the menu's #<category>-section links changed the address and
moved nothing. examples.js now resolves a gallery anchor to its filter on a
click, on Back/Forward and when the page opens at one, keeps the address in
step with the filter pills, and moves focus to the gallery as a followed link
would.

Two featured ids named no card, three no-JavaScript fallback links named PDFs
the site does not ship, the structured data said JVM 21+ where every module
targets Java 17, and the page counted 16 CV presets where 26 ship.
ShowcaseSiteGuardTest, added to the CI guard job, fails on a featured id, card
file, site link, anchor or filter pill that resolves to nothing, and reads
examples.json with a strict JSON reader so a manifest the page could not load
fails too.
if (at + 4 > text.length() || !HEX_DIGITS.matcher(text).region(at, at + 4).matches()) {
throw error("a malformed \\u escape");
}
char c = (char) Integer.parseInt(text, at, at + 4, 16);
throw error("an unexpected character");
}
at = number.end();
return Double.valueOf(number.group());
A card cropped its preview to a 248-pixel band from the top, so a document
was judged by its header and a wide slide lost its sides. Cards and featured
tiles now shrink the page into a fixed-height box at its own aspect ratio,
and the image tags drop the 595x842 size that was wrong for the 26 previews
that are not A4.
A card opened a lightbox holding a single image: to compare the CVs a reader
returned to the grid for each one, and nothing in the address said which document
was on screen, so a document could not be shared, reopened or reached with Back.

A card, a featured tile or a family tile now opens its family in a viewer that
shows the whole first page, moves with Previous, Next and the arrow keys, says
where it is in the family, and links the PDF and the source of the document
shown. A switch moves to the other families of the category, each reopening on
the document it was left on. The address #/<category>/<family>/<id> reopens the
same document on a reload and from a shared link: opening records one history
entry, moving inside the viewer replaces it, and closing returns the address and
the focus to where the reader came from.

- gallery-viewer.js: the addresses, the navigation over the catalogue, and the
  dialog. Left and Right are read from the document while it is open, because at
  the ends of a family the step button holding focus is disabled and the browser
  hands focus back to the page, which would take the keys with it; a button about
  to be disabled passes focus to the other step, or to Close.
- examples.js: the catalogue the viewer navigates, the family strip, and the
  routing on a click, on load and on hashchange. A viewer address selects the
  filter of its own category, so closing lands there, and a browser without
  <dialog> shows that gallery rather than the top of the page.
- ShowcaseSiteGuardTest: card, family and category ids are unique and stay
  readable in an address, and a viewer address written into a page must name a
  family and a document it holds.
- scripts/site/gallery-viewer.test.mjs: 17 cases over the addresses, the
  navigation and the dialog, the last four run against a DOM small enough to live
  in the file — the history entries an open and a move record, what closing hands
  back, the token that keeps a late preview off a newer document, and the focus a
  disabled button must not take out of the dialog. The guard job runs every
  harness in scripts/site/, so a second one cannot sit there never running.
…y holds

The viewer moved only on its two arrows and the arrow keys. On a phone that left a
44px target as the only way forward, nothing on screen said what the other 26 CVs
looked like, and every step waited on a page image that had not started loading
until the reader asked for it.

A drag across the page now moves a document, a strip under the page holds every
document of the family, and the pages either side are fetched before they are asked
for. The strip marks the document on screen and follows it, whether the reader got
there by a drag, an arrow or the strip itself.

- gallery-viewer.js: the drag is Pointer Events, touch and pen only — a mouse has
  the buttons and the keys. One pointer at a time, so a second finger is a pinch and
  not a swipe; nothing from a pinch-zoomed page; at least max(48px, 15% of the
  stage) across and more across than down; and none of it from within 24px of the
  screen edge, where the drag belongs to the browser's own back gesture. The click
  the browser sends after a drag is swallowed, so a swipe never also presses what it
  ended on. The strip and the family chips share one keepInView, which stops
  animating for a reader who asked for less motion.
- The thumbnails point at the card previews the gallery behind the dialog already
  loads, and files sized for a 46px slot belong with the catalogue they are generated
  from. A reader who has asked to save data gets no strip at all: a row of whole pages
  is the one thing that mode is asking not to download, and the pages either side are
  not fetched ahead either. The strip is a single tab stop, on the document shown, so
  a keyboard reader reaches the page's own links without passing 27 thumbnails.
- index.html, styles.css: the strip, and `touch-action: pan-y pinch-zoom` on the
  stage so a vertical drag still scrolls and a pinch still zooms.
- scripts/site/gallery-viewer.test.mjs: eleven cases more, run against the same stub
  DOM, which grew a stage with a width, a strip with a row to scroll and a clock the
  test drives. The strip is the family's, marks one document and is a single tab stop;
  a thumbnail replaces the history entry rather than adding one, and the one already
  shown asks for nothing; the neighbours are fetched ahead, and neither they nor the
  strip in data-saver mode; the document shown is brought into view, and not smoothly
  for a reader who asked for less motion; a short, vertical, two-fingered, pinched,
  moused or edge-started drag is not a swipe, and one that only clears 48px is not a
  swipe on a wide page either; and the click a drag leaves behind presses nothing,
  inside a window that shuts on its own timer.
A card carried a title, a description, tags, its files and a link to its source. That is
enough to draw it and nothing else: the site could not say which preset renders a document,
what a reader needs on their classpath to run it, how many pages it has, or how large its
preview is. The viewer's strip and the family tiles, both of them small, pointed at the same
whole-page previews the cards use.

- ShowcaseMetadata: a card now carries what kind of card it is, the repo-relative path to
  its source, the artifacts a reader needs, and — where its example builds exactly one
  template preset — that preset and the model it composes, plus the card it re-renders with
  different options. The source link and the path are built in one place, so they cannot
  drift apart. A card in a template category is an example needing the engine alone until
  the preset pass promotes it: three of them build no preset at all, and claiming one, or a
  module they never touch, is a line a reader pastes into their own pom. A feature card that
  does build one stays a feature card — invoice-http-stream builds ModernInvoice only to
  have a document worth streaming.
- ShowcaseSync: the manifest gains a schemaVersion, and each card the preview's pixel size
  and the document's page count — both read from the render already being done — beside the
  register's new fields. Every control character is escaped as \u00XX, not just the five with
  short forms; a raw one is refused by the page's own fetch. Each document also gets a 320px
  thumbnail under showcase/thumbnails/, halved a step at a time, because one bilinear step
  from a page that wide samples too little of it and leaves fine strokes aliased.
- ShowcasePresetRegistrationTest: holds each card's preset and model to the example that
  builds them, read from its source rather than from the register, so a renamed preset or a
  card carrying its neighbour's cannot reach the site. What a card says it is and what it
  asks for are checked in both directions. It found two feature cards asking a reader for the
  engine alone while rendering a template preset.
- ShowcaseSiteGuardTest: fails on a manifest with no schemaVersion or one written to a
  contract the site does not read, a card whose measurements are not its preview's, a page
  count below one, a thumbnail that is not published or is no longer thumbnail-sized, and a
  preset count in the page copy the catalogue does not hold — 26 CV presets and 15 letters,
  counted as distinct presets, since a variant re-renders one rather than adding one.
- examples.js, gallery-viewer.js: a card declares its preview's real width and height, so the
  image reserves its slot at the right shape instead of appearing out of nothing; the strip
  and the family tiles read the thumbnails. Opening the CV family fetches 1.4 MiB of strip
  images where it fetched 5.2 MiB.
- The catalogue is regenerated. Five documents are published with their previews, all of
  them carrying right-to-left text, each proven to differ from the committed copy inside its
  page streams while rendering byte-for-byte the same twice over; the decks of the two that
  have one go with them. The other 112 documents and 6 decks are restored: every PDF differs
  run to run only inside /ID, and every deck only in the zip's modification time.
The published site stated the version in five places that inherit from no pom, and the cut
rewrote the first match of each. That is two failures waiting: a page carrying a second
install snippet keeps it a release behind while every check passes, and a spot that stops
matching — renamed, reformatted, moved — is skipped silently, so the cut reports success and
publishes a page still naming the previous release. The guard read four of those spots, also
first-match, and never read the Maven Central download link at all.

- index.html: a `release-context` JSON block holds stableVersion, releaseTag and javaMinimum.
  The JSON-LD and the install snippets still repeat the version, because a crawler and a
  reader with no JavaScript both have to see the right release; what changes is that they are
  now copies of something, and every copy is held equal to it.
- cut-release.ps1: the bump covers seven spots and every occurrence of each, and a pattern
  that matches nothing throws, naming the spot, instead of leaving it behind. Its no-change
  notice also spent its life reporting nothing — `"$tag?"` reads as a variable called `tag?`,
  so a maintainer re-running a cut was told the page was "already ()".
- release-script-check.yml: the function is lifted by AST and run on a fixture whose second
  Gradle snippet starts a release behind. Eight tokens move, a second run changes nothing,
  and a renamed spot refuses without writing.
- VersionConsistencyGuardTest: every occurrence of all seven spots is held to the release,
  including the downloadUrl, and a shape that has vanished fails rather than passing for
  want of anything to check. The showcase snippet pattern arrays go with the first-match
  checks that used them.
- web/README.md and docs/contributing/release-process.md say where the version lives, that
  every occurrence moves, and that a vanished spot stops the cut.
The gallery showed a document and a link to its source, and stopped there. A reader who
wanted one of their own had to guess the coordinates, find the class to call, and work out
which artifacts that particular document needs — and the catalogue could not have told them
correctly: of the 117 cards, 53 named a dependency set that compiles and then throws on a
missing font, ten left out a backend the card needs, and two offered a command that cannot
run at all.

- The viewer carries a panel under the document: the Maven and Gradle coordinates at the
  release the page names, the preset class and the record it composes, the family's worked
  snippet, the command that runs the example, and the runnable source and the family's guide
  at the release tag. A card that builds no preset is offered as a runnable example and makes
  no preset claim. With no release context it names no coordinates at all rather than a
  version it does not know.

- The snippet is the block `DocumentationSnippetCompileTest` already compiles, copied into
  the manifest at sync time: the site is served from `web/` alone and cannot read a page
  under `docs/`, so the alternative was hand-written code on the one page that must not be
  hand-written. The page that teaches this path, `using-templates.md`, taught it in ellipses
  — `CvDocument doc = …` — and is now real code behind that marker.

- A document drawn in a bundled face cannot be reproduced from `graph-compose` and
  `graph-compose-templates`: it compiles, then throws `Bundled font resource not found` at
  the first glyph, because those faces left the engine in v1.8.0. The artifact carrying them
  keeps its own version line — Central has 1.0.0 and 1.1.0 against an engine at 2.4.0 — so
  naming it at the release version is a 404, and the aggregate is the one coordinate correct
  at the version the page shows. Whether a document needs it is measured from the PDF, not
  declared: it differs card by card inside one family, 25 of 27 CVs and 4 of 7 invoices.

- Fonts were not the only omission. Ten cards reach a second backend and asked a reader for the
  engine alone, which ends in `MissingBackendException` at render. Two name the DOCX backend in
  an import; nine need the PPTX one, which is discovered by format and so appears in no source
  at all — eight of those publish a deck beside their PDF, four of them rendered by a sibling
  class, so keying on the example's own text would still have missed them. The requirement now
  follows from what a card publishes as well as from what its example names, and where a
  document also needs the bundled faces the aggregate stands in for the engine and templates
  without swallowing the backend beside it. `table-advanced` and `transforms` have no `main` —
  `GenerateAllExamples` renders them — and were offered an `exec:java` command that answers
  "doesn't contain a main method"; they now say what renders them instead.

- Every claim is checked against the example's own source or its rendered document, in both
  directions: `ShowcaseBundledFontClaimTest` for the faces, `ShowcaseCardInstructionsTest` for
  the backends and the run command, `ShowcaseSnippetScopeTest` for whether a published snippet
  even comes from a page the compile gate scans, and `ShowcaseSiteGuardTest` for a snippet that
  has drifted from its block, a `PRESET` card missing what the panel shows, and a family guide
  that is no longer a page. `s9-cv-templates` renders a CV from the published aggregate with
  nothing else installed, which is how the first of these defects surfaced.
…k its room

Two things a reader could not do in front of a document. A multi-page one could only be judged
by its first page or by downloading the PDF — 33 of the 117 documents have more, up to eight.
And the panel added under the document took 42% of the viewport against the stage's 21%: an
A4 page rendered at 104x147, which is a thumbnail with a dialog around it.

- The panel is collapsed behind a disclosure and opens when a reader asks for it, staying open
  for the rest of the session so paging through a family does not re-collapse it on every
  document. Collapsed, the stage holds 54-71% of the viewport across 360/390/768/1440 against
  the 21% that shipped; opened it holds 24-26%, still more than the regression left it by
  default. Its cap came down from 42vh to 30vh (34vh on a narrow screen) because the toggle
  costs room too, and at 42vh the opened panel left the stage 149px — smaller than the state
  it was added to fix. Opened, the panel is now a little larger than the stage (273px against
  219px at 910): that is a reader's deliberate choice, reversible in one click, where the
  regression was what everyone got without asking.

- Every page after the first is published at 1.0x into showcase/pages/<category>/<group>/, named
  as a reader counts them: page 1 is the existing preview, so these start at -2. 70 images,
  3.66 MiB. The scale follows what the viewer displays — a page renders at about 374 CSS pixels,
  so 595px covers it and 1.5x would publish pixels nothing shows; measured across all 33
  documents, the same pages cost 6.27 MiB at 1.5x and 2.47 at 0.75x, which is already soft
  against that stage. "pages" joins the subtree list ShowcaseSync clears, so a renamed example
  cannot leave 70 orphans reachable by URL and absent from the manifest.

- A row under the stage pages through them: "Page x of N" with its own small steps, deliberately
  unlike the large document arrows either side of the page, so the two kinds of movement never
  read as one control. Previous and Next, the arrow keys and a swipe keep moving between
  documents. The page index lives where every change of document passes and a redraw does not,
  so moving on starts the next document at its own first page. Paging reuses the load token, so
  a slow page cannot land under a document the reader has already left, and the alt text stopped
  telling a screen reader "first page" on page five. The page after the one on screen is warmed,
  behind the same data-saver gate as everything else the viewer fetches ahead.

- The guards cover what the browser would otherwise be the only witness to: the panel's height
  budget and that its disclosure is not a growing flex item (at `flex: 1 1 100%` the button asked
  for the whole dialog and left the stage 40px); every family guide the panel links being a page
  that exists, keyed to a family the catalogue has, with its #fragment answering to a heading;
  and every published page being a file, one per page the card declares. 16 checks in
  ShowcaseSiteGuardTest, 46 cases in the viewer harness — including that a swipe still moves
  documents once a document has pages, which until now was only true by construction, and that
  stepping to a single-page document hands focus on before the page row disappears.
// A cap in pixels is not a budget against the document: on a short window it is
// the whole dialog, and this guard could not read it as too tall either.
tooTall.add(cap + " (not measured against the viewport)");
} else if (Integer.parseInt(viewport.group(1)) > PANEL_HEIGHT_CEILING) {
The site's HTML was hand-maintained beside a catalogue that already knew everything in it. A new
example meant hand-editing three places in the page and hoping they agreed: the index a visitor
without JavaScript gets, the JSON-LD a crawler reads, and the preset counts written out in prose.
They did not agree — that index listed 38 of the 117 documents the site publishes, so two thirds of
the catalogue reached a reader with no JavaScript as nothing at all.

- `web/index.html` and `web/sitemap.xml` are now rendered from `web-src/` by
  `scripts/site/build.mjs`. Nothing else in `web/` is touched: the stylesheet, the scripts, the
  assets and the whole `showcase/` tree stay static, so the check below compares two pages instead
  of diffing a 25 MiB tree. GitHub Pages keeps serving the folder exactly as committed.

- What the page says now comes from data. The no-JavaScript index carries **every** document, under
  its category and group, with the category headings keeping the ids the menu and the sitemap
  resolve to. The preset counts are computed by the same rule the site guard counts them with —
  distinct preset classes within a group, so a card that re-renders another's preset is not a
  second preset. The JSON-LD item list and the sitemap's documents keep their editorial names in
  `web-src/data/featured.json` while their URLs are resolved from the manifest, so a renamed PDF
  cannot leave a crawler pointed at nothing.

- The release version is written down once, in `web-src/data/release.json`, and the build injects
  it into the seven spots on the page that inherit from no pom. `cut-release.ps1` moves those two
  values and rebuilds the pages rather than rewriting them, which a later build would undo. The
  rebuild runs after the catalogue sync, not beside the version bump: the pages are generated from
  `web/examples.json` too, and a rebuild before that sync would render them from a catalogue the
  cut then replaces — publishing an index missing whatever the release added. It also sits before
  the verify gate, so the site guards read the pages the release actually ships. The release commit
  stages both pages and the data together, since staging one without the others tags a release
  whose pages and source disagree, and the post-release flip back to `/blob/<branch>` rebuilds and
  stages them the same way. `VersionConsistencyGuardTest` is unchanged — it reads the built page,
  so it now holds generated text.

- `scripts/site/build.test.mjs` fails when what is committed under `web/` is not what `web-src/`
  builds, and CI's guard job already runs every harness in that folder. The build refuses rather
  than publishes: a token the build does not produce, a value no template uses, a featured id
  naming no card, an empty featured list, and a card with no title or PDF each stop it by name —
  a missing title used to reach the page as the literal word "undefined". Line endings are always
  LF and the comparison is LF-normalised, so the check reads the same on a checkout that converts
  them.

Verification: the site harness is 13 cases; `ShowcaseSiteGuardTest` (16), `VersionConsistencyGuardTest`
(23) and the six guards that read the release script are green — 63 tests. The new refusals were each
watched failing for their own reason: a page edited by hand, a token renamed, a token dropped, a
featured id naming no card, and a count written into the template past the catalogue — 5 of 5. The
release function was lifted by AST and run: both values move, the non-version field stays, a second
bump is a no-op, and a renamed spot refuses by name and writes nothing. In the browser the
regenerated page states v2.4.0 in the badge and the release block, keeps its filter pills, carries
117 links and 3 + 25 headings in the no-JavaScript index, escapes `Lists &amp; Bullets`, keeps the
7 JSON-LD items and the viewer dialog, and logs no errors. The full reactor gate is green: 13
modules, BUILD SUCCESS, 782 core and 1779 qa tests among them, no failures.
…newcomer knows

The home page opened on a heading four lines tall beside three cropped, overlapping previews, and
on a phone it hid them altogether, so the first screen was all words. The guide to authoring
templates sat above the catalogue, ahead of anything a visitor could use. And the catalogue's
categories were named after how the examples module is organised — Features and Flagships — which
tells someone arriving nothing about what they will get.

- The menu is Templates · Examples · Documentation · Releases · GitHub, with a Get started button.

- The hero is a short heading, one sentence, the Java minimum and the release, Get started and
  Browse templates, and one whole document with an Invoice / CV / Proposal / Report switch. Each
  entry is a catalogue card named in `web-src/data/featured.json`, and the build reads its preview,
  size, PDF and viewer address from that card, so the hero cannot point at a document the site does
  not publish. The first document is on the page as built, so a reader without JavaScript sees it
  and can open its PDF.

- `web/home.js` shows the switch, and shows the link into the viewer only once `examples.js`
  announces that the viewer exists (`gallery-viewer-ready`): the viewer needs the catalogue and
  `<dialog>`, so a link shown when the script merely loaded would change the address and open
  nothing whenever the catalogue failed to load. A choice changes the picture, its caption and both
  links together once the new preview has decoded, and a preview that becomes ready after a newer
  choice is dropped; swapped at once, "Open PDF" pointed at the new document while the old picture
  was still on screen. `.hero-visual [hidden]` keeps both hidden until then — the stylesheet's one
  `[hidden]` rule was scoped to the viewer, and `display:flex` would have shown a switch that swaps
  nothing.

- The heading's size follows the column it sits in, so it stays within two lines at every width
  from 320 to 1440 pixels; beside the document a fixed size had wrapped it to four lines at 1000 and
  1024 and to three up to 1180. A phone gets one compact document instead of none, and on a short laptop screen
  the document gives up width so that it, its caption and the switch stay in the first screen.

- What visitors read is renamed and nothing they link to is. Features are listed as Examples, the
  large complete documents as Showcase, and their group as Complete documents; those cards' visible,
  searchable tag is `showcase` rather than `flagship`, and three descriptions stop calling a
  document a flagship or a report a ready-to-ship template. Category ids are unchanged, so published
  paths, shared viewer addresses and the `#showcase`, `#templates-section`, `#features-section`,
  `#flagships-section` and `#install` anchors keep working. The gallery section drops the visible
  word Showcase so that it names one thing, the search box is labelled for the catalogue, and the
  JSON-LD drops "(flagship)".

- The authoring guide moves into a Documentation block with its three links, the documentation
  index and the latest release, and the PowerPoint note moves from the hero into the feature grid.
  Inline code in those blocks was drawn in a colour set for the dark code panels and all but
  vanished in the light theme; it now follows the text. Long unbreakable calls no longer widen the
  feature cards past a 320px screen. The selected switch option keeps full text contrast, with its
  state carried by the border and tint. A visually hidden heading restores the page's heading order
  and gives its section the label its `aria-labelledby` already named. The rules for the fanned
  stack and the old callout are gone.

Verification: the site harness is 18 cases and `home.test.mjs` 12, run against the hero parsed out of
the built page; the viewer harness is green. Every new check was watched failing for its own reason
against the final code, 12 of 12: the viewer link shown before the viewer exists, the caption
swapping before the picture, a late preview applied over a newer choice, the viewer address left
behind on a swap, the figure hook renamed to a name still containing `data-hero`, the link moved out
of the figure, the event renamed in `examples.js`, the page not loading `home.js`, the switch no
longer hidden by the build, swapped address segments, the PDF link rendered hidden, and a switch
label used as a title. In the browser, with the fonts loaded, the heading holds two lines at 23
widths from 320 to 1440; there is no horizontal overflow from 320 up; inline code reads at
12.6–15.4:1 in both themes; and the switch ends inside the first screen at 1280×720 and 1366×768.
The examples module is 93 tests green, and the full reactor gate — `ShowcaseSiteGuardTest` and
`VersionConsistencyGuardTest` among it — is BUILD SUCCESS with 782 core and 1779 qa tests.
…utput format keeps

The install section offered one coordinate, the engine, whatever a reader had come for. Someone after
a template was handed a coordinate that renders none — and `graph-compose-templates` on its own brings
neither the PDF backend nor the fonts most presets are drawn in. Someone after a deck or a Word file
was handed nothing, and nothing on the page said what PowerPoint or Word keep of a document and what
they lose.

- The install section offers four scenarios, each as a labelled Maven and Gradle (Groovy DSL)
  coordinate at the release the page names: a PDF from your own layout (`graph-compose`, the engine
  and its PDF backend); ready-made templates (`graph-compose-bundle` — the engine and its PDF
  backend, the CV, cover letter, invoice, proposal, receipt and rota templates, the bundled fonts and
  colour emoji, with the independently versioned companions pinned for the reader); and an editable
  PowerPoint deck (`graph-compose-render-pptx`, Beta) or a Word document
  (`graph-compose-render-docx`), each added beside `graph-compose` or the bundle. The Gradle lines are
  labelled Groovy because the single quotes they carry do not compile in a `build.gradle.kts`.

- An output formats block sets PDF, PowerPoint and Word side by side, with the limits a reader should
  know before choosing, taken from the backend capability matrix it links to. PDF is the reference:
  fonts you register or take from the bundle are embedded and subset (the standard fonts, the default
  Helvetica among them, are not), links, bookmarks and metadata are written, a document can be
  encrypted, and the deterministic mode renders byte-identical files. PowerPoint is geometry-identical
  with most vector drawing as editable shapes; a clip that would cut content, and inline SVG with
  clips, exact stroke styles or art outside its view box, become pictures; distinct per-corner radii,
  numeric dashes and radial gradients are approximated; there is no encryption, and only the first
  bookmark on a slide survives, as its name. Word gets real paragraphs, tables, block images, and
  headers and footers with live page numbers from page zones — but not the text header and footer
  (`DocumentHeaderFooter`), shapes, lines, barcodes, inline images, links or watermarks; it names its
  fonts without embedding them, and ignores hanging indents. The PowerPoint card the feature grid
  carried says nothing the block does not, and is gone.

- `scripts/site/build.test.mjs` requires every scenario's coordinate in both forms, each at the
  release, and refuses a fonts or emoji coordinate outright. `VersionConsistencyGuardTest` reads the
  bare `graph-compose` coordinate alone, so a stale bundle or backend snippet would pass it; and
  requiring both forms is what catches a snippet the pattern can no longer read — a line slipped in
  between artifactId and version — which would otherwise go unchecked while its other form passed.

- Three defects this markup leans on are fixed with it. Muted text in the light theme sat at 4.45:1
  on the page background, under the 4.5:1 minimum for body text, and now reads at 6:1. The featured
  tiles' titles were an `h5` straight under an `h3`, and are an `h4`, with their size and weight
  unchanged. And a menu link to a section landed its heading under the sticky header — at 94px under
  a 141px header on a phone — and now lands below it, at every breakpoint where the header changes
  height.

Verification: the site harness is 19 cases, `home.test.mjs` 12 and the viewer harness 46, all green.
The coverage case was watched failing for each of its reasons, 4 of 4: a stale bundle version behind
a line the pattern cannot read past, a Word coordinate a release behind, a fonts coordinate offered
at the release, and the PowerPoint Gradle form removed. In the browser there is no horizontal overflow
at 320, 390, 768, 1024 or 1440; the scenarios sit in one column and then two, the formats in one, two
and three; no heading level is skipped anywhere on the page; muted text in the light theme reads at
6.03–6.46:1; and a menu link lands its heading at 243px below a 141px header on a 390px phone, at 273px
below 146px at 768, and at 207px below 79px at 1440. The full reactor gate is BUILD SUCCESS, with
`ShowcaseSiteGuardTest` and `VersionConsistencyGuardTest` green against the final page.
A document could be seen in the viewer and nowhere else. The viewer lives behind JavaScript and a
fragment address, so a reader without a script got a bare PDF link, a crawler got the PDF and the
home page, and no address led to a document's pages together with what reproducing it takes.

- Each of the 117 documents gets a generated page at `<category>/<family>/<id>/`, the same three
  segments as its viewer address: every page of the document as an image at its own pixel size,
  each linking into the PDF (at that page, in the viewers that read `#page=`); the PDF, and the
  deck where one is published; what reproducing it takes; and the other documents of its family.
  Each page has a canonical address, a description, Open Graph tags and a JSON-LD block, and the
  sitemap lists them all beside the URLs it already had.

- What a page tells a reader is not written a second time. The viewer's panel logic moves out of
  `createViewer` into a pure `panelModel(card, catalogue, release)` in `gallery-viewer.js`, with
  `pagePath(route)` as the one place a page's address is formatted. The viewer draws its panel from
  the model, and `scripts/site/build.mjs` loads the same script through `node:vm` to render the
  page's section. The site header, footer and theme scripts become partials shared by the home page
  and the document pages; the home page came out byte-identical from that step alone.

- The build owns only what it wrote where it sits. A document page carries a generator mark, and the
  build counts a page as its own only when that page's canonical address also names the place it
  sits — a generated page copied elsewhere to start a page by hand keeps the mark, and is not the
  build's to delete. An owned page no card builds any more — a card renamed, moved or removed — is
  deleted with the directories it leaves empty, and `--check` fails on a page that is missing, stale
  or orphaned without changing the tree. A card id that is not address-safe, a category that would
  write into `assets/` or `showcase/`, two cards with one id, and a page image that is not a
  published PNG stop the build before it writes.

- Links in. The no-JavaScript index links each document's page rather than its bare PDF, and each of
  its family headings carries the family's viewer address as its id, so a page's family link — which
  opens the viewer where a script runs — lands on that family's list where none does. The hero's
  second link goes to the page of the document on screen, so it works without a script, and the
  event `home.js` waited on to reveal a viewer link is gone from both sides. The viewer gains a
  Details link beside Open PDF, and the JSON-LD item lists name pages instead of PDFs.

- The pages exposed two faults in the panel, fixed where both views read them. A family's worked
  snippet composes one preset, yet it was captioned "Compose it" on every card of the family: Blue
  Banner's reader was promised code that builds `BoxedSections`. Only the card of the preset the
  snippet calls `create(` on, matched as a whole name, says so now; the others say the snippet
  comes from the docs, and where it is published on a page other than the family guide — the CV
  snippet is on `using-templates.md`, the family starts at the quickstart — they link that page as
  well. And every value was set in monospace, sentences included; class names and paths are
  marked as literals and set as code, prose is not. The viewer also stops captioning every document
  "First page shown", which was untrue of the 33 documents it pages through.

- `ShowcaseSiteGuardTest` finds the generated pages instead of reading a fixed list, resolves each
  link from the page's own directory — keeping the trailing slash that makes `../cv-b/` a page,
  refusing a root-relative link or one that climbs out of `web/`, and reading a link that is not a
  valid URI the way a browser does rather than failing on it — and checks an anchor against the page
  it points into, where a `#page=` after a PDF is not an anchor. The release cut stages the pages with
  the glob pathspec `:(glob)web/*/*/*/index.html`, in the release commit and in the post-release
  one, added only where the site build exists because a pathspec matching nothing fails the add; a
  new step in `release-script-check.yml` runs the function the cut calls over a rewritten, a
  deleted and an added page and fails if it stages anything else.

Verification: the site build harness is 39 cases, the viewer harness 53 and the hero harness 10,
all green. Each new check was watched failing for its own reason: 28 of 28 sabotages of the build,
the page template, the viewer and the hero were caught by the case they target, with its message;
a broken stylesheet link, a section anchor and an anchor into another page planted in one generated
page, and a guard that crashed on an invalid URI, were each reported by ShowcaseSiteGuardTest (19
green); and the staging step, run in a scratch repository, passed on the real function and failed
on a pathspec one level short, on one reaching `web/showcase/`, and on a post-release commit that
stopped staging pages. Dry runs of the cut and of -PostReleaseOnly show the page pathspec in both
`git add` lines and leave the tree untouched. In the browser, all 117 pages show no horizontal
overflow at 320, 360, 390, 768, 1024 and 1440 pixels — after the fix for 11 feature pages whose
descriptions are API calls with no break opportunity — text on a page reads at 6:1 or better in
both themes, Details, Back and a page's family link each land where they name, and with scripts
disabled a family address resolves to that family's heading and scrolls it below the header. The
full reactor gate is BUILD SUCCESS.
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.

2 participants