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

@hecto/ui-core

v3.0.0

Published

Framework-neutral contracts and state transitions shared by every Hecto adapter (React, Vue, Angular). This package owns **pure logic only** — no DOM, no framework runtime, no JSX. Anything here must be importable from a plain Node script without pulling

Readme

@hecto/ui-core

Framework-neutral contracts and state transitions shared by every Hecto adapter (React, Vue, Angular). This package owns pure logic only — no DOM, no framework runtime, no JSX. Anything here must be importable from a plain Node script without pulling in React/Vue/Angular.

The extraction pattern (template: Select)

When a component's interaction logic (open/closed, active index, keyboard navigation, controlled/uncontrolled resolution, etc.) needs to be identical across frameworks, extract it once as a pure reducer here, then bind it per framework with a thin adapter hook/composable/service.

  1. State + actions live in ui-core as data, not classes. SelectState is a plain readonly interface; SelectAction is a discriminated union. The reducer (reduceSelect(state, action): SelectState) is a pure function — same inputs always produce the same output, no side effects, no framework types anywhere in its signature.

    export interface SelectState {
      readonly open: boolean;
      readonly value?: string;
      readonly activeIndex: number;
    }
    export type SelectAction =
      | { type: "toggle" }
      | { type: "close" }
      | { type: "open" }
      | { type: "move"; direction: -1 | 1; options: readonly SelectOption[] }
      | { type: "select"; value: string; options: readonly SelectOption[] }
      | { type: "activate"; index: number };
    
    export function reduceSelect(state: SelectState, action: SelectAction): SelectState;
    export function createSelectState(
      options: readonly SelectOption[],
      value?: string,
      open?: boolean,
    ): SelectState;
  2. Each framework adapter binds it with the thinnest glue that framework offers, and nothing else. The reducer must not be reimplemented, wrapped in extra logic, or duplicated per framework.

    • React (packages/ui/src/react/useSelectState.ts): React.useReducer(reduceSelect, createSelectState(...)).
    • Vue: a computed/ref pair driven by the same reduceSelect calls inside a composable.
    • Angular: a signal updated by calling reduceSelect inside the component/service.

    The binding hook owns only wiring (framework state primitive ↔ reducer call). Business rules — which arrow key moves which direction, what select does to activeIndex, disabled-option skipping — stay in reduceSelect so all three frameworks get them for free and in sync.

  3. The public component API never changes shape because of this. Props, events, and exported types for the component (e.g. React's SelectProps) are untouched by the extraction — only the component's internal implementation swaps local useState calls for the shared reducer via the binding hook. Existing tests (unit, Storybook, etc.) must keep passing unmodified; if they don't, the extraction changed observable behavior and that's a bug in the port, not an acceptable side effect.

  4. Value commit vs. interaction state stay separate. Controlled/ uncontrolled value resolution (a prop-driven concern, e.g. React's useControllable) is a different responsibility from open/active-index interaction state, and is not folded into the ui-core reducer. Each framework keeps its own idiomatic controlled-value primitive and calls into the shared reducer for the rest.

Applying this to other components

Follow the same four steps for any component whose interaction logic must be identical across frameworks (Dialog's open/focus-restore transitions, Combobox filtering + active index, Carousel index clamping, etc.):

  1. Model the state as a plain readonly interface and the transitions as a discriminated-union action type.
  2. Write one pure reducer function in ui-core (plus an initializer if the state needs one).
  3. Add one binding hook/composable/service per framework, in that framework's package, that does nothing but wire the reducer into that framework's state primitive.
  4. Leave the component's public props/events and existing tests untouched — the reducer replaces internal useState calls, not the component contract.

DialogState/reduceDialog, cycleFocusIndex, and toggleValue in this package are smaller building blocks in the same spirit — reusable pure transitions extracted once so adapters don't reimplement them.

Styling entries

Three CSS exports, one JS rule: every class string a component renders lives in its own module under src/classes/ (shared runs in classes/shared.ts), re-exported through src/classes.ts so bundlers can drop unused components wholesale via the package sideEffects field.

| Entry | Contents | Use when | | --- | --- | --- | | @hecto/ui-core/styles.css | utilities + tokens (all themes) + semantic classes + framework-neutral global utilities | zero-build default: one import, everything works | | @hecto/ui-core/utilities.css | every component's compiled Tailwind utilities, no token definitions | you compose tokens/.vs-* layers yourself | | @hecto/ui-core/semantic.css | semantic .vs-* rules plus framework-neutral global utilities, no imports | the lean path below |

Global utility partials are authored in src/global-utilities/ and listed once in global-utilities.mjs. The full styles.css imports every partial. The build inlines those partials into semantic.css, while bounded dist/components/*.css leaves receive only the families referenced by their source closure. The partials are copied to dist/global-utilities/*.css as side-effectful CSS assets; do not hand-edit generated CSS.

Lean CSS: compile only what you use

styles.css ships every component's utilities and all 76 themes (~149 KB resolved, ~26 KB gz). A consumer already running Tailwind v4 can instead compile a sheet scoped to the components and themes they actually ship, because each component's class strings live in their own dist/classes/<component>.js module that @source can scan:

@import "tailwindcss/theme.css";
@import "tailwindcss/utilities.css";
@import "@hecto/tokens/base.css";
@import "@hecto/tokens/themes/dark.css"; /* just the themes you ship */
@import "@hecto/tokens/tailwind.css";
@import "@hecto/ui-core/semantic.css";
@theme { --shadow-*: initial; }
@source "./node_modules/@hecto/ui-core/dist/classes/shared.js";
@source "./node_modules/@hecto/ui-core/dist/classes/button.js";
@source "./node_modules/@hecto/ui-core/dist/classes/spinner.js";

Measured for Button (base + dark theme): ~66 KB raw / ~11 KB gz vs ~149 KB / ~26 KB for the full styles.css chain. Each extra component or theme adds roughly 1–3 KB.

Rules of the recipe:

  • Always scan classes/shared.js — cross-component runs (focus ring, floating panel, overlay scrim) live there, referenced by the other modules at runtime so their text never appears in the component files.
  • Scan what a contract composes from: combobox.js reuses select.js; popover.js and dropdown-menu.js reuse trigger-button.js; Button's loading state renders spinner.js.
  • semantic.css after the compiled utilities — it carries only the rules Tailwind can't express (keyframes, vendor pseudo-elements) and assumes tokens + utilities are already in scope.
  • Light theme is base.css's default paint; named themes override it via data-theme on <html>.

Owned native widget inputs

For repeated native Gantt or Kanban rendering, acquire a snapshot in the caller's scope when input data changes, then pass it as the component's snapshot prop:

import { createGanttSnapshot, createKanbanSnapshot } from '@hecto/ui-core'
import { createSelectSnapshot } from '@hecto/ui-machines/select'

const timeline = createGanttSnapshot({ rows, columns, getRowId: row => row.name })
const board = createKanbanSnapshot(cards)
const choices = createSelectSnapshot(options)
// Gantt({ snapshot: timeline, onRangeChange }), Kanban({ columns, snapshot: board })
// Select({ snapshot: choices, onValueChange })

Acquisition reads the documented fields once, copies immutable records and builds keyed indexes. Acquire again for every data revision. Duplicate IDs throw before rendering. Existing rows, cards and options props retain their general getter behavior; snapshots are an explicit alternative. Gantt derives shared geometry once and applies draft ranges only to the active bar. Kanban snapshots are controlled inputs: accept a move by supplying a new snapshot, or retain the old snapshot to reject it. Select preserves string versus record selection values; record values refer to the acquired immutable records.

Gantt pointer movement is coalesced per animation frame, refreshes layout after resize or scroll, and releases listeners on cancel, lost capture, track removal or disposal. A final pointer-up applies a pending move before requesting the drop.