Browse interactive examples, component APIs and integration guides.
Framework-independent TypeScript UI components for tournament management and competition displays.
This library provides the UI components of the CourtHive competition management platform. Components are framework-agnostic (vanilla TypeScript) and render CODES records β CourtHive's canonical competition data standard, a superset of the ITF's TODS β as produced by the Competition Factory engine.
Used by:
- TMX β the tournament desk client
- courthive-public β the public viewer and player-facing pages
- epixodic β point-by-point match tracking
- CourtHive.com β the marketing site
- Competition Factory documentation β interactive examples
Requires Node.js 24 or later (engines.node is >=24; 22 is maintenance-only, end of life April 2027).
pnpm add courthive-components(External consumers may also use npm install courthive-components or
yarn add courthive-components. Inside the CourtHive monorepo,
pnpm is the only supported manager.)
SVG-only consumers such as a marketing hero can import from courthive-components/courts.
The segmented bar is also available from courthive-components/segmented-bar.
These additive ESM entries avoid loading the root editor, scoring and engine dependencies.
Import courthive-components/features.css for their styles; it contains the small court/bar stylesheets.
The existing root export and dist/courthive-components.css remain supported with their existing cascade.
Run pnpm build && pnpm verify:consumer-bundle to check consumer JS and feature CSS budgets.
Update a budget deliberately with pnpm verify:consumer-bundle --update-baseline after reviewing the measurements.
import { renderMatchUp, openScoreEntryDialog, compositions } from 'courthive-components';
// Render a match display
const matchUpElement = renderMatchUp({
matchUp: myMatchUpData,
composition: compositions.Australian
});
document.getElementById('container').appendChild(matchUpElement);
// Open the score entry dialog
openScoreEntryDialog({
matchUp: myMatchUpData,
sides: [{ participantName: 'Ann' }, { participantName: 'Bea' }],
onSubmit: ({ outcome }) => {
console.log('Engine-ready outcome:', outcome);
}
});Display a single match with scores, participants, and status indicators.
import { renderMatchUp, compositions } from 'courthive-components';
const element = renderMatchUp({
matchUp: matchUpData,
composition: compositions.Wimbledon,
isLucky: false
});Display a complete draw structure (bracket, round robin, etc.).
import { renderStructure, compositions } from 'courthive-components';
const element = renderStructure({
matchUps: drawMatchUps,
composition: compositions.Basic
});Display a single round of matches within a structure.
import { renderRound } from 'courthive-components';
const element = renderRound({
matchUps: roundMatchUps,
roundNumber: 1
});Display participant information (name, rating, country, etc.).
import { renderParticipant } from 'courthive-components';
const element = renderParticipant({
participant: participantData
});Display round headers for draw structures.
import { renderRoundHeader } from 'courthive-components';
const element = renderRoundHeader({
roundName: 'Quarterfinals',
roundNumber: 3
});Wrapper component for draw containers with scrolling and layout.
Autocomplete input for participant selection.
import { renderParticipantInput } from 'courthive-components';
const input = renderParticipantInput({
matchUp: matchUpData,
side: matchUpData.sides[0],
composition: { configuration: { participantProvider: () => participantsList } },
eventHandlers: { assignParticipant: ({ participant }) => console.log('Selected:', participant) }
});Base modal system with flexible configuration.
import { cModal } from 'courthive-components';
cModal.open({
title: 'Confirm Action',
content: 'Are you sure?',
buttons: [
{ label: 'Cancel', close: true },
{ label: 'Confirm', onClick: handleConfirm, close: true }
]
});Score entry in a modal: three interchangeable approaches over one card, a live format picker, irregular endings with their policy reason codes, and reopening a recorded outcome.
import { openScoreEntryDialog } from 'courthive-components';
openScoreEntryDialog({
matchUp, // prefills a recorded outcome: matchUp.score.sets, matchUpStatus, winningSide
sides: [{ participantName: 'Ann', seed: '1' }, { participantName: 'Bea' }],
approach: 'dynamicSets', // 'dynamicSets' | 'freeScore' | 'dialPad'; the operator can switch in the dialog
statusCodeGroups, // the scoring policy's matchUpStatusCodes; omit and no reason chips are drawn
clearable: true, // false when a later match depends on the result, so an empty submit is refused
disabledMatchUpStatuses: [], // statuses the engine's SCORE action refuses for this matchUp
onApproachChange: (approach) => savePreference(approach),
onSubmit: ({ outcome, sets }) => {
// outcome is engine-ready: hand it to setMatchUpStatus as is
}
});Scoring Approaches:
- dynamicSets - Set-by-set entry with real-time validation
- freeScore - Flexible text-based entry (e.g., "6-4 6-3")
- dialPad - Touch-friendly numeric keypad
Interactive modal for selecting/editing match formats.
import { getMatchUpFormatModal } from 'courthive-components';
getMatchUpFormatModal({
existingMatchUpFormat: 'SET3-S:6/TB7',
callback: (newFormat) => {
console.log('Format:', newFormat);
}
});Render dynamic forms from configuration.
import { renderForm } from 'courthive-components';
const inputs = renderForm(container, [
{ field: 'name', label: 'Name', type: 'text' },
{ field: 'age', label: 'Age', type: 'number' }
]);Render individual form fields.
Render button groups with consistent styling.
Render dropdown/context menus.
Form validation utilities.
Slide-out drawer component for side panels.
import { drawer, initDrawer } from 'courthive-components';
initDrawer(); // Initialize once
drawer.open({
title: 'Details',
content: myContent,
side: 'right' // 'left' | 'right'
});Tooltip/popover system using Tippy.js.
import { tipster } from 'courthive-components';
tipster({
target: buttonElement,
content: 'Click to edit',
placement: 'top'
});Pre-configured visual themes matching Grand Slam tournaments:
import { compositions } from 'courthive-components';
// Available compositions:
compositions.Australian; // Australian Open colors
compositions.French; // Roland Garros colors
compositions.Wimbledon; // Wimbledon colors
compositions['US Open']; // US Open colorsSee the composition options reference for serving indicators, header/footer visibility, theme inheritance and editor behavior.
Pre-defined match format codes.
import { MATCH_FORMATS } from 'courthive-components';
console.log(MATCH_FORMATS.SET3_S6_TB7);
// 'SET3-S:6/TB7'Get the current package version.
import { courthiveComponentsVersion } from 'courthive-components';
console.log(courthiveComponentsVersion());π Interactive Documentation: Storybook
The Storybook includes:
- Live component demos
- Interactive examples
- Configuration options
- Usage patterns
- Integration guides
Import the bundled CSS:
import 'courthive-components/dist/courthive-components.css';Colour is driven entirely by CSS custom properties defined in src/styles/theme.css, in a :root
block (light) and a [data-theme='dark'] block. Never hardcode a colour in component CSS, and
never use Bulma classes or --bulma-*: class names that merely follow Bulma's naming are ours and
are fine, but the palette must resolve through a token or it cannot follow the theme.
| family | what it is | use it for |
|---|---|---|
--chc-* |
The general design system. Semantic roles β --chc-text-primary, --chc-bg-elevated, --chc-border-primary, --chc-container-* intents and their --chc-on-* foregrounds. |
Anything shared: draws, cards, tables, buttons, notifications. |
--sp-* |
A slate palette with translucent, layered surfaces (--sp-panel-bg, --sp-card-bg, --sp-border) plus finer border and status intensities. |
The schedule page, scheduling profile, policy catalog, topology builder and interactive scoring β surfaces designed as panels over a ground. |
They are not duplicates. --chc-* began as a compatibility layer whose light values match
previously hardcoded ones; --sp-* is a newer, denser palette with alpha-composited surfaces
--chc-* has no equivalent for. Consumers bridge both: TMX aliases 29 --chc-* and 13 --sp-* to
its own --tmx-* system.
Rules that keep them working:
- Every token must be defined in
theme.css, in both blocks. A token used but never defined renders its call-site fallback in both themes β which is how--sp-border-focusshipped an invisible focus border. - Do not write a fallback.
var(--sp-muted), nevervar(--sp-muted, #888). Fallbacks drift from the definition β--sp-mutedonce had five different ones β and the token stops being a single source of truth. - The library must not reference a consumer's tokens.
--tmx-*belongs to TMX; reaching for it from here inverts the dependency. --chc-on-*is the readable foreground for a--chc-container-*fill. Both themes are held to WCAG AA bysrc/styles/theme-contrast.test.ts; change a container colour and that test tells you whether its foreground still works.- Brand themes (
src/styles/themes.css,.chc-theme-*) keep literal colours. A brand does not follow the light/dark theme.
The package includes TypeScript definitions:
import { openScoreEntryDialog } from 'courthive-components';
import type { ScoreEntryDialogParams } from 'courthive-components';
const params: ScoreEntryDialogParams = {
matchUp: myMatchUp,
sides: [{ participantName: 'Ann' }, { participantName: 'Bea' }],
onSubmit: ({ winningSide, outcome }) => {
console.log('Winner:', winningSide, 'engine outcome:', outcome);
}
};
openScoreEntryDialog(params);Components read the CODES records that tods-competition-factory produces:
import { tournamentEngine } from 'tods-competition-factory';
import { renderMatchUp, openScoreEntryDialog } from 'courthive-components';
// Get match data from factory
const { matchUp } = tournamentEngine.findMatchUp({ matchUpId });
// Render with components
const display = renderMatchUp({ matchUp });
// Score with the dialog; its outcome is already the shape setMatchUpStatus reads
openScoreEntryDialog({
matchUp,
sides: matchUp.sides.map((side) => ({ participantName: side.participant?.participantName ?? '' })),
onSubmit: ({ outcome }) => {
tournamentEngine.setMatchUpStatus({ matchUpId, drawId: matchUp.drawId, outcome });
}
});# Install dependencies
pnpm install
# Start Storybook
pnpm storybook
# Build library
pnpm build
# Run tests
pnpm test
# Build Storybook for deployment
pnpm build-storybookEvery stable GitHub release builds Storybook from its release tag before publishing the npm package.
After npm publication succeeds, the release workflow publishes that same Storybook artifact to
GitHub Pages. Prereleases leave the stable documentation in place.
The site's release.json records its package version, tag and source commit.
The workflow uses the existing Release Bot App to push gh-pages, so GitHub starts the Pages build.
Publishing is serialized, and older release runs cannot replace the latest release's documentation.
PR CI also builds the production Storybook to catch documentation build failures before release.
To refresh the latest stable release's documentation without publishing npm again, run Publish package and Storybook from the Actions tab with its existing release tag and storybook_only checked:
gh workflow run npm-publish.yml --ref main -f tag=v7.8.0 -F storybook_only=trueReplace v7.8.0 with the latest stable release tag. If only the documentation publishing job failed,
rerun that failed job. A dev-to-main checkpoint updates the release PR; merging the release PR publishes
the release and starts the package/documentation workflow.
Contributions are welcome! This library is actively used in production tournament management applications.
Branch from dev and open pull requests against dev; main advances at release checkpoints.
- Maintain framework-agnostic vanilla TypeScript
- Follow existing component patterns
- Add Storybook stories for new components
- Include TypeScript types
- Test with Competition Factory integration
MIT β see LICENSE.
- Competition Factory: https://github.com/CourtHive/competition-factory
- TMX: https://github.com/CourtHive/tmx