Lineup Editing (architecture)
How a player's lineup is edited in code, placing, removing, swapping, and moving contracts, and switching schema. The rules these operations enforce (nothing is ever dropped, 11 positions, bench semantics) live in Lineup Rules (domain).
The DraftLineup seam
dto/lineupMutations.ts defines the editable state and the operations on it:
DraftLineup = {
formation: DraftFormationDTO; // schema + position->contract map + chemistry
bench: ContractDTO[]; // contracts not currently placed
}All editing logic is a set of pure functions of the shape (state: DraftLineup...args) => DraftLineup. They never mutate the input; they return a new state (or the same reference when nothing changes, e.g. removing from an empty position or moving onto an occupied slot):
assignToPosition(state, position, contract): place a contract, displacing any current occupant to the bench.removeFromPosition(state, position): clear a position, returning its contract to the bench.swapSlots(state, fromId, toPos, toId): the general move/swap covering every source/target combination: position↔position, position↔bench, bench↔bench, and bench→position.toPosmay be the literal"bench";toIdis the contract currently at the target, if any.moveToEmpty(state, fromId, targetPos): move onto an empty position only.setSchema(state, nextSchema): remap placed contracts to the new schema viachangeSchema. Any contract the remap cannot carry into the new schema is appended to the bench rather than silently dropped, so no contract is ever lost on a schema change. The bench reference is preserved when nothing is dropped.
Because the mutations are pure and Vue-free, the branchy slot/bench logic is unit-tested directly in frontend/src/tests/formation/lineupMutations.spec.ts.
The reactive shell
frontend/src/composables/useTeamLineup.ts owns the live reactive state (draft, benchContracts) plus server sync, dirty tracking, chemistry, and the save mutation. Its editing handlers are thin wrappers: snapshot the live state into a DraftLineup, call the matching pure mutation, and write the result back via applyMutation. TeamPage.vue calls the same handler names, so no view API changed.
Touch input
ArticleNode.vue accepts drops from two input paths that both resolve to the same swap / dropOnEmpty emits:
- HTML5 drag-and-drop (mouse/desktop): unreliable on touch devices, so it stays mouse-only.
- Long-press-and-drag (
frontend/src/composables/useTouchDragDrop.ts): a long press lifts a floating clone that follows the finger; releasing over another article swaps, over an empty pitch slot moves. It autoscrolls the nearestion-contentnear the viewport edges so a bench tile can reach an off-screen pitch row without scrolling by hand first. The hit-test that turns a drop point into a swap/move/no-op decision (resolveDrop) is a pure function of the target element'sdata-article-id/data-positionattributes, kept separate from the gesture/DOM-geometry code so it can be unit-tested directly.
TeamFormation/BenchSection re-emit an ArticleNode's dropOnEmpty as their own moveToEmpty, so both input paths land on the exact same useTeamLineup handlers described above. An editable prop threads down to disable both paths on read-only hosts (the dashboard preview).
Related documentation
- Lineup Rules (domain): the invariants the mutations exist to uphold.
- Chemistry Links Rendering: chemistry composition for the placed formation (
computeChemistryLinks).
