Skip to content

About

shared components for competition projects

Resources

Contributing

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Latest commit

Β 

History

2,535 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

courthive-components

Browse interactive examples, component APIs and integration guides.

Framework-independent TypeScript UI components for tournament management and competition displays.

Storybook npm version License: MIT

Overview

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:

Installation

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.)

Feature imports

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.

Quick Start

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);
  }
});

Components

Display Components

renderMatchUp

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
});

renderStructure

Display a complete draw structure (bracket, round robin, etc.).

import { renderStructure, compositions } from 'courthive-components';

const element = renderStructure({
  matchUps: drawMatchUps,
  composition: compositions.Basic
});

renderRound

Display a single round of matches within a structure.

import { renderRound } from 'courthive-components';

const element = renderRound({
  matchUps: roundMatchUps,
  roundNumber: 1
});

renderParticipant

Display participant information (name, rating, country, etc.).

import { renderParticipant } from 'courthive-components';

const element = renderParticipant({
  participant: participantData
});

renderRoundHeader

Display round headers for draw structures.

import { renderRoundHeader } from 'courthive-components';

const element = renderRoundHeader({
  roundName: 'Quarterfinals',
  roundNumber: 3
});

renderContainer

Wrapper component for draw containers with scrolling and layout.

Input Components

renderParticipantInput

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) }
});

Modal Components

cModal

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 }
  ]
});

openScoreEntryDialog

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

getMatchUpFormatModal

Interactive modal for selecting/editing match formats.

import { getMatchUpFormatModal } from 'courthive-components';

getMatchUpFormatModal({
  existingMatchUpFormat: 'SET3-S:6/TB7',
  callback: (newFormat) => {
    console.log('Format:', newFormat);
  }
});

Form Components

renderForm

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' }
]);

renderField

Render individual form fields.

renderButtons

Render button groups with consistent styling.

renderMenu

Render dropdown/context menus.

validator

Form validation utilities.

UI Components

drawer

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'
});

tipster

Tooltip/popover system using Tippy.js.

import { tipster } from 'courthive-components';

tipster({
  target: buttonElement,
  content: 'Click to edit',
  placement: 'top'
});

Compositions

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 colors

See the composition options reference for serving indicators, header/footer visibility, theme inheritance and editor behavior.

Constants

MATCH_FORMATS

Pre-defined match format codes.

import { MATCH_FORMATS } from 'courthive-components';

console.log(MATCH_FORMATS.SET3_S6_TB7);
// 'SET3-S:6/TB7'

Utilities

courthiveComponentsVersion

Get the current package version.

import { courthiveComponentsVersion } from 'courthive-components';

console.log(courthiveComponentsVersion());

Documentation

πŸ“š Interactive Documentation: Storybook

The Storybook includes:

  • Live component demos
  • Interactive examples
  • Configuration options
  • Usage patterns
  • Integration guides

Styling

Import the bundled CSS:

import 'courthive-components/dist/courthive-components.css';

Theme tokens β€” two families, on purpose

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-focus shipped an invisible focus border.
  • Do not write a fallback. var(--sp-muted), never var(--sp-muted, #888). Fallbacks drift from the definition β€” --sp-muted once 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 by src/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.

TypeScript Support

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);

Integration with Competition Factory

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 });
  }
});

Development

# Install dependencies
pnpm install

# Start Storybook
pnpm storybook

# Build library
pnpm build

# Run tests
pnpm test

# Build Storybook for deployment
pnpm build-storybook

Publishing documentation

Every 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=true

Replace 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.

Contributing

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.

Guidelines

  • Maintain framework-agnostic vanilla TypeScript
  • Follow existing component patterns
  • Add Storybook stories for new components
  • Include TypeScript types
  • Test with Competition Factory integration

License

MIT β€” see LICENSE.

Links

About

shared components for competition projects

Resources

Contributing

Security policy

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages