6.7 KiB
CLAUDE.md
Guidance for agents that work in this repository. README.md describes what the app does for its users. This file describes how it is built.
Commands
npm install
npm run dev # http://localhost:5173
npm test # the parity suite, see "Tests"
npm run build # static files in dist/
npm run fixtures # regenerate test fixtures, see "Tests"
npm run lint reports findings that come from upstream, mostly
a11y/useButtonType. These findings are old, so lint is not part of CI. Before
you treat a finding as new, compare it against git show upstream/main:<file>.
Where the transforms fit
upload/.rosz -> unzip -> DOMParser -> applyTransforms -> Create40kRoster* -> render
^
src/transforms/index.js
src/App.jsx runs the transforms between the parse of the XML and the build of
the roster. A toggle therefore rebuilds the cards from the original XML, and
the user uploads nothing a second time. For the same reason, a saved roster
holds the raw roster XML and not the parsed object.
src/transforms/config.js lists the abilities that
the transforms strip and split. When you find more of them, add them to
abilitiesToStrip and abilitiesToConvert.
Lore text
src/helpers/lore.js matches a roster name against
public/Lore.csv in three steps:
- The normalized name. Case, accents, curly quotes and punctuation all differ between the roster and the export.
- The name with its last word in the singular form. Example: "Myphitic Blight-haulers" against "Myphitic Blight-hauler".
- The longest known name that the roster name ends with. Example: "Thousand Sons Chaos Spawn" against "Chaos Spawn".
This search resolves every unit in the bundled 10th-edition examples.
parseLore finds columns by header name. If someone exports the file again
with a faction column, parseLore can match that column against the
catalogue of the force. Nothing else has to change. Today the export has no
such column, so the longest entry wins for a name with lore in more than one
faction.
Two properties of the card header are less obvious than they look:
- The italic text needs a fourth font file.
ConduitITCStdshipped as three upright faces, and:rootinsrc/index.csssetsfont-synthesis: none. A request for italic therefore printed upright text and gave no warning.public/fonts/ConduitITCStd Italic.woff2and its@font-facerule correct this. The element also setsfont-synthesis: style. As a result, a face that fails to load degrades to a slanted upright face and not to no italic at all. - The lore panel has absolute position, so it cannot make the header taller.
Without help, a long legend is clipped: the longest entry in the export
overruns a 15rem header at each width below approximately 1300px. A
ResizeObserveron the text feeds themin-heightof the header instead. The observer is necessary because the text wraps differently at each card width.
Only the 10th-edition and 11th-edition renderers show lore. Leave the
9th-edition renderer in src/9th/ alone. It lays out its header differently.
Tests
The transforms began as three standalone Python scripts
(merge_duplicate_units, remove_leader_abilities, convert_choice_abilities)
that rewrote the .ros file before the upload to FancyScribe. The scripts are
gone, but the tests measure against their output.
src/transforms/__fixtures__/ holds four 11th-edition rosters. For each roster
it also holds the output of those scripts: <name>.{merge,strip,convert,all}.ros.
The suite runs each transform over the input and asserts that the result is the
same roster. This is worth more than a snapshot of the current code, because
the expected output comes from a different implementation. A person
verified that implementation with printed cards.
CAUTION: Do not regenerate an existing fixture from the JavaScript code. The
test then compares the code against itself and passes whatever the code does.
scripts/update-fixtures.mjs therefore refuses to overwrite a fixture without
--force. Use the script when you add a new example roster, and read the diff:
# Put a new roster in src/transforms/__fixtures__/, then:
npm run fixtures
The comparison is structural, not textual. The Python scripts edited the file
as text and kept its format byte for byte. These transforms build DOM nodes, so
attribute order and whitespace differ legitimately. The suite compares
generated id and typeId values as tokens. The requirement is that ids are
shared and distinct in the same pattern, not that both implementations hash
alike.
integration.test.js pushes the transformed document through the real roster
parser and checks what lands on the card:
- one Skitarii card that carries both weapons
- the data-tether ability on the model that brings it
- no Support ability
- one row for each Canticle
The tests run under jsdom, which does not implement scoped selectors like a
browser. force.querySelectorAll("force>selections>…") finds nothing under
jsdom. A browser matches the selector against the whole tree and then keeps the
descendants of force, so the parser found no units at all. The two call sites
that depend on this behavior now use :scope>…, which works in both.
Merges from upstream
FancyScribe is under active development, and 11th-edition support landed
recently. This fork touches little of it. The upstream remote is configured:
git fetch upstream
git merge upstream/main
Expect conflicts only in src/App.jsx, index.html, vite.config.js and
package.json. src/transforms/ is completely new and never conflicts.
The fork also hides three things that upstream shows, because a generic
datasheet cannot use them: the roster overview card with its charts, the unit
composition, and the points cost of each unit. These decisions live in
src/fork.js, which upstream does not have.
src/10th/Roster.jsx uses them on as few lines as possible:
- It imports
ShortSummaryTablefrom../fork, not from./ShortSummaryTable. This one-line change leaves the render site untouched. The upstream component stays in the tree, unused, so its future diffs continue to apply. hideModelCountis pinned toHIDE_UNIT_COMPOSITIONand is no longer a piece of checkbox state.
Only the two checkboxes and the pts span are deleted. A merge that touches
them therefore reports a conflict instead of quietly bringing them back.
vite.config.js sets base: "/", because the app is served at the root of a
domain. Upstream sets /fancyscribe for GitHub Pages. If you serve the app
from a subpath, change this setting.