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

@labpics/colors

v0.10.0

Published

Contextual compiler and runtime resolver for client-owned color-token contracts. WASM core, zero runtime dependencies.

Readme

@labpics/colors

Агностичный контраст-движок для дизайн-систем. Получает фоновый цвет и тему — возвращает полный набор цветовых ролей вашей системы. Словарь ролей не встроен в пакет: его задаёт конфиг дизайн-системы (ThemeConfig), загружаемый через loadConfig; имена вида --lab-label-primary, --lab-border-base в примерах ниже — из конфига дизайн-системы labui. CSS-переменные несут готовое значение oklch(L% C H) (для полупрозрачных ролей — oklch(L% C H / A)); сырой #RRGGBB остаётся данными роли (roles.<ключ>.hex). Ядро написано на Rust и скомпилировано в WebAssembly; пакет не имеет runtime-зависимостей.

Ядро возвращает данные, не затрагивает DOM. Три вспомогательные функции переводят эти данные в живые CSS-переменные: applyTheme (разовое применение), watchTheme (реактивное — обновляется при изменении фона) и adaptTheme (плавная адаптация для фона, меняющегося каждый кадр).


Установка

Пакет публикуется в npm-реестр Labpics:

npm install @labpics/colors

При сборке из монорепо:

npm run build   # → pkg/ (wasm + JS-обёртка + .d.ts)

Пакет экспортирует @labpics/colors/build-metadata.json — self-declared machine-readable metadata конкретной сборки: npm/core versions, exact source SHA, digest и SHA-256 conformance manifest/family set, а также размер и SHA-256 WASM-байтов. Release-gate сверяет объект целиком с исходными файлами и повторяет проверку после чистой установки tarball. Это integrity metadata внутри артефакта, не криптографическая provenance/Sigstore-аттестация, не runtime telemetry и не сетевой запрос.


Как использовать

Разовое применение

import init, { LabColors, applyTheme } from "@labpics/colors";
import dsConfig from "./theme.config.json"; // паспорт вашей дизайн-системы (ThemeConfig)

await init();                                // загрузить WASM-модуль (один раз)
const engine = new LabColors();              // пустой движок: словаря ролей ещё нет
engine.loadConfig(JSON.stringify(dsConfig)); // без этого шага resolveTheme отклонится: config_required

const result = engine.resolveTheme("#FFFFFF", "light");
// result.vars  → готовые CSS-значения по ролям ВАШЕГО конфига:
//                { "--lab-label-primary": "oklch(<L>% <C> <H>)", … }
// result.roles → детали каждой роли (css, hex, контраст, флаги)

applyTheme(document.documentElement, result);   // записать все --lab-* в элемент

Реактивное отслеживание

watchTheme синхронизирует переменные с фактическим фоном элемента автоматически.

import init, { LabColors, watchTheme } from "@labpics/colors";

await init();
const colors = new LabColors();
colors.loadConfig(JSON.stringify(dsConfig));   // конфиг дизайн-системы (см. квик-старт)

const panel = document.querySelector(".panel") as HTMLElement;
const watcher = watchTheme(panel, { colors, theme: "light" });
// panel теперь несёт правильные --lab-* для своего фона

watcher.setTheme("dark");   // переключить тему
watcher.refresh();          // принудительная пересинхронизация
watcher.stop();             // отключить наблюдателя

Поддерживает два режима изменений:

  • Дискретные (переключение темы, обновление класса, DOM-мутация) — отслеживаются MutationObserver автоматически.
  • Непрерывные (CSS-анимация, кадровый фон, который никогда не мутирует inline style) — управляются вручную через refresh() из собственного цикла requestAnimationFrame. refresh() дёшев: пересчитывает только при реальном изменении строки фона.

Плавная адаптация к анимированному фону

watchTheme пересчитывает весь набор при каждом изменении фона. Для фона, меняющегося каждый кадр (анимация, параллакс, размытый фон), adaptTheme — более элегантный вариант: проверяет дёшево каждый кадр, пересчитывает и плавно переходит к новым цветам только когда контраст реально ухудшается.

