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.
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/docsThe 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
| Command | What it does |
|---|---|
npm run build | Builds the package: tokens and utilities are generated first, then CSS and JS are written to packages/ivolt/dist. |
npm test | Vitest: unit tests for the JS components and the contract tests over the CSS sources and the documented snippets. |
npm run test:browser | Playwright. 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 sizes | Reports the size of each built artefact. A change that moves these numbers noticeably should say why. |
npm run pack-smoke | Packs 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-examples | Loads the example pages and recipes and asserts they still work against the current build. |
npm run build:docs | Builds the static documentation site. |
npm run verify | Everything 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-examplesTwo 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-, utilitiesiv-u-, custom properties--iv-, data attributesdata-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:targetfallbacks 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
@layerand import nothing. Entries assign layers at import time, and each module is imported exactly once per entry. - Breakpoints through
@custom-medianames — nevervar()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:
- States: default, hover, focus, active, disabled, and any loading or error state it needs.
- Keyboard behaviour, documented key by key.
- A fallback for when JavaScript does not run.
- Documentation with fixtures.
- Tests: unit for behaviour, browser for rendering, axe in both themes.
JavaScript
- No top-level access to
document,window,navigatorormatchMedia; SSR imports must not throw. - No
eval, nonew Function, noinnerHTMLwith external data, noon*attributes. The package must run underscript-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:
| Check | Fails when |
|---|---|
| Modules declare no layer and import nothing | A source module adds @layer or an @import |
No !important outside the sr-only utilities | Any other declaration uses it |
| Prefixes on classes and custom properties | An unprefixed name appears in the sources |
Element selectors scoped to .iv-root | A 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-media | A media query reads a custom property |
| Utilities match the declared matrix exactly | The generated file gains or loses a class relative to the contract |
| Entries import each module once and declare the layer order | A duplicate import silently drops a layer assignment |
| Snippets match the distribution | Documented 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 verifybefore 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.