Skip to content

re:builder - #162

Merged
julesduroux merged 77 commits into
mainfrom
preprod
Oct 23, 2026
Merged

julesduroux merged 77 commits into
mainfrom
preprod

Conversation

@julesduroux

Copy link
Copy Markdown
Collaborator

No description provided.

Yutsa and others added 30 commits October 6, 2026 10:46
…ership searches (#131)

* Re:Builder: keep unique illustrations, fix collection and digital ownership searches

- ac-card-art compares its source list by value: a unique whose card object is replaced once its
  printed effect arrives kept the same <img> src, so `load` never fired again and the art stayed hidden.
- The collection / ownership searches send list filters as `key[]`: PHP kept the last value of a
  repeated `rarity=…`, so the default search asked for Exalted cards of one type and one set only.
- collection-search.php forwards those lists to the collection API in its array form too
  (cacCollectionQuery, tested in tests/CollectionQueryTest.php).
- The stack's ownership mock answers GET /api/collection with a few filtered cards, for the e2e test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Simplify: reset card art on its displayed src, share the owned query rationale

- ac-card-art resets `loaded` when the displayed src changes rather than the source list.
- ownedParams' doc carries the `key[]` rationale once; collection query loop without array_values.
- e2e: the cards API answer waits for the shown illustration instead of a fixed delay; the digital
  collection test also runs on mobile (« Plus » menu); screenshots for the pull request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* e2e: check the physical collection search too, against the stack's mock

The stack's collection API (COLLECTION_API_URL) is the ownership mock, which also answers
GET /api/collection with the collection API's filter names (cardType, cardSet): the real API only
takes altered.re tokens. The collection test fails with the previous client or the previous proxy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Stack: start the decks API with the latest Mercure

The decks API image is built on the latest FrankenPHP base, whose Mercure now refuses publisher_jwt /
subscriber_jwt outside compatibility mode: the container exited at startup and the e2e job failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…a token or a hero (#132)

* Re:Builder: number alt-art copies 1, 2, 3, one unnumbered marker for a token or a hero

The markers showed `slotIndex + 1`: a token's single copy showed « 2 » in production (the ownership
API's slot indexes most likely start at 1). They are now numbered by position; a family with one
copy (token, hero) has one marker with a check and no number, and the first copy moves first, as on
the site's widget. The stack's ownership mock gives tokens one copy, as the service does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Simplify the copy numbers of the alt-art markers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Simplify: number the copies once, fewer marker assertions

- alt-art-slots numbers the copies once per render instead of once per illustration; the pointer
  cursor is set on the button markers only.
- e2e: the token markers are checked with two locator counts instead of two assertions a marker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… list (#133)

* Re:Builder: check several effect values in a row, without reopening a list

The effect editor's three criteria (Quand · déclencheur, Si · condition,
Alors · effet) used a combobox that closed after each pick. They now use a
checkbox list that stays open:

- Window: the criteria as cards on the left (values one per line, « ou » on its
  own line), the checkbox list of the selected criterion on the right, at a
  fixed height so checking never moves the list.
- Compact: each criterion shows its values and an « Ajouter un … » add button
  that opens the list as a step of the same sheet, over its whole height.
- A list is narrowed again only when the two other criteria change (the
  server already leaves the narrowed criterion out), so checking values does
  not reshape the list being checked.

New components: ac-check-list, ac-or-values, and the ac-ability-picker step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: duplicate an effect from its summary

A copy button between edit and delete on each effect summary puts a copy of
the effect right after it, with its own id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: share the list helpers of the effect editor

- option-list.ts holds normalizeSearch and groupStarts, used by ac-combobox,
  ac-check-list and ac-select (which had its own copy of the search folding).
- One _glyph.scss partial for the Altered Icons glyph of the three lists.
- ac-check-list's search field uses the design system's ac-input-icon.
- The effect editor builds its criteria from one text table, reads the
  picked values through one helper, and renders the intro and the load
  error from a single template each.
- newEffectId() for the copy of a duplicated effect; ac-combobox drops its
  emptyPlaceholder input, unused since the effect editor stopped using it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
)

Uniques are cards whose text is read. On a 2560 px window the results grid
showed 8 of them per row, too small to read. The Uniques grid now caps at 7
columns: the cards only grow where 7 would be narrower than the usual 210 px,
so 1920 px (5 per row) and 1440 px (3 per row) do not change, nor do the other
tabs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Re:Builder: the whole deck at a glance from 768 px, read and edit

Deck bar above the deck page and the editor: hero art with a faction border,
the name on two lines at most, format and legality, counts per type, cost
curve (hand and reserve), terrain totals, and every action at hand. The
editor's search and starting hand get a one-row version with a mini curve
and a « ⋯ » menu.

« Aperçu » is now a board of piles, one column group per type, like the
deck image: every group has the same row count, the smallest that fits 12
card columns. In the editor each pile has a stepper on hover or focus; a
card brought to 0 stays, faded, until the view is left.

My own deck opens in the editor on « Aperçu »; « Terminer » goes back to
the deck list. Someone else's deck opens on its read-only page. The deck
panel keeps its card list only (summary and stats are in the bar) and
starts hidden on « Aperçu ». Phones are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: deck board fills the window's width, copies of a pile further apart

The number of card columns follows the board's measured width (150 px a
column at least), so a wide window gets fewer rows; cards grow up to 260 px.
Each copy behind the front one is 16 px lower, to count them at a glance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: deck settings button stands out on any hero art

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: deck board cards stay readable, 150 px wide at least