import init, { LabColors, adaptTheme, effectiveBackground } from "@labpics/colors";

await init();
const colors = new LabColors();
colors.loadConfig(JSON.stringify(dsConfig));   // конфиг дизайн-системы (см. квик-старт)

const surface = document.querySelector(".hero") as HTMLElement;
const adaptive = adaptTheme(surface, {
  colors,
  theme: "light",
  background: () => effectiveBackground(surface, { fallback: "#101012" }),
});

adaptive.start();           // запустить внутренний requestAnimationFrame-цикл
adaptive.setTheme("dark");  // смена темы применяется мгновенно
adaptive.stop();            // остановить цикл

Для фона с градиентом или изображением можно передать несколько образцов — цвета будут корректны для наиболее сложного из них:

adaptTheme(hero, {
  colors,
  theme: "light",
  strict: true,
  background: () => sampleBackdrop(hero),  // например ["#0B0B0E", "#3A3A40"]
});

Темы

| Имя темы | Назначение | |----------|------------| | "light" | Светлая тема | | "dark" | Тёмная тема | | "light-ic" | Светлая с повышенным контрастом | | "dark-ic" | Тёмная с повышенным контрастом |


Справочник публичного API

new LabColors()

Создаёт движок без словаря ролей: до loadConfig любой вызов resolveTheme отклоняется ошибкой config_required. Повторные одинаковые вызовы resolveTheme обслуживаются из внутреннего кэша (кэш-пространство привязано к отпечатку конфига).


engine.resolveTheme(bgHex, theme): ResolvedTheme

  • bgHex — фон в формате #RGB или #RRGGBB.
  • theme"light" | "dark" | "light-ic" | "dark-ic".

Возвращает объект ResolvedTheme:

interface ResolvedTheme {
  theme: ThemeName;
  background: string;                     // нормализованный #RRGGBB
  vars: Record<string, string>;           // достижимые роли: "--lab-<ключ>" → готовое CSS-значение oklch
  roles: Record<string, RoleResult>;      // все роли с деталями
}

type RoleResult =
  | SolvedColor
  | TranslucentRole
  | GlowRole
  | MaterialRole
  | NoneRole
  | UnreachableRole;

Каждая роль — одна из шести категорий результата; у Glow есть два терминальных состояния:

  • SolvedColor — цвет найден (kind: "color", поля css — готовое oklch(L% C H), hex — тот же цвет как данные, lc, wcagRatio, …).
  • TranslucentRole — полупрозрачная роль лестницы или альфа-аналога (kind: "translucent"): css — готовое oklch(L% C H / A), tintHex — тинт как данные, alpha, плюс compositeHex / compositeLc / compositeWcag — exact encoded-sRGB8 reference-композит на фоне резолва и его контраст. Конкретный renderer и color-management pipeline проверяются отдельно.
  • GlowRole — discriminated union. kind: "glow" несёт два цвета для mix-blend-mode: screen: cssVar — halo, ${cssVar}-core — core, ${cssVar}-alpha — каноническая alphaCss. compositeProfile / compositeGuarantee описывают exact point-композит отдельно от decisionGuarantee. layerRecipeProfile фиксирует общий рецепт слоёв "cam16-jprime-oklab-cusp-v1", а обязательный appearanceDiagnosticProfile — CAM16 appearance-модель результата. Отдельный selectionDiagnosticProfile сообщает, применялась ли CAM16-диагностика именно при выборе состояния: для exact no-op он равен null, для legacy solve — "cam16-ucs-jprime-li2017-v1". targetStatus различает эти ветви явно: "exact-noop-unreachable", "legacy-reached" или "legacy-unreachable". Determinate-форма — union из этих трёх согласованных ветвей: смешать stable/legacy profile, guarantee, selection diagnostic, status и degraded на уровне TypeScript невозможно. Outward interval не входит в Glow capability этого релиза. kind: "glow-indeterminate" означает, что профиль stable-v1 не выбрал target/max state без sound bound; для такой роли CSS-переменные не эмитятся и legacy fallback не применяется. Цель и все *CompositeHex / *AchievedDj относятся только к изолированным point-слоям, а не к полному blur/overlap-эффекту, браузеру или дисплею.
  • MaterialRole — union трёх терминальных исходов: satisfied transparent endpoint (alpha = 0), satisfied bisection bracket (alpha побитно равна upperAlpha) и degraded opaque endpoint (alpha = 1). alphaStatus, alphaGuarantee и compatibility-поле guaranteed согласованы типом. floor — запрошенный пол; он держится только при alphaStatus: "satisfied". Primary остаётся солид-каноном, -01 — тинтом, -02 — опаковой базой. Численный профиль явно фиксирует operation order: encoded-srgb-byte-scale-affine-platform-binary64-powf-v1: original WCAG 2.1 (2018) split 0.03928, conservative binary64 channel envelope и обе стороны пересечённого EOTF seam. Bracket доступен только после directed-search guard; это platform-characterization, а не двухугловая теорема, глобальная монотонность, первый passing state или точная минимальная alpha.
  • NoneRole — роль намеренно пустая по дизайну (kind: "none"), не ошибка.
  • UnreachableRole — ни один цвет не удовлетворяет требованиям для этого фона (kind: "unreachable").

