@runsnative/components
v0.11.2
Published
RunsNative component library - web components for context-first experiences
Readme
@runsnative/components
RunsNative web components — context-first UI primitives built as native custom elements.
Installation
npm install @runsnative/componentsCDN Usage
@runsnative/components is published to npm and automatically available via jsdelivr within minutes of each release. No manual cache invalidation required.
Load all components (single script tag)
<script type="module"
src="https://cdn.jsdelivr.net/npm/@runsnative/components@latest/dist/runsnative.min.js">
</script>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@runsnative/components@latest/dist/tokens.css">Pin to a specific version (recommended for production)
<script type="module"
src="https://cdn.jsdelivr.net/npm/@runsnative/[email protected]/dist/runsnative.min.js">
</script>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@runsnative/[email protected]/dist/tokens.css">Using @latest always resolves to the newest published version. For production, pin to a specific version to avoid unexpected changes.
URL pattern
| Asset | URL |
|---|---|
| All components bundle | https://cdn.jsdelivr.net/npm/@runsnative/components@{version}/dist/runsnative.min.js |
| Design tokens CSS (default theme) | https://cdn.jsdelivr.net/npm/@runsnative/components@{version}/dist/tokens.css |
| A canonical theme | https://cdn.jsdelivr.net/npm/@runsnative/components@{version}/dist/tokens-{theme}.css |
Replace {version} with a semver tag (e.g. 0.5.0) or latest. Replace {theme} with a canonical theme slug (see below).
Loading a canonical theme
tokens.css ships the neutral default theme. To render components in one of the canonical themes instead, load its theme CSS after tokens.css (its :root tokens override the default by cascade order) and set data-run-theme on <html>:
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@runsnative/[email protected]/dist/tokens.css">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@runsnative/[email protected]/dist/tokens-arctic-frost.css">
<html data-run-theme="arctic-frost" data-run-mode="dark">Theme slugs match the published dist/tokens-<slug>.css filenames (e.g. arctic-frost, botanical-garden, dotorg-site) — one per brand config in brand-configs/. Each theme CSS supports data-run-mode="light"|"dark" and data-run-contrast="high" (see docs/decisions/brand-theme-terminology-correction-v1.md for the theme/skin/brand terminology).
Preventing a flash of unstyled theme (FOUC)
Because the theme override CSS loads as a stylesheet, a page that sets data-run-theme via JavaScript after load will flash the default theme first. Stamp the attributes synchronously in <head>, before the stylesheets, so the browser never paints the default:
<script>
(function () {
var html = document.documentElement;
var theme = localStorage.getItem('run-theme');
var mode = localStorage.getItem('run-mode') || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
var skin = localStorage.getItem('run-skin');
if (theme) html.setAttribute('data-run-theme', theme);
html.setAttribute('data-run-mode', mode);
if (skin) html.setAttribute('data-run-skin', skin);
})();
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@runsnative/[email protected]/dist/tokens.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@runsnative/[email protected]/dist/tokens-arctic-frost.css">Adjust the localStorage keys to whatever your app persists the user's theme/mode/skin choice under — the snippet only needs to run before the stylesheet <link> tags.
Skin Authoring
@runsnative/components exposes three sub-path entrypoints for authors building custom image-based or SVG-based skins.
Importing the skin authoring modules
import { nineSlice, tileAndCaps, generateTintFilter, measureBaseLum }
from '@runsnative/components/image-skin';
import { sanitizeSvg, composeSvgSkin }
from '@runsnative/components/svg-skin';
import { createSkinLoader, registerSkinLoaders, loadSkinModule }
from '@runsnative/components/skin-loader';TypeScript declarations (.d.ts) ship alongside each module.
image-skin — bitmap renderers
Provides the slicing and tinting math for CSS-tier image skins. The author supplies asset URLs (via CSS custom properties), slice coordinates, and a measured base luminance; this module emits the CSS declarations.
nineSlice({ slice, width, srcVar, repeat?, fill? })— CSS declarations for a 9-sliceborder-image(corners + tileable edges + center fill). Returns a CSS declaration block string.tileAndCaps({ capSlice, capWidth, srcVar, repeat?, fill? })— Fixed-width end caps with a seamlessly tileable middle band. A degenerate 9-slice with zero top/bottom slice.generateTintFilter({ color?, brandToken?, baseLum, scope? })— Computes afeColorMatrixthat tints a neutral (grayscale) asset toward a brand color, normalized by the asset's measured luminance. Returns{ matrix, values, color, svgFilter(), cssFilter() }.measureBaseLum(imageData)— Alpha-weighted mean luma of a neutral asset, in[0, 1]. Feed this togenerateTintFilterasbaseLum.
svg-skin — SVG-layer composition
Composes a complete skin module from a multi-layer SVG and a component-DOM render function. Handles sanitization, per-layer fill token wiring, and @keyframes animation generation — gated behind prefers-reduced-motion: no-preference.
composeSvgSkin({ svg, render, styles?, layers?, meta? })— Returns a frozen{ styles, render }skin module compatible withdefineSkin(). Thelayersmap accepts selectors into the SVG with optionalfill(token name or{ token, fallback }) andanimationconfig.sanitizeSvg(svg)— Strips<script>,<foreignObject>,on*attributes, andjavascript:URLs. Called automatically bycomposeSvgSkin; exported for tooling and tests.
skin-loader — lazy-load wiring
Centralises the dynamic-import pattern needed to prevent Vite from pre-bundling PNG assets (which hangs vitest). Use createSkinLoader in component classes:
import { createSkinLoader } from '@runsnative/components/skin-loader';
// In your component class body:
const _myCustomSkin = createSkinLoader('my-skin', 'button');
// In updated():
if (_myCustomSkin.loaded) {
this.shadowRoot.adoptedStyleSheets = [_myCustomSkin.styles];
_myCustomSkin.module.render(this);
} else {
_myCustomSkin.load().then(() => this.requestUpdate());
}createSkinLoader(skinName, component)— Returns{ loaded, styles, module, meta, load() }.metaexposes the skin's rendering tier for C+ dispatch.loadSkinModule(skinName, component)— ReturnsPromise<unknown>for the raw skin module. Use as astatic skinLoadersvalue.registerSkinLoaders(map)— Register literal() => import('./…')thunks so bundlers can see them. Call once as a side-effect import from your skin registry file.
Registration with a component
The static skinLoaders map in each component file — which wires skin names to their loader functions — is generated automatically by the factory from .js files in the component's skins/ directory. Skin authors do not edit the component file directly; the factory regenerates it on every promote.
Testing
Unit tests run in two environments (full contract + the curated allowlist live in the header of vitest.browser.config.js — read that before authoring or moving tests):
| Invocation | Config | Environment | CI runner |
|---|---|---|---|
| npm test | vitest.config.js | jsdom | components-jsdom-test.yml (nightly) |
| npm run test:wc | vitest.browser.config.js | Chromium (Playwright) | components-browser-test.yml (nightly) |
- Every
design-system/**/*.test.jsfile runs in both environments;*.browser.test.jsfiles run in Chromium only. - The Chromium suite is green by construction via an explicit
EXCLUDEDallowlist block in its config — each exclusion carries a reason and a graduation ticket. Never exclude silently. - Environment-specific expectations use the canonical marker pattern (PR #1155):
navigator.userAgent.includes('jsdom') ? it.fails : it(orit.skip), always with a WHY comment. - The browser config does not load
vitest.setup.js(jsdom polyfills/location patch) — real-platform fidelity is the point. Tests must not navigate the page or assume jsdom API shapes. - Fast local loop: scope the browser run, e.g.
npx vitest run --config vitest.browser.config.js design-system/text-motion/.
Publishing
Releases are human-initiated via the Publish @runsnative/components GitHub Actions workflow (workflow_dispatch). To publish a new version:
- Go to Actions → Publish @runsnative/components → Run workflow
- Select bump type:
patch(bug fixes),minor(new components),major(breaking changes) - The workflow builds, verifies, bumps the version, publishes to npm, and pushes a git tag
Prerequisite: NPM_TOKEN must be set as a GitHub Actions secret with publish access to the @runsnative npm org.
The jsdelivr CDN serves the new version within minutes of the npm publish completing.