The column count now comes from the narrowest card allowed, counting each
group's padding: a deck with Uniques takes more rows instead of shrinking
its cards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: editor deck panel as tall as its cards, the window at most

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: filters panel ends at the window's bottom under any header

Its height left out a fixed 136 px, the old editor header; the deck bar is
taller, so « Réinitialiser » and « Rechercher » were below the window. The
panel now measures how far it sits below its sticky position.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: a click anywhere on an effect criterion opens it

« Quand », « Si », « Alors »: only the title row was a button, the values
below it (« Tous les déclencheurs »…) did nothing. The title's button now
covers the whole card; the values' own buttons stay on top.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: more room around the deck bar's buttons

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: deck settings show the hero's illustration, not its card

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: disable the filters the Favoris tab cannot apply

The site's favorites search filters its table by faction, set and rarity
only: the name, cost, power, type and advanced fields did nothing there,
with a chip as if they did. On Favoris they are now disabled and faded,
under a note, and give no chip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: a duplicated deck opens in the editor, on « Aperçu »

From the deck page (someone else's deck or one's own) and from the editor's
« Dupliquer en local », as « Dupliquer » in the editor already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: review fixes on the deck board and the filters panel

- The board keeps at 0 only the cards removed with a pile's stepper (or the
  card zoom), not every reference it showed: a print swapped in the zoom or
  by Global alt-art mode no longer leaves a faded duplicate behind.
- The filters panel measures its offset again each time it is shown from its
  tab, not only on scroll: reopened without scrolling, its buttons stayed
  below the window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: simplify the deck bar, the board and the shared deck actions

- The deck bar opens the hero zoom itself (no copy in each page) and renders
  its settings button once; overlines use the design system's
  .ac-text-overline.
- deck-actions.ts holds the image source and the delete message, used by the
  deck page and the editor.
- The board sorts its groups only when the cards change; a resize only
  recomputes the column count. Decklist order is one shared comparator.
- Deck cards take a boolean for the editor link, not a route array built
  for every card on each change detection.