Недостижимость отдельных ролей — часть успешного результата. Отклоняет весь вызов (как Error) только при невалидном аргументе:

| Код ошибки | Причина | |------------|---------| | config_required | конфиг ещё не загружен (loadConfig не вызывался) | | invalid_background | bgHex не является #RGB или #RRGGBB | | unknown_theme | theme не входит в список допустимых |


engine.loadConfig(json): string

Загружает конфиг дизайн-системы (JSON по типу ThemeConfig; схема — в репозитории: docs/decisions/0001-config-boundary.md, TS-типы — в поставляемом labcolors.d.ts). Это единственный источник словаря ролей — встроенной таблицы в движке нет: до загрузки конфига resolveTheme отклоняется ошибкой config_required. Полный preflight проверяет не только имена ролей, но и итоговый CSS-namespace: -core/-alpha Glow и -01/-02 Material не могут быть затёрты другой ролью или алиасом. Невалидный конфиг отклоняется структурной ошибкой invalid_config: … и НЕ меняет состояние. Возвращает отпечаток конфига — 16 hex-символов; разные конфиги дают разные отпечатки и разные кэш-пространства.

Каждый Glow-рецепт обязан явно выбрать numerical-decision profile; default и неявного legacy-пути нет:

{
  "kind": "glow",
  "source": { "kind": "brand" },
  "step": "base",
  "decision_profile": "stable-v1"
}

stable-v1 возвращает typed glow-indeterminate, когда sound bound для нетривиального CAM16 target/max-решения недоступен. Если существующая интеграция обязана пока сохранить прежнюю CSS-эмиссию, укажите legacy-platform-dependent-v1 явно и обрабатывайте его как compatibility profile, а не как численную гарантию. Полная миграция с rollback-порядком — в гайде exact alpha / typed Glow.


engine.recheckContrast(bgHex, fgHexes, theme): Float64Array

Дешёвая покадровая проверка: какие контрасты дают цвета fgHexes на фоне bgHex под темой theme, без полного резолва (один прямой ход модели на фон плюс по одному на каждый передний план). Возвращает Float64Array пар [lc, wcagRatio] в порядке fgHexes: индекс 2·i — знаковый Lc цвета i, 2·i+1 — его WCAG-отношение. Это примитив, которым adaptTheme решает, пора ли пересчитывать.


engine.recheckContrastMulti(bgHexes, fgHexes, theme): Float64Array

