starting_pitcher
Morrison pitched yesterday — scratched from tonight's lineup.
Help your app
play by the rules.
Try It Live — Tonight’s Lineup
Section titled “Try It Live — Tonight’s Lineup”Tonight’s card is set — full roster against a right-handed pitcher. Now break it. Toggle Morrison to fatigued and watch him get pulled from the mound. Flip the opposing pitcher to a lefty and the platoon swaps kick in. Hit “Random injury” a few times and see slots empty out as players land on the IL.
Three rule types drive the whole card: enabledWhen (injuries and fatigue scratch players), oneOf (platoon matchups swap batters when the pitcher flips), and requires (Morrison needs rest before he can start). Every change produces fouls — fields that just lost their player and need a replacement. Clear the card and rebuild from scratch if you want to see it from the other direction.
How the Demo Works
Section titled “How the Demo Works”The lineup card is a React component backed by a real @umpire/core instance. Here’s how the state flows:
// One field per player. Everything that affects eligibility is a condition.type Conditions = { opposingPitcher: 'L' | 'R' injuries: Record<string, boolean> morrisonRested: boolean}
const lineupUmp = umpire<typeof fields, Conditions>({ fields, rules: [ // oneOf: only one branch is active at a time. // Delgado (L) starts vs righties, Vega (R) starts vs lefties. oneOf('firstBasePlatoon', { vsRighty: ['delgado'], vsLefty: ['vega'], }, { activeBranch: (_v, c) => c.opposingPitcher === 'L' ? 'vsLefty' : 'vsRighty', reason: 'platoon matchup', }),
// enabledWhen: disabled when the predicate returns false. ...playerIds.map(id => enabledWhen(id, (_v, c) => !c.injuries[id], { reason: 'on the injured list', }) ),
// Morrison can't pitch without rest. enabledWhen('morrison', (_v, c) => c.morrisonRested, { reason: 'needs rest', }), ],})// Values: only players assigned to lineup slots. Bench players have no// values, so play() only fires for players actually in the lineup.const values = Object.fromEntries( Object.values(lineup).filter(Boolean).map(id => [id, id]),)
// useUmpire handles check() + play() + snapshot tracking internally.const { check: availability, fouls } = useUmpire(lineupUmp, values, conditions)
// availability.delgado → { enabled: false, reason: 'platoon matchup' }// fouls → [{ field: 'delgado', reason: 'platoon matchup', ... }]// (only if Delgado was in the lineup when the pitcher flipped)check() tells you what’s available right now. play() tells you what just fell out of play — fields that were enabled before but disabled after, and still hold values that should be cleaned up.
Minesweeper
Not just forms.
Umpire’s field-availability model works anywhere state fits a plain object with interdependent options. This minefield models 64 cells as fields, game state as conditions, and reasons as machine-readable enums — no forms anywhere in sight.
In its simplest possible form, there are three enabledWhen rules per cell. 192 rules total. 1ms to evaluate the entire board.
Signup Example
Section titled “Signup Example”import { enabledWhen, requires, umpire } from '@umpire/core'
const ump = umpire({ fields: { email: { required: true, isEmpty: (v) => !v }, password: { required: true, isEmpty: (v) => !v }, confirmPassword: { required: true, isEmpty: (v) => !v }, referralCode: {}, companyName: {}, companySize: {}, }, rules: [ requires('confirmPassword', 'password'),
enabledWhen('companyName', (_v, cond) => cond.plan === 'business', { reason: 'business plan required' }),
enabledWhen('companySize', (_v, cond) => cond.plan === 'business', { reason: 'business plan required' }),
requires('companySize', 'companyName'), ],})// Personal plan — company fields disabledump.check( { email: 'alex@example.com', password: 'hunter2' }, { plan: 'personal' },).companyName// → { enabled: false, reason: 'business plan required' }
// Business plan — companySize requires companyNameump.check( { email: 'alex@example.com', password: 'hunter2' }, { plan: 'business' },).companySize// → { enabled: false, reason: 'requires companyName' }
// Switch plans — what should reset?ump.play( { values: { companyName: 'Acme', companySize: '50' }, conditions: { plan: 'business' } }, { values: { companyName: 'Acme', companySize: '50' }, conditions: { plan: 'personal' } },)// → [// { field: 'companyName', suggestedValue: undefined },// { field: 'companySize', suggestedValue: undefined },// ]This example is intentionally about availability, not validation. confirmPassword depends on password being present, but Umpire does not check whether the two values match. That belongs in your validation layer.
What It Does
Section titled “What It Does”Umpire answers one question: given the current field values and conditions, which fields should be enabled, required, or due for cleanup?
Where Umpire Ends and Your Code Begins
Section titled “Where Umpire Ends and Your Code Begins”Umpire is intentionally small — it handles availability so you can handle everything else with whatever tools you already use.
- State — yours. Umpire reads values, never writes them. Use React state, Zustand, signals, a plain object — whatever owns your form.
- Validation — yours, but composable.
check()bridges validators (Zod, regex, functions) into availability rules. See Composing with Validation. - Rendering — yours. Umpire returns
enabled: booleanper field. Hide it, disable it, dim it — your call. - Cleanup — yours, but guided.
play()recommends which stale values to reset. You decide when and whether to apply them.
Install
Section titled “Install”npm install @umpire/coreUse @umpire/core when you just need pure availability logic. Add @umpire/react, @umpire/signals, @umpire/store, or a store-specific entry point like @umpire/zustand, @umpire/redux, @umpire/pinia, @umpire/tanstack-store, or @umpire/vuex when you want an adapter on top.
Where To Next
Section titled “Where To Next”- Try the Quick Start to learn each rule primitive with interactive demos.
- Read Availability vs Validation for the mental model.
- Read Field Satisfaction Semantics before writing rules with
isEmpty. - Use umpire() and Rules as the reference for the core API.
- See Signup Form Walkthrough for the same example broken down step by step.