- The filters panel is measured once a frame at most, and not on phones.
- Split buttons share their menu positions with the editor's « ⋯ » menu.
- Dead labels, translations, Sass variable and duplicate rules removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ng a list (#136)

The hand calculators (« Choisir… », A, B) and the subtype filter closed their
list after each pick. ac-combobox gets a keepOpen mode, on for both:

- Each option has a check box; a press checks or unchecks it and the list
  stays open. Checked values stay in the list, in bold.
- Enter in the search field checks the first unchecked match and empties
  the field, as in the effect editor.
- The chips go under the button, so checking never moves the button or the
  list under the pointer.
- The listbox only gets the checked options it lists: the CDK listbox rejects
  a selected value it has no option for (one the search hides).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…erminer », physical collection fixed (#137)

* Re:Builder: « Mes decks » instead of « Terminer », hero settings in the search bar, collection search with several card types

- The editor's « Terminer » goes: « ‹ Mes decks » of the deck bar leaves the editor (shown in the
  reduced bar too), the deck is saved on the way out as before.
- The reduced deck bar (Recherche, Main de départ) keeps the settings button on the hero art:
  the hero can be changed without going back to « Aperçu ».
- The collection API fails (HTTP 500) on several cardType[]: the proxy sends the card type only when
  there is one and filters several itself. Re:Builder sends four types by default, so its
  « Collection physique » tab always failed on preprod.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: Rares, Exalteds and Uniques against the format's caps in the deck bar

Back from the deck panel's summary removed in #135, with the caps the site's builder shows: one badge
per capped rarity (12/15), grey below the cap, green at it, red above. Under « Puissance totale » in
the full bar, a column of its own in the reduced one; a rarity without a cap (Singleton, Sandbox) is
left out. The reduced bar drops its mini curve from 1320 px, the terrain totals from 1060 px and the
rarities from 900 px; the full bar keeps the rarities when the terrain totals go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… cardSet, HTTP 500 on several types) (#138)

#137 filters several card types in the collection proxy, on the items' cardType. The mock answered
with type / set instead, so the proxy dropped every card and the « Collection physique » e2e found 0.
The mock now names the fields as the collection API does, and fails on several cardType[] like it:
the e2e checks the proxy's own filtering.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…s app bar (#139)

* Re:Builder: on phones too, « Mes decks » opens a deck in the editor; « ⋯ » in its app bar

Like from 768 px, a deck of « Mes decks » opens in the editor on « Aperçu ». The deck page's actions
(image, copy the list, duplicate, delete) were reached through « Terminer », gone in #137: the
editor's app bar gets the deck bar's « ⋯ » menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: one element per app bar block, so « Partager » and « ⋯ » are both projected

Content projection only takes a control-flow block with a single root: with both buttons in one
@if, neither reached the app bar's actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…nly @mobile tests (#140)

The e2e job wrote its Docker layer cache from each pull request: a cache scoped to that PR,
unreadable by the others, so every new PR rebuilt both images (~3-4 min), and the repository
cache sat above its 10 GB limit. stack-images.yml now writes it on preprod (push, weekly,
manual); pull requests only read it.

On the CI, the mobile project runs only the tests tagged @mobile (bottom navigation, sheets,
app bar, touch sizes): 56 tests instead of 88. Locally both projects still run everything,
for pr-screenshots.mjs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…n a slow network (#141)

- SPA pages: a server-rendered placeholder (manifest `placeholder`) shown while the scripts load, kept by the
  host runtime until the plugin's first screen, replaced by an error notice if a module fails to load.
  Re:Builder's draws the screen of the URL (decks list, deck page, editor views) in its layout.
- Design system: `ac-skeleton` (text, title, control, control-sm, circle, card, panel), `[acSkeleton]` directive.
- DeckStore.opening: deck bar, panel, board, preview, list view, hand and deck page show skeletons in their own
  layout instead of « 0 cartes », « Deck vide », « Non légal ». owned() is unknown while the deck opens, so the
  actions stay in skeleton rather than « Modifier » appearing then disappearing.
- Card search waits for configure() (the deck's faction and format) before its first request.
- Decks list: skeletons at a deck card's size, « Chargement… » for the count.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Re:Builder: the hero portrait opens « Choisir un héros » (desktop deck bar and mobile summary)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: one portrait button in the deck bar, one hero picker config

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…app bar (#143)

* Re:Builder: phones keep the hero and the rarity caps in the editor's app bar

In « Recherche » and « Aperçu » on phones, the app bar of an editable deck shows the hero (it opens « Choisir un
héros ») and, under the deck name, the Rares / Exaltées / Uniques against the format's caps, what the deck bar shows
from 768 px. The bar stays on screen while scrolling; the condensed search head sticks below it. Its title opens
« Résumé du deck »: hero, legality, a bar per capped rarity, counts per type. « Partager » moves to the « ⋯ » menu
there.

Also: ac-progress--success / --danger and ac-badge--dense in the design system, a [titles] slot on ac-app-bar,
cached eager art (the hero after a tab change) shown without placeholder or fade, and shared helpers for the rarity
labels (rarityLimitItems), the « Non légal » label and the hero art.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* e2e: read the created deck's id from the editor URL, not from the POST's body

On the CI, Chrome had dropped the body of the deck creation response (the editor's images fill its buffer) by the
time the test read it: « response.json: No data found for resource ». The status is still checked on the response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… 21 s on 3G, no layout shift) (#144)

* Re:Builder: announce the page's modules in <head> (modulepreload), one round trip instead of five

The build manifest lists, per page slug and language, the chunks a page imports before it can draw
(esbuild metafile); the shell announces them with the entry modules and the shadow-root CSS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: the page's data sooner on a slow network

- Module preloads start once the page's stylesheets have loaded: the first paint keeps their bandwidth.
- main.ts requests the deck of the deck page and of the editor while the modules download; the app takes
  that answer (prefetchInterceptor) instead of sending its own request.
- Ownership check of the editor from papi/rebuilder/my-deck-ids (ids only, read server side) instead of
  downloading every deck of the account.
- JSON of the API relay and of the plugin endpoints gzipped (Debian's mod_deflate leaves JSON out).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: no layout shift when the app replaces the skeleton, less art and code ahead on a slow network

- Host runtime: the plugin's container and the server's placeholder share one grid cell until the first
  screen is drawn, so the placeholder is never pushed down (CLS ~1.0 -> ~0).
- Decks list skeletons keyed by the list's length: decks arriving above them never move them.
- Card art lookahead of half a screen on a 3G or slower connection (or data saver), instead of 2.5 screens.
- Test hand, odds and calculators (with the CDK's drag and drop) in their own chunk, loaded with their view.
- e2e: module preloads, one deck request, ownership from my-deck-ids, gzipped JSON, no layout shift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: the prefetch reads its answer in full; skeleton e2e tests hold the scripts for real

An unread error body kept the prefetched request open (no network idle on an unknown deck). The skeleton
tests now turn the browser cache off, so the held main module is not served from the memory cache, and wait
for the response only (a module script holds DOMContentLoaded).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: hashed build files cached for a year; A/B load-performance bench

- plugins/rebuilder/.htaccess: Cache-Control immutable on main-, polyfills-, chunk-HASH.js and the hashed
  fonts (mod_headers): a repeat visit loads no module from the network.
- tests/e2e/perf-ab.mjs: two versions of the site side by side on an emulated 3G / slow 4G phone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: request the modules right after the skeleton, not from <head>

Started once the head's stylesheets had loaded, the module preloads took the bandwidth of the rest of the
page and of the skeleton's stylesheet: the skeleton was painted ~0.3 s later than on preprod (screencast,
3G). The inline script now follows the placeholder, so it runs once the skeleton can be painted.
e2e: the ownership test waits for the save before reloading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* e2e: the held-scripts tests unregister the site's service worker first

Installed by the first visit, it answered main.js itself, out of the routes' reach: the skeleton test
failed one run in three. The ownership test waits for the app bar's hero on phones, where « Partager »
moved to the « ⋯ » menu (#143).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…e » lists the Uniques (#145)

* Re:Builder: a deck opens at the top of its page; Propriété numérique lists the Uniques

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: Propriété numérique keeps the Kickstarter set by default, steadier scroll e2e

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: one owner for the « légales » rule, page key from the active route

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ones included (#146)

* Re:Builder e2e: back from a deck puts the decks list where it was, phones included

On phones the list did come back at its offset; the test scrolled the deck card
under the sticky app bar, so Playwright's click scrolled the page back to the
top before the deck opened, and the list rightly restored 0. The new test
places the card just below the app bar and checks the exact offset on desktop
and mobile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder e2e: one scroll below the app bar, URL checks through at()

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… (focus, scrollIntoView and anchors no longer hidden) (#147)

* Re:Builder e2e: back from a deck puts the decks list where it was, phones included

On phones the list did come back at its offset; the test scrolled the deck card
under the sticky app bar, so Playwright's click scrolled the page back to the
top before the deck opened, and the list rightly restored 0. The new test
places the card just below the app bar and checks the exact offset on desktop
and mobile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder e2e: one scroll below the app bar, URL checks through at()

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Scroll offset under the sticky site header and the Re:Builder app bar

<html> gets scroll-padding-top: --ac-page-top + --ac-scroll-top-offset, so
keyboard focus, scrollIntoView, #anchors and find in page stop below the
sticky bars instead of behind them (WCAG 2.2 SC 2.4.11). ac-app-bar raises
--ac-scroll-top-offset on <html> while it sticks (shadow DOM: :has() cannot
see it) and gets a sticky input that replaces the editor's .static override.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder e2e: scrollIntoView puts the card below the sticky bars on its own

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ac-app-bar: offset only while a sticky bar is in the document

The Decks list stays mounted, detached, while a deck is open
(DecksListReuseStrategy): a lifecycle refcount kept its bar's offset after
leaving the list (desktop after a resize, editor search view). The bars are
now checked after every render, and only those in the document count.
Position override through a host style binding; shorter docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…fects; Collection physique lists its Uniques (#149)

* Re:Builder: the account tabs show the Uniques with their stats and effects; Collection physique lists its Uniques

- Propriété numérique and Collection physique: the APIs give a Unique's costs, not its effects (and the ownership
  API names its powers forest/mountain/ocean), so the Uniques showed 0 · 0 · 0 and no text. Their faces now come
  from the cards API batch, through one CardsApiService.uniqueFaces() shared with the editor and cached for the session.
- Collection physique offers the Unique rarity (on by default) and the « légales » filter, like the other account tabs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder e2e: the Unique of the account tabs is centered in its evidence

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…; a clear message when an alt art lacks copies (#150)

- Card zoom: the card shrinks to leave room for the copies, the illustrations and « Accéder au détail », which always show whole; window heights in dvh instead of vh (phone browsers' bars).
- Alt arts: picking an illustration once more than owned says « Vous n'avez pas assez d'exemplaires de cet art alternatif. » without calling the service; a 409 from the service says the same instead of the references and counts.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#151)

* SPA pages: the server's skeleton stays until the page draws, no empty frame in between

The runtime overlays the placeholder and the plugin container in one grid cell and removes the
placeholder once the container has some height. Grid items stretch to the cell, so the container
was as tall as the placeholder as soon as it was mounted: the placeholder went before the app drew
its page, and phones showed an empty frame between the two skeletons. The container now keeps its
content's height (align-self: start) while they overlap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* SPA pages: the first paint is the page's skeleton, not the site header over an empty page

The site's hidden menus (mobile menu, account popover, sidebar: ~27 KB) come between the header and the
mount point in the HTML, so the browser painted the header alone for 20-50 ms before it parsed the
server's skeleton. <head> now names the mount point in <link rel="expect" blocking="render"> (spaMountId()):
the first paint waits until the skeleton is parsed, at about the same time the skeleton used to show.
Browsers without render-blocking `expect` (Safari, Firefox) ignore it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… in Re:Builder

The ownership service's « Global » mode rewrote every deck with the global preferences, on the site and on
Board Game Arena (apply-to-deck), so a deck could not keep prints of its own. The settings page now sets
default alt arts only, and a deck stores the prints it uses:

- Settings (plugin ownership): one mode, « Arts alternatifs par défaut », for cards and tokens.
- A player still in « Global » is switched to « par deck » on their next visit (includes/func.alt-arts.php);
  their decks wait for their default alt arts until Re:Builder opens them (table alt_art_pending_decks).
- Re:Builder: a card added from the search takes the default alt art of its copy; « Arts alternatifs » in
  the search lists the alt arts the player owns after their card, added as shown; the brush of a card
  changes its prints for the deck; « Arts par défaut » applies the defaults to the deck; the token window
  shows each token large with its illustrations. Import, a copy of someone else's deck and a hero picked
  without « Alt arts » take the defaults.
- Local stack: docker-compose.ownership.yml runs the real AlteredOwnership service, with owned alt arts,
  default alt arts and a deck to switch from « Global ».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on phones, and systematic loading checks (#152)

* Re:Builder on phones: the deck's app bar goes from its skeleton to its final state; the app's styles before its first screen

- Editor app bar (phones): while the deck opens and its owner is unknown, a skeleton in the final layout (back
  button, hero, name over the rarities, save status slot, « ⋯ ») instead of « Chargement du deck… » then a plain
  title. The server's skeleton (placeholder.php) draws the same bar, back chevron included.
- Runtime: the plugin container stays hidden (in place) and the server's skeleton stays until the shadow root's
  stylesheets have loaded, so no frame shows the app unstyled (10 s fallback for a stalled request).
- The server's skeleton keeps the site's font through the shadow root (`:host { all: initial }` used to turn it serif).
- « Nouveau deck » on phones has an in-flow first screen, so its server skeleton goes away.
- Design system: `--ac-badge-height` and `ac-skeleton--badge`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder « Mon deck » on phones: nothing moves while the owner, the alt-art mode and the rarity icons arrive

- « Choisir les arts des jetons » keeps its place in skeleton while the deck's owner or the alt-art mode is not known
  (EditorAltArts.tokensChoice: null), in the app and in the server's skeleton: it used to push the list down when it came.
- Rarity icons carry their files' sizes (width / height): sized by their height only, they took no width until loaded,
  and the summary's rarity row wrapped late (+24 px).
First visit on a phone (CPU 6×, 4G, empty cache): CLS 0.095 → 0.002.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder editor: the quantity steppers from the start, not a read-only « ×1 » until the owner is known

DeckStore.steppersShown: the editor's rows, tiles and piles draw their steppers unless the deck is known not to be
the user's. A change still waits for editable() (setQuantity, and the board's own removed-cards list).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Loading checks for every Re:Builder screen: skeleton, styles, layout shifts, in the CI and as a timeline script

- tests/e2e/loading.ts: a recorder installed before the page loads (empty frame, plugin shown unstyled, skeleton font,
  forbidden texts, CLS with the moved boxes), throttling profiles, empty cache.
- plugins/rebuilder/e2e/loading.spec.ts: every screen (screens.ts), first and second visit, phone network, desktop
  and mobile; CLS budgets per screen (loading-budgets.json: 0.01, or today's value for a known shift, with its reason).
- tests/e2e/loading-timeline.ts (npm run loading:timeline): frame-by-frame image and report of a screen, two stacks
  side by side with --compare (before / after).
- First paint: <link rel="expect"> now names an element after the mount point (spaFirstPaintId()). Chromium paints
  once the expected element is inserted, and the parser could stop inside the placeholder: about 1 load in 25 showed
  the site header alone first; 0 in 90 since.
- AGENTS.md § Checks, the plugin's AGENTS.md and README.md say when to run them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Loading budgets: the CI's values for the editor « Aperçu » (desktop) and « Nouveau deck » (mobile, second visit)

The CI measures these known shifts above the local runs: the deck's owner and the page's footer come in another
order there. #154 removes them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#154)

* Re:Builder on phones: the deck's app bar goes from its skeleton to its final state; the app's styles before its first screen

- Editor app bar (phones): while the deck opens and its owner is unknown, a skeleton in the final layout (back
  button, hero, name over the rarities, save status slot, « ⋯ ») instead of « Chargement du deck… » then a plain
  title. The server's skeleton (placeholder.php) draws the same bar, back chevron included.
- Runtime: the plugin container stays hidden (in place) and the server's skeleton stays until the shadow root's
  stylesheets have loaded, so no frame shows the app unstyled (10 s fallback for a stalled request).
- The server's skeleton keeps the site's font through the shadow root (`:host { all: initial }` used to turn it serif).
- « Nouveau deck » on phones has an in-flow first screen, so its server skeleton goes away.
- Design system: `--ac-badge-height` and `ac-skeleton--badge`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder « Mon deck » on phones: nothing moves while the owner, the alt-art mode and the rarity icons arrive

- « Choisir les arts des jetons » keeps its place in skeleton while the deck's owner or the alt-art mode is not known
  (EditorAltArts.tokensChoice: null), in the app and in the server's skeleton: it used to push the list down when it came.
- Rarity icons carry their files' sizes (width / height): sized by their height only, they took no width until loaded,
  and the summary's rarity row wrapped late (+24 px).
First visit on a phone (CPU 6×, 4G, empty cache): CLS 0.095 → 0.002.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder editor: the quantity steppers from the start, not a read-only « ×1 » until the owner is known

DeckStore.steppersShown: the editor's rows, tiles and piles draw their steppers unless the deck is known not to be
the user's. A change still waits for editable() (setQuantity, and the board's own removed-cards list).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Loading checks for every Re:Builder screen: skeleton, styles, layout shifts, in the CI and as a timeline script

- tests/e2e/loading.ts: a recorder installed before the page loads (empty frame, plugin shown unstyled, skeleton font,
  forbidden texts, CLS with the moved boxes), throttling profiles, empty cache.
- plugins/rebuilder/e2e/loading.spec.ts: every screen (screens.ts), first and second visit, phone network, desktop
  and mobile; CLS budgets per screen (loading-budgets.json: 0.01, or today's value for a known shift, with its reason).
- tests/e2e/loading-timeline.ts (npm run loading:timeline): frame-by-frame image and report of a screen, two stacks
  side by side with --compare (before / after).
- First paint: <link rel="expect"> now names an element after the mount point (spaFirstPaintId()). Chromium paints
  once the expected element is inserted, and the parser could stop inside the placeholder: about 1 load in 25 showed
  the site header alone first; 0 in 90 since.
- AGENTS.md § Checks, the plugin's AGENTS.md and README.md say when to run them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Loading checks at seven screen sizes: a CLS table per screen and width, what moved named, 1280 and 1024 px in the CI

- tests/e2e/loading-matrix.ts (npm run loading:matrix): every Re:Builder screen at 360, 390, 820, 1024, 1280, 1440
  and 1920 px, first and second visit, in one table, with the shifts of each load.
- The recorder names what moved: the element in the box that moved (a node of the plugin's shadow root, which the
  layout-shift entries leave out), with two ancestors.
- loading-timeline.ts: --viewport for any of these sizes, --frames for the frames at full size.
- loading.spec.ts: on the desktop project, a second visit at 1280 and 1024 px too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re:Builder: no layout shift while a screen loads, at every screen size

- Deck bar (deck page, editor): one height for every deck, from the server's skeleton to the deck. Each row of text on
  one line (the name whole in its tooltip), the actions' columns as wide in skeleton as the buttons, and the curve,
  terrain totals and rarities going at widths where the name keeps about 210 px (at 1280 px it was squeezed to 89 px
  and the bar 317 px high). The cost curve's legend goes under the chart, so that the curve stays down to 1250 px.
- « Mes decks »: the count of the tab at the width of three digits while it loads; « Nouveau deck » after the list once
  it has loaded; the loading notice no longer wraps to a row of its own; the community deckbuilders' banner comes with
  the page (the server puts the list in the placeholder) instead of pushing the page down when it arrives.
- « Nouveau deck »: the server's skeleton is the app's page and, where the app's window opens, the window.
- Editor: the hint, the save state and « Arts des jetons » have their place while the owner is unknown, so that the
  modes row does not wrap later (tablets).
- New deck form on phones: the heroes' row has one height for every faction and while the heroes load.
- SPA pages: the server's skeleton fills the window, as the page does, so that the footer is not pushed down; the site
  logo has its size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Loading budgets: the CI's values for the editor « Aperçu » (desktop) and « Nouveau deck » (mobile, second visit)

The CI measures these known shifts above the local runs: the deck's owner and the page's footer come in another
order there. #154 removes them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The search no longer lists the owned alt arts after their card: a card's plain print stands for all its
prints, and a copy added from the search takes its default alt art. The brush changes the prints in the deck.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h or drag; a card at 0 leaves the deck board

A copy left free shows its default alt art, faded, and takes it; a touched
illustration goes on the first free copy, a dragged one on the copy it is
dropped on; a touched copy goes back to its default (slotDefaults,
slotChoices). The deck board no longer keeps a card brought to 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne pile of the deck board, copy 1 in front

On by default, remembered in the browser. The brush's copy 1 is the deck's
own choice: the prints the defaults do not explain come first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… is freed for the next illustration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o that saving moves nothing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a warning (#156)

* CI: check only the plugins a pull request changes

A pull request that only changes files under plugins/<id>/ runs only that
plugin's PHP, Node, front-end and Playwright tests; any other change runs
them all, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CI: pass plugin ids and spec folders through the environment, restricted to safe characters

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Loading checks: a CLS over budget is a warning with a screenshot, posted as a pull request comment

The CLS of a screen varies from one CI run to the next and made pull
requests red for shifts they did not cause. Over budget, the loading spec
now attaches a screenshot of the settled page with the boxes that moved
drawn on it; the CI job "CLS warnings" pushes the screenshots to the
pr-screenshots branch and keeps one pull request comment up to date. The
other loading problems (empty frame, unstyled, font, forbidden text) still
fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Yutsa and others added 27 commits October 13, 2026 17:37
… on a phone

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without a type filter the mock only searched tokens, so the default alt arts page of the
local stack showed no character, spell or permanent. It now reads the Common and Rare card
groups (Uniques have no alt art) 200 at a time and counts skip / take in families, as
AlteredOwnership's /api/alt-arts/search does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fan of what decks take is larger, a token's or a hero's single card much larger, and
the illustrations under the keys are 200 px wide. A card of the fan or an illustration
opens in core-altered-cards' card lightbox, as on the cards pages (tilt, « Accéder au
détail »), without its alt-art widget since the keys of the page already choose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and faded

A key that was off and a disabled key looked alike (60 % and 30 % opacity). A key that can
be pressed now has its text in --ac-color-text-2 and a darker bottom edge at full opacity;
a disabled key loses its edge and fill, in --ac-color-text-disabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e right

As in Re:Builder's brush (step 1 the illustrations, step 2 « Ce que le deck utilise »): the
card's name on top, its illustrations and keys on the left, the fan of what decks take on a
panel on the right (below the illustrations on a phone).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…decks take, centred

As in Re:Builder's brush: the illustrations on a band ending in a chevron, which fits into
the notch of the « what decks take » panel, its fan centred vertically; on a phone the
panel goes under the band, the chevron pointing down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s sideways

A family with many prints (heroes have up to 5) made rows over 1000 px tall, the fan of
what decks take far from the first keys. The illustrations now stay on one line that
scrolls sideways, snapping to each print, with an arrow on each side while there is more
that way; on a phone two prints and the edge of the next are in view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a card's line of illustrations has more on one side, a mask fades the cut
illustration out on that side (10 % of the line, 96 px at most).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/decks sends every deck whatever the page: an account of 1000 decks or more got the same list
again and again, until the decks API ran out of memory and the switch to « par deck » failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The service fills each line from the family's 1st card: a deck with the alt art on one line and the plain
print on another (1st card alt art, 2nd and 3rd plain) got the alt art twice when leaving « Global »,
though only one copy is owned. The lines of a family are now sent as one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he chevron

The e2e reads the player's mode instead of expecting « par deck ».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Five sections (how a game plays, the world and its art, ways to play,
build your own deck, who runs Altered now) with a contents sidebar that
highlights the section being read.

- Texts live in one JSON file per language (pages/_discover/lang), English as
  the fallback; includes/page-lang.php loads them.
- Section 2 draws two illustrations per faction from the cards API (a standard
  Hero, and a non-Hero, non-Token, non-Unique card), with the artist credit.
- Section 4 has a formats picker with the same comparison table for each format.
- Design system: new ac-toc contents component (navigation family).
- Linked from nowhere yet: no menu entry, the page only answers on its URL.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Discover page: new /pages/discover, reachable by direct URL only
Prod moves to PHP 8.4.27 first while preprod keeps 7.4 until its own
runtime switch, so the PHP job runs on both versions to catch code that
would only break one of them. The ownership mock of the e2e stack moves
to 8.4 for consistency.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PHP job ran on 7.4 and 8.4 while preprod kept PHP 7.4 after prod had
switched. Both environments now run 8.4.27, so 7.4 no longer guards any
runtime: the job goes back to a single 8.4 run, and the docs stop asking
for 7.4-compatible code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re:Builder conflicts: the PR's alt-art editor and deck board kept, with
preprod's loading states (DeckStore.steppersShown, skeletons while the
deck's owner and the alt-art mode are not known, now for « Appliquer les
arts par défaut » too). The e2e createServerDeck helper takes the copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ger keeps the player in « Global »

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A content creator filming the site leaked their address: the homepage
newsletter field was filled with the account's e-mail.

- Newsletter: the field is always empty; signed-in users get « Use my
  account address », which takes the address from the session.
- Design system: ac-tooltip and ac-masked (tooltip.css), dsMaskEmail()
  and ac_masked_email(): c•••@gmail.com, the full address in a tooltip
  on hover or focus.
- Account menu, account page and the hello-world demo page show the
  address masked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, no layout shift at 1024 px

The switch and « Arts par défaut » come with the deck's owner: at 1024 px the
row wrapped then, and the board moved 44 px down (CLS 0.043). Their skeletons,
at their widths, make the row wrap before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Never show the signed-in user's e-mail address in full
CI: drop the PHP 7.4 lint now that preprod runs 8.4.27
Default alt arts: one setting for the defaults, each deck keeps its own prints (Re:Builder)
github-actions Bot added a commit that referenced this pull request Oct 23, 2026
@github-actions

Copy link
Copy Markdown

⚠️ Loading: CLS over budget

Not a failure: the CLS of a screen varies from one CI run to the next. Each screenshot is the settled page, the boxes that moved drawn on it (dashed: before, solid: after). Frame by frame: npm run loading:timeline -- <screen> in tests/e2e. Run: https://github.com/Altered-Community/alteredcore-website/actions/runs/39951623560

desktop · editor, Main, second visit at 1024 px: skeleton until the first screen, styled, no layout shift

CLS 0.2211 (budget 0.01)

Shifts
1884 ms  0.0004
  x600 y111 87×24 → x522 y111 165×24 (ac-badge.ac-badge.ac-badge--orange < button.legality < app-deck-bar.reduced)
2008 ms  0.2207
  x24 y306 652×494 → x24 y791 652×9 (h2.overline < app-hand-stats < div.hand)
  x356 y533 320×256 → x0 y0 0×0
  x24 y533 320×256 → x0 y0 0×0
  x357 y474 318×46 → x0 y0 0×0
  x373 y421 286×36 → x0 y0 0×0

mobile · editor, Main, second visit: skeleton until the first screen, styled, no layout shift

CLS 0.4311 (budget 0.01)

Shifts
1857 ms  0.0001
  x248 y67 136×36 → x248 y63 136×44 (svg.ac-icon < ac-icon < button.ac-icon-button.ac-icon-button--ghost)
2000 ms  0.431
  x16 y193 358×651 → x16 y585 358×259 (span.label < li.tone-character < ul)
  x16 y589 358×190 → x0 y0 0×0
  x16 y790 358×54 → x0 y0 0×0
  x17 y526 356×50 → x0 y0 0×0
  x29 y474 332×36 → x0 y0 0×0

@julesduroux
julesduroux merged commit bce353a into main Oct 23, 2026
10 checks passed

This branch was successfully deployed

1 active deployment
preprod — 4fa66468 Deployed Oct 22, 2026 by julesduroux via Sync to server #195
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.

4 participants