Батч-вариант recheckContrast для меняющегося фона (градиент / картинка / bg-blur / стекло): проверяет один набор fgHexes сразу против НЕСКОЛЬКИХ сэмплов фона bgHexes за один вызов, разделяя прямой ход модели каждого переднего плана между всеми сэмплами (он от фона не зависит). Байт-в-байт равен N отдельным вызовам recheckContrast, пара за парой — это закреплено parity-тестом границы, — только быстрее: ~2.5× на 3 сэмплах. Возвращает плоский background-major Float64Array: сэмпл s, цвет i лежит в (s · fgHexes.length + i) · 2 (lc) и +1 (wcagRatio). Именно этим вызовом adaptTheme схлопывает worst-case цикл по сэмплам в одно обращение к движку; на одном сэмпле выигрыша нет — контроллер остаётся на recheckContrast.


engine.muddiness(hex): number

muddiness возвращает замороженный числовой выход исторической формулы. Это experimental compatibility proxy: legacy-имя сохранено для совместимости, но значение не является валидированным на наблюдателях человеческим вердиктом clean/dirty и не должно использоваться как production decision. Сам диапазон [0, 1] — свойство формулы, а не шкала человеческого восприятия.


applyTheme(element, result): void

Записывает все достижимые роли из result.vars в element.style через setProperty. Устаревшие --lab-* от предыдущего вызова сбрасываются перед записью — роль, потерявшая достижимость, не остаётся висеть.


watchTheme(element, options): WatchController

Синхронизирует --lab-* переменные с фактическим фоном элемента. Применяет немедленно при создании и возвращает контроллер.

interface WatchThemeOptions {
  colors: LabColors;
  theme: ThemeName;
  background?: string | (() => string);  // явный фон (если автоматический невозможен)
  target?: HTMLElement;                  // куда писать переменные (по умолчанию: element)
  fallback?: string;                     // фон при полностью прозрачной цепочке (по умолчанию "#FFFFFF")
  observe?: boolean;                     // авто-обновление при DOM-мутациях (по умолчанию true)
  root?: Node;                           // корень MutationObserver (по умолчанию: documentElement)
}

interface WatchController {
  refresh(force?: boolean): ResolvedTheme | null;  // пересчитать и применить, если фон изменился
  setTheme(theme: ThemeName): void;                // переключить тему и применить
  background(): string;                            // последний вычисленный фон (hex)
  stop(): void;                                    // отключить наблюдателей
}

Для поверхности над изображением, градиентом или размытым фоном — где автоматическое вычисление невозможно — передайте явный background (hex-строку или функцию, возвращающую hex).


adaptTheme(element, options): AdaptController

Режим с dwell-фильтром: дешёвая проверка каждый кадр, пересчёт и плавный переход только при устойчивом ухудшении контраста. Это один порог с выдержкой, не двухпороговый гистерезис. Применяет немедленно и возвращает контроллер.

interface AdaptThemeOptions {
  colors: LabColors;
  theme: ThemeName;
  background?: string | string[] | (() => string | string[]);
                                     // один hex или несколько образцов фона (наихудший учитывается)
  target?: HTMLElement;              // куда писать переменные (по умолчанию: element)
  fallback?: string;                 // фон при прозрачной цепочке (по умолчанию "#FFFFFF")
  dropFraction?: number;             // запас контраста до пересчёта (по умолчанию 0.2)
  sustainMs?: number;                // минимальное время удержания нарушения (по умолчанию 120)
  dwellMs?: number;                  // минимальный интервал между пересчётами (по умолчанию 250)
  easeMs?: number;                   // длительность перехода (по умолчанию 280; уменьшается при reduced-motion)
  strict?: boolean;                  // legacy characterized clamp; не universal floor certificate (по умолчанию false)
  reducedMotion?: boolean;           // переопределить системную настройку
}

interface AdaptController {
  tick(now?: number): void;          // один шаг (или использовать start())
  setTheme(theme: ThemeName): void;  // переключить тему мгновенно
  start(): void;                     // запустить внутренний requestAnimationFrame-цикл
  stop(): void;                      // остановить цикл
  current(): Record<string, string>; // текущие применённые --lab-* переменные
}

strict сохраняет прежнее runtime-поведение, но не является доказательством минимального или проходящего состояния на каждом кадре: путь Oklab→gamut clip→sRGB8 немонотонен. Finite hard-floor-safe replacement ведётся в #287; до него этот флаг следует считать compatibility characterization.

