152 lines
6.7 KiB
Markdown
152 lines
6.7 KiB
Markdown
# CLAUDE.md
|
|
|
|
Guidance for agents that work in this repository. [README.md](README.md)
|
|
describes what the app does for its users. This file describes how it is built.
|
|
|
|
## Commands
|
|
|
|
```sh
|
|
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`](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`](src/helpers/lore.js) matches a roster name against
|
|
`public/Lore.csv` in three steps:
|
|
|
|
1. The normalized name. Case, accents, curly quotes and punctuation all differ
|
|
between the roster and the export.
|
|
2. The name with its last word in the singular form. Example: "Myphitic
|
|
Blight-haulers" against "Myphitic Blight-hauler".
|
|
3. 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**. `ConduitITCStd` shipped as
|
|
three upright faces, and `:root` in [`src/index.css`](src/index.css) sets
|
|
`font-synthesis: none`. A request for italic therefore printed upright text
|
|
and gave no warning. `public/fonts/ConduitITCStd Italic.woff2` and its
|
|
`@font-face` rule correct this. The element also sets
|
|
`font-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
|
|
`ResizeObserver` on the text feeds the `min-height` of 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:
|
|
|
|
```sh
|
|
# 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:
|
|
|
|
```sh
|
|
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`](src/fork.js), which upstream does not have.
|
|
`src/10th/Roster.jsx` uses them on as few lines as possible:
|
|
|
|
- It imports `ShortSummaryTable` from `../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.
|
|
- `hideModelCount` is pinned to `HIDE_UNIT_COMPOSITION` and 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.
|