npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zakkster/lite-hueforge

v1.0.1

Published

Reactive OKLCH color-system designer — thin composer over the @zakkster color stack. Curve-driven scale generation, APCA contrast, design-token exports, and Canvas slider-track baking.

Downloads

270

Readme

@zakkster/lite-hueforge

Reactive OKLCH color-system designer. Curve-driven Radix-style palettes, APCA contrast, 7 token formats, image extraction. Zero-GC during slider drag.

npm version sponsor Zero-GC npm bundle size npm downloads npm total downloads TypeScript lite-signal peer Dependencies license

The engine that powers Hueforge.app. Usable standalone for any OKLCH-aware design-system tooling: build palettes from a base color and a lightness curve, render 12-step Radix-style scales, verify pairwise APCA contrast, simulate color-vision deficiency, and export to CSS / Tailwind / SCSS / DTCG JSON / Figma Tokens / SwiftUI / Android XML.

Highlights

  • Reactive. Built on @zakkster/lite-signal. Editing any input on any scale recomputes every dependent step in the same frame, through the signal graph — no manual diffing.
  • Zero-GC slider drag. Each scale's 12-step output is a pre-allocated array of pre-allocated slot objects, mutated in place per recompute. Under --expose-gc, 100k base-axis mutations retain < 1 byte per recompute.
  • 22 curve presets + CSS cubic-bezier custom curves. The full lite-ease family plus identity; switch via dropdown or evaluate [p1x, p1y, p2x, p2y] tuples.
  • Real APCA 0.1.9. Not the WCAG 2 contrast ratio (which the APCA spec author calls flawed). Signed Lc score with polarity-aware noise clamp; APCA_THRESHOLDS ships the tier constants.
  • Brettel-Vienot-Mollon CVD simulation. Protanopia / Deuteranopia / Tritanopia — the matrices applied in linear sRGB, round-tripped to OKLCH.
  • CSS Color 4 gamut mapping. High-chroma OKLCH that exceeds sRGB clips in the chroma direction (not channel-by-channel in linear sRGB), preserving hue stability across the top of every scale.
  • Image-based seeding. extractPaletteFromImage(img, n) returns one neutral + n-1 hue-separated chromatic slots, suitable as createScale inputs.

Install

npm install @zakkster/lite-hueforge

Peer dependencies (install if not already present):

npm install @zakkster/lite-signal @zakkster/lite-color @zakkster/lite-color-engine @zakkster/lite-ease

Quick start

import { createPalette, createScale, toTokens } from '@zakkster/lite-hueforge';
import { effect } from '@zakkster/lite-signal';

// 1. Build a palette
const palette = createPalette('brand');
const primary = createScale({
    name:  'Primary',
    base:  { l: 0.55, c: 0.22, h: 268 },
    curve: 'ease-in-out-quad',
});
palette.addScale(primary);

// 2. React to changes — every step recomputes through the lite-signal graph
effect(() => {
    const steps = primary.steps();
    console.log('Primary 600:', steps[6]);   // { step: "600", l, c, h }
});

// 3. Edit any axis, watch it propagate
primary.setBase('h', 240);                   // shift hue -> all 12 steps update
primary.setCurve('ease-in-out-cubic');       // switch curve preset
primary.setCurve([0.42, 0, 0.58, 1]);        // OR a CSS cubic-bezier tuple

// 4. Export design tokens
console.log(toTokens(palette, 'tailwind'));  // tailwind.config.js snippet
console.log(toTokens(palette, 'json'));      // DTCG JSON
console.log(toTokens(palette, 'figma'));     // Tokens Studio JSON

API

Palettes & scales

createPalette(name?: string): Palette

Reactive palette container with a name signal, a scales array signal, and a shared palette.curve signal. Pass palette.curve to createScale({ curve }) to make a scale follow palette-wide curve changes.

createScale(opts): Scale

Build a reactive 12-step scale from a base OKLCH color and a curve preset.

createScale({
    name:  'Primary',                       // required
    base:  { l: 0.55, c: 0.22, h: 268 },    // required
    curve: 'ease-in-out-quad',              // preset name, bezier tuple, or shared signal
    minL:  null,                            // optional: lock step "50" L
    maxL:  null,                            // optional: lock step "1000" L
});

Edits to base.l/c/h, curve, minL, or maxL propagate to the 12 steps via the lite-signal graph. Default endpoints come from LMAP and a damped baseL offset (±0.6 × (baseL - 0.55), gamut-clamped) — adjust baseL to brighten or darken the whole scale; lock minL / maxL to override.

Curve presets (full lite-ease family + identity):

linear
ease-in-sine,  ease-out-sine,  ease-in-out-sine
ease-in-quad,  ease-out-quad,  ease-in-out-quad
ease-in-cubic, ease-out-cubic, ease-in-out-cubic
ease-in-quart, ease-out-quart, ease-in-out-quart
ease-in-quint, ease-out-quint, ease-in-out-quint
ease-in-expo,  ease-out-expo,  ease-in-out-expo
ease-in-circ,  ease-out-circ,  ease-in-out-circ

Legacy aliases 'ease-in-out' / 'ease-in' / 'ease-out' are also accepted (map to -quad / -cubic / -sine respectively for v0.1.x back-compat).

Custom bezier curves via setCurve([p1x, p1y, p2x, p2y]) — CSS-cubic-bezier semantics (P0 = (0,0), P3 = (1,1)). p1x and p2x are clamped to [0, 1] per spec; y axis can overshoot for springy curves.

getStep(scale, index): () => ScaleStep

Tracking accessor for one step (0..11).

selectStep(scaleOrGetter, indexGetter): () => ScaleStep

Reactive accessor for the step at a dynamic index — typically driven by a "selected step" UI signal. Use this instead of computed(() => scale.steps()[idx()]) — the naive version silently fails to re-fire when the index hasn't changed but the scale itself has been edited (see the zero-GC note below).

Accepts either a Scale directly or a function returning one (for the "active scale switches" pattern).

Color math