Управляйте через start() (внутренний rAF-цикл) или вызывайте tick() из собственного цикла. Смена темы применяется мгновенно — это осознанное намерение, а не дрейф. Перед применением результата контроллер проверяет provenance как stable-, так и legacy-ветвей Glow. Нетипизированный внешний mock с невозможным сочетанием profile/guarantee/diagnostic/status отклоняется TypeError; fallback-цвет из такого объекта не применяется.


effectiveBackground(element, options?): string

Возвращает непрозрачный reference-#RRGGBB, вычисленный для поддерживаемого подмножества DOM background-color. Это не browser pixel capture и не сертификат цвета, который реально видит наблюдатель. Helper обходит цепочку предков и композитит распознанные слои поверх fallback (по умолчанию белый).

const bg = effectiveBackground(panel);                        // например "#0F1014"
const bg2 = effectiveBackground(panel, { fallback: "#101012" });

Честное ограничение: работает только с поддерживаемыми сплошными и полупрозрачными background-color; неподдерживаемый CSS, неполная прозрачная цепочка, background-image, градиент, blur и video не дают полного наблюдения. Текущий compatibility helper ещё может отбросить неподдерживаемый слой или использовать fallback; для correctness передайте фон явно. Typed Unknown replacement принадлежит #283.

Дополнительно экспортируются вспомогательные функции для работы со слоями: parseCssColor, compositeOver, compositeStackToHex, toHex и oklabLerp (перцептуально равномерная интерполяция между двумя hex-значениями — траектория цвета при переходе).


Размер бандла

Размер не закреплён в документации приблизительным числом: оно устаревает при любом изменении солвера. SSOT — шаг CI report bundle size (gzip) в ci.yml, который для каждого коммита печатает точные raw-байты и результат gzip -9 отдельно для labcolors_bg.wasm и wasm-bindgen-обёртки labcolors.js.

Это весь движок: CAM16, солверы контраста, лестницы и граница конфига. .wasm поставляется отдельным ассетом. Будет ли его загрузка критическим путём первого рендера, определяет интеграция: до первого resolveTheme инициализация обязана завершиться; приложение может preload/кэшировать модуль. JS-хелперы (applyTheme, watchTheme, adaptTheme, effectiveBackground) имеют именованные экспорты и допускают tree-shaking, но их размер также следует мерить сборкой, а не описывать приблизительно.

Release packer закреплён на Node 24.14/npm 11.9; это не consumer floor. Публичный контракт Node >=22.11.0 независимо прогоняется отдельным CI job. Browser- матрица использует прямую передачу wasm-байтов в init({ module_or_path }) и headless Chrome из CI. ESM/URL-обёртка генерируется wasm-pack, но совместимость с конкретной версией Vite, webpack, Next или другим браузером не заявляется без отдельного smoke-гейта.

Для аудиторов цепочки поставки

  • Build metadata: экспорт @labpics/colors/build-metadata.json декларирует source SHA и conformance pack для установленного WASM; verifier отклоняет любое лишнее, отсутствующее или несовпадающее поле. Объект не подписан и не заменяет отключённую npm/Sigstore provenance-аттестацию.
  • Network access (Socket и др.): единственный fetch в пакете (pkg/labcolors.js) загружает СОБСТВЕННЫЙ .wasm-файл пакета при init(url) — стандартный лоадер wasm-bindgen. Ни внешних адресов, ни отправки данных, ни исполнения при импорте. В node-пути (передача байтов) fetch не вызывается.
  • Bundlephobia BuildError: их webpack-конвейер не умеет .wasm-ассеты («loader customization needed») — так падает почти любой WASM-пакет. Реальный размер конкретного коммита показывает CI-шаг report bundle size (gzip).
  • Zero runtime JS-dependencies: npm-поле dependencies пусто — транзитивной JS/npm-цепочки поставки нет. Rust-крейты сборки (serde, serde_json и др.) компилируются ВНУТРЬ .wasm (учтены CI-замером); их цепочка аудируется на стороне сборки — cargo audit (RustSec) в CI lab-colors.