Skip to content
Español

Navigate

Type to search. Press Escape to close.

    Project

    Contributing

    iVOLT is a small codebase with strict rules. Most of the rules exist because a design system breaks slowly: one unprefixed class, one !important, one component documented with markup that no test ever renders. The contract tests below encode those rules so a pull request fails fast instead of drifting.

    Licence: MIT. Language: code, identifiers and public docs in English. Every change to the public surface: a decision record in docs/DECISIONS.md.

    Setting up

    git clone https://github.com/iNTERVOLUTIONS-Labs/iVOLT
    cd iVOLT
    npm ci                 # install from the single lockfile, never `npm install` in CI
    npm run build          # generate packages/ivolt/dist
    npm run dev:docs   # builds the package first when dist/ is missing       # Astro dev server for apps/docs

    The repository is an npm workspace with one lockfile at the root. dist/ is generated and not versioned, so build before running the browser suite, the size report or the pack smoke test.

    Commands

    CommandWhat it does
    npm run buildBuilds the package: tokens and utilities are generated first, then CSS and JS are written to packages/ivolt/dist.
    npm testVitest: unit tests for the JS components and the contract tests over the CSS sources and the documented snippets.
    npm run test:browserPlaywright. Chromium only by default; IVOLT_ALL_BROWSERS=1 npm run test:browser runs Chromium, Firefox and WebKit. Covers behaviour, axe, overflow from 320 to 1920px, RTL, themes, docs pages and recipes.
    npm run sizesReports the size of each built artefact. A change that moves these numbers noticeably should say why.
    npm run pack-smokePacks the package with npm pack, installs the tarball into a scratch project and checks that the exports resolve and that unused components are tree-shaken.
    npm run check-examplesLoads the example pages and recipes and asserts they still work against the current build.
    npm run build:docsBuilds the static documentation site.
    npm run verifyEverything above in sequence. This is the gate before a phase closes.

    Playwright needs its browsers installed once (npx playwright install); the axe checks run through @axe-core/playwright.

    Repository layout

    iVOLT/
    ├─ package.json              # workspaces + root scripts
    ├─ package-lock.json         # the only lockfile
    ├─ docs/                     # contracts, design system, quality bar, ADRs, roadmap
    ├─ packages/ivolt/
    │  ├─ tokens/tokens.json     # single source of truth for tokens
    │  ├─ fixtures/<family>/*.html   # one source for docs, tests and starters
    │  ├─ src/css/               # reset, tokens (generated), base, layout/, components/,
    │  │                         # utilities (generated), core.css, ivolt.css, ivolt.flat.css
    │  ├─ src/js/                # core/ (registry, options, events, focus, keys),
    │  │                         # components/, theme.js, index.js, auto.js, iife.js
    │  ├─ scripts/               # build-tokens, build-utilities, build-css, build-js, sizes
    │  └─ dist/                  # generated, not versioned
    ├─ apps/docs/                # Astro static site
    ├─ examples/                 # plain-html, vite, recipes/
    ├─ tests/                    # unit, browser, contracts, integration
    └─ scripts/                  # pack-smoke, check-examples, sync-examples

    Two files are generated and must never be edited by hand: src/css/tokens.css (from tokens/tokens.json) and src/css/utilities.css (from the utilities config). Change the source and rebuild. The same goes for custom-media.css, reset.layer.css and core/breakpoints.js.

    Rules for a change

    CSS

    • Prefix everything. Classes iv-, utilities iv-u-, custom properties --iv-, data attributes data-iv-.
    • No !important. The single exception is .iv-u-sr-only, and it is documented as such.
    • Specificity at or below (0,2,0) for component selectors, so a utility — emitted with a doubled selector at exactly (0,2,0) and loaded last — always wins. The documented :target fallbacks for dialog and drawer are the only allowances.
    • Element selectors only inside .iv-root. Nothing in the package may style a bare element globally.
    • Modules declare no @layer and import nothing. Entries assign layers at import time, and each module is imported exactly once per entry.
    • Breakpoints through @custom-media names — never var() inside a media query, which does not work.
    • Colours come from semantic tokens. A raw hex in a component file is a bug; it will not follow the themes.

    Fixtures are the single source

    A fixture in packages/ivolt/fixtures/<family>/<case>.html is simultaneously the preview on the docs page, the copyable snippet, and the markup the tests render. Do not write example markup inline in a docs page for a component, and do not write test markup inside a spec file. If the documented snippet and the tested markup can diverge, eventually they will.

    A family is finished when

    Counting variants does not widen the scope. A component family is done only when it has all five:

    1. States: default, hover, focus, active, disabled, and any loading or error state it needs.
    2. Keyboard behaviour, documented key by key.
    3. A fallback for when JavaScript does not run.
    4. Documentation with fixtures.
    5. Tests: unit for behaviour, browser for rendering, axe in both themes.

    JavaScript

    • No top-level access to document, window, navigator or matchMedia; SSR imports must not throw.
    • No eval, no new Function, no innerHTML with external data, no on* attributes. The package must run under script-src 'self'.
    • No dependencies, no network requests, no telemetry.
    • init() is idempotent; destroy() restores the DOM to the attributes it found, which is tested.
    • Events are prefixed iv: and are part of the contract — adding or renaming one is a contract change.

    Contract changes need an ADR

    Anything that changes a public surface — a class name, a token name, an option, an event, an export, the layer order — needs an entry in docs/DECISIONS.md recording the decision, the alternatives considered and the evidence. The ADR is part of the pull request, not a follow-up.

    The contract tests

    These run in npm test and fail loudly if a rule above is broken:

    CheckFails when
    Modules declare no layer and import nothingA source module adds @layer or an @import
    No !important outside the sr-only utilitiesAny other declaration uses it
    Prefixes on classes and custom propertiesAn unprefixed name appears in the sources
    Element selectors scoped to .iv-rootA bare element selector escapes the scope in base.css
    Specificity ceiling (0,2,0)A component selector goes above it, outside the documented fallbacks
    Breakpoints via @custom-mediaA media query reads a custom property
    Utilities match the declared matrix exactlyThe generated file gains or loses a class relative to the contract
    Entries import each module once and declare the layer orderA duplicate import silently drops a layer assignment
    Snippets match the distributionDocumented markup stops matching what the package actually ships

    If a contract test blocks a change you believe is right, the fix is an ADR that moves the contract, not an exception in the test.

    Pull requests

    • Run npm run verify before opening one, and say in the description which checks you ran and which you could not.
    • Keep the change focused on one family or one concern; mixed refactors are hard to review against a contract.
    • Report accessibility bugs and browser bugs with the fixture name, the engine and the steps — the fixture makes it reproducible in one command.
    • From 1.0 the public surface follows SemVer. A change that is right but breaking waits for a major and is recorded first; a change that is convenient but unrecorded never lands.

    Licence

    MIT, Copyright (c) 2026 iNTERVOLUTIONS. See LICENSE in the repository root. Contributions are accepted under the same licence.