| Function | Signature | Notes | |---|---|---| | toHex | (color) => '#rrggbb' | OKLCH → hex, sRGB-cube clamped | | fromHex | (hex) => OklchColor | hex → OKLCH (3- and 6-char, with/without #) | | oklchToLinearSrgb | (L, C, H) => [r, g, b] | OKLCH → linear sRGB, gamut-clamped | | linearSrgbToOklch | (r, g, b) => OklchColor | inverse of above |

Round-trips preserve color within ±1 byte per channel.

Accessibility

apcaPair(text, bg): number

APCA 0.1.9 contrast between two OKLCH colors. Returns signed Lc:

apcaPair({ l: 0,    c: 0, h: 0 }, { l: 1,    c: 0, h: 0 });  //  106.0  black on white
apcaPair({ l: 1,    c: 0, h: 0 }, { l: 0,    c: 0, h: 0 });  // -108.0  white on black (reverse polarity)
apcaPair({ l: 0.55, c: 0, h: 0 }, { l: 0.55, c: 0, h: 0 });  //    0    identical -> below noise clamp

Readability tiers (use Math.abs(lc)):

| Lc | Tier | |---:|---| | ≥ 90 | preferred body text | | ≥ 75 | Tier 1: body text minimum (conventional "pass") | | ≥ 60 | Tier 2: medium-weight text | | ≥ 45 | Tier 3: large or decorative text | | < 30 | illegible |

Constants exposed as APCA_THRESHOLDS = { BODY: 75, MEDIUM: 60, LARGE: 45, MIN: 30 }.

simulate(color, mode): OklchColor

CVD simulation via Brettel-Vienot-Mollon matrices, applied in linear sRGB and round-tripped to OKLCH. Modes: 'none' | 'deuteranopia' | 'protanopia' | 'tritanopia'. Unknown modes pass through unchanged. Frozen mode list: CB_MODES.

Image extraction

extractPaletteFromImage(image, scaleCount = 5, opts?): OklchColor[]

Returns one neutral slot (averaged grayscale of the image) plus scaleCount - 1 chromatic slots separated by at least opts.minHueDistance degrees of hue (default 30°). Suitable for seeding a palette from a brand reference, photo, or screenshot. Browser-only: requires HTMLImageElement + Canvas 2D.

Slider tracks

bakeSliderTrack(canvas, axis, fixed, opts?)

Paint a slice-aware OKLCH gradient onto a <canvas>. The track shows how axis varies while the other two axes are held at fixed's values:

bakeSliderTrack(canvasEl, 'l', { l: 0, c: 0.22, h: 268 });
// L track from 0 -> 1 at the (C=0.22, H=268) slice

Re-bake on (C, H) change to keep the L track visually accurate to the slice the user is editing.

Exports

7 token formats, all dispatched via the same builder:

toTokens(palette, 'css');       // CSS Custom Properties
toTokens(palette, 'tailwind');  // tailwind.config.js snippet
toTokens(palette, 'scss');      // SCSS variables + Sass maps
toTokens(palette, 'json');      // DTCG JSON (Style Dictionary v4)
toTokens(palette, 'figma');     // Tokens Studio plugin JSON
toTokens(palette, 'swiftui');   // SwiftUI Color extension
toTokens(palette, 'android');   // colors.xml

Frozen list: EXPORT_FORMATS. Each format also has a direct export (toCssVars, toTailwindConfig, etc.) accepting format-specific options (e.g. { format: 'hex' } to emit #rrggbb instead of oklch() literals for legacy build chains).

All exporters read non-reactively. Call from an event handler or inside untrack().

Zero-GC slider drag

The headline performance claim. Each scale's stepsCache is a 12-slot array allocated once at createScale. Recompute walks the array and mutates slot.l/c/h in place; the array reference is reused across reads. The computed carries { equals: () => false } so downstream observers still fire.

// Under --expose-gc, on Node 22 sandbox:
//
//   scale.setBase('l', v) + read steps()    2.4M ops/s   0.003 B/op
//   scale.setBase('h', v) + read steps()    2.5M ops/s   0.000 B/op
//   5-scale palette, mutate one per tick    2.0M ops/s   0.000 B/op
//
// Real hardware (Apple Silicon, Node 23): ~2x these numbers.

Footgun avoided by selectStep: because the array reference is stable, the naive pattern computed(() => scale.steps()[idx()]) short-circuits lite-signal's Object.is dedupe — the slot ref is the same too, the L/C/H were just mutated in place. Use selectStep(scale, idx); it carries the { equals: () => false } lever and propagates correctly.

TypeScript

Full declarations ship as Hueforge.d.ts. All 28 exports typed, including CurvePreset (union of preset names), CurveBezier (4-tuple), CurveValue (the setCurve argument), ExportFormat, CbMode, OklchColor, ScaleStep.

Testing

node:test (no test-runner dependency), six files:

npm test            # 108 tests, fast
npm run test:gc     # adds --expose-gc; zero-GC contract engages
npm run bench       # internal throughput baseline

| File | Tests | Coverage | |---|---:|---| | test/01-core.test.js | 32 | palette + scale construction, steps generation, reactivity, getStep / selectStep, constants | | test/02-exports.test.js | 28 | toCssVars, toTailwindConfig, toScss, toJsonTokens, toFigmaTokens, toSwiftUI, toAndroidXml, toTokens dispatcher | | test/03-color-math.test.js | 12 | toHex / fromHex round-trips, OKLCH ↔ linear-sRGB, gamut clamping, 3-char / 6-char hex parsing | | test/04-a11y-and-simulation.test.js | 15 | APCA 0.1.9 (polarity, identical-pair, near-noise), APCA_THRESHOLDS, CB simulation per mode | | test/05-curves-and-image.test.js | 15 | extractPaletteFromImage (hue separation, neutral slot, scaleCount), evalCubicBezier, chroma-curve anchors | | test/06-zero-gc.test.js | 6 | cache identity (same array, same slots), heap-delta budget for single-scale L drag / 5-scale palette / H rotation | | total | 108 | |

The 06-zero-gc heap-delta tests skip without --expose-gc so npm test runs cleanly. npm run test:gc engages them.

License

MIT — © Zahary Shinikchiev.

Built on the @zakkster/lite-* zero-runtime-dependency ecosystem.