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

@forgeax/engine-ui

v0.3.11

Published

Browser UI is an asset plus a small behavior island. The asset package owns the stable HTML/CSS payload; game code supplies dynamic text, template clones, input and lifecycle.

Downloads

11,461

Readme

@forgeax/engine-ui

Browser UI is an asset plus a small behavior island. The asset package owns the stable HTML/CSS payload; game code supplies dynamic text, template clones, input and lifecycle.

[!IMPORTANT] engine-ui is browser-only. A headless consumer can validate/import the asset, but must not call mountUi without a DOM.

Evidence and recovery

UI authoring is a producer boundary. The source declaration and its cook receipt are joined with catalog packageUrl/cookReceiptUrl and Pack v2 HTML/CSS/artifact verification as AssetEvidence. Preview and runtime mounting do not infer readiness from the catalog alone.

Use notCooked, ready/current, ready/stale, and unknown as distinct states; artifact/package results are notChecked, passed, or failed. Recover by fixing the authoring source or recooking, then run lookup/verify --guid --project --catalog --json. A headless probe can validate evidence, but mountUi still requires a DOM.

Shortest recipe

Headless validation is available without a DOM:

import { validateUiAuthoring } from '@forgeax/engine-ui/authoring';

const checked = await validateUiAuthoring({ sourcePath: 'hud.ui.html', html, css });
if (!checked.ok) return emitJson(checked.error.detail.diagnostics);

Use checked.value.category (native, normalizable, or runtime-bound) and checked.value.diagnostics as the machine-readable authoring result. The source strings are returned byte-for-byte unchanged.

import { mountUi } from '@forgeax/engine-ui';

const loaded = assets.loadByGuid<UiAsset>(HUD_GUID);
if (!loaded.ok) return report(loaded.error);
const mounted = mountUi(loaded.value, { root: ctx.uiRoot, layer: 60 });
if (!mounted.ok) return report(mounted.error);
ctx.effect(() => () => mounted.value.dispose(), 'game/hud');

Preview and deterministic capture

The public preview seam keeps browser-only work behind @forgeax/engine-ui/preview:

import {
  captureUiPreview,
  createDomPartScenario,
  createUiPreviewSession,
} from '@forgeax/engine-ui/preview';

const session = createUiPreviewSession({
  guid: HUD_GUID,
  assets,
  root: previewRoot,
  rect: { width: 320, height: 180 },
  scenario: createDomPartScenario({ requiredParts: ['root', 'score'] }),
});
const opened = await session.open();
if (!opened.ok) return recover(opened.error.code, opened.error.hint);
const captured = await captureUiPreview(session, browserAdapter);
if (!captured.ok) return recover(captured.error.code, captured.error.detail);
await writePng(captured.value.png);
await writeJson(captured.value.evidence);

When subscribing to asset changes, await the listener result. A subscribed target refresh that rebuilds an invalid revision returns structured preview-load-failed; read error.detail.diagnostics for each source path/range and repair hint, then call session.retry() after fixing the source. Do not parse message.

sequenceDiagram
  participant A as AI or CI
  participant S as Preview session
  participant B as Browser adapter
  A->>S: open(GUID, scenario)
  S-->>A: mounted or closed error
  A->>B: readiness + freezeClock
  B-->>A: viewport, scale, fonts, resources, failures
  A->>S: captureUiPreview(session, adapter)
  S->>B: screenshot only when every gate passes
  B-->>S: PNG bytes
  S-->>A: { png, evidence }

The adapter must report viewport, deviceScale, fonts, resources, scenario, and clock as true, with empty console, page, and request failure arrays. Its screenshot seam should return the actual mounted host screenshot from the browser compositor, not a fixture byte string. evidence records the frozen clock, discovered data-ui-part names, DOM host count, focus, resource failures, and lifecycle state. Keep the PNG and evidence together; the image is visual context, while the JSON is the functional authority.

| Capture code | Recovery action | | --- | --- | | capture-not-ready | Read error.detail.unmet, satisfy each named gate, then retry in the same fixed browser. A failed result never has png. | | capture-failed | Read error.detail.stage, repair the adapter's screenshot capability, and retry. | | preview-scenario-missing-part | Restore the declared data-ui-part and call retry() or rebuild the session. | | preview-load-failed | Inspect error.detail.diagnostics for source-located repair hints, fix the target source, then call retry(). |

[!WARNING] Do not treat a screenshot as proof that fonts, companions, focus, or disposal succeeded. Do not parse message, mount a fallback into document.body, or replace a missing companion with a stand-in asset.

flowchart LR
  source["*.pack.json + UiAsset GUID"] --> load["loadByGuid<UiAsset>"]
  load --> mount["mountUi(root, layer)"]
  mount --> instance["UiInstance: host + open ShadowRoot + signal"]
  instance --> behavior["dynamic DOM/framework island"]
  behavior --> dispose["dispose on Stop/pagehide"]

Producer authoring descriptor

Every kind: 'ui' catalog row publishes the versioned AssetAuthoringCapability.ui descriptor from @forgeax/engine-types. It is the discovery contract for AI and editor consumers: preview names createUiPreviewSession, mount names the mountUi lifecycle and onAction port, and the state, action, and read projections point to the host's JSON-shaped gameProjection seam. Input and navigation use the DOM-native focus/ownership contract; font files remain producer-owned UI artifact companions; localization exposes the native i18next operation through the opt-in localization entry.

This descriptor exposes existing producer seams. It does not give an editor permission to inspect a game World or invent a second binding registry. Runtime action/read IDs and their semantic bodies stay owned by the game bootstrap, while the Gateway and Content Browser project the catalog facts unchanged.

Authoring and resources

Profile and diagnostics

The versioned profile is exported as UI_AUTHORING_PROFILE and described by src/authoring/profile.schema.json. Classification precedence is runtime-bound then normalizable then native; quality findings remain warnings and do not change an otherwise accepted result.

Blocking diagnostics use the shared import shape: code, severity, sourcePath, sourceRange, rule, expected, actual, hint, and optional relatedLocations. Branch on error.code === 'source-validation-failed' and inspect error.detail.diagnostics; do not parse error messages.

Normalizable surfaces (inline style, global selectors, root or parent URLs, generated classes) are reported without changing source. Runtime-bound surfaces (scripts, inline handlers, remote URLs, CSS imports, and CSS-in-JS markers) must be moved to a consumer-side framework island.

Authoring sources (.ui.html plus same-name .ui.css) remain the source-to-payload input for consumers that own an importer. The importer pairs the files and records relative image/font reads as private companions. The manifest owns exactly one public GUID; companions do not receive consumer GUIDs. Shared/demo author sources remain in the assets submodule; a consumer template may carry a small, template-owned pair under its own assets/ root when the UI is part of that template's source closure. Use the pair for readable multiline authoring and keep the prepared .pack.json output generated.

The standalone DevKit host includes createUiImporter() in its default build-time importer composition. A custom Vite host should register the same self-declared { key: 'ui', import, finalize } owner directly in its pluginPack({ importers: [...] }) list; do not maintain a second project registry or wrap it with another key.

The sidecar's source names the .ui.html file, and the importer derives the same-directory .ui.css companion. The generated Pack keeps the runtime UiAsset shape (html and css strings); source readability and runtime payload format are separate concerns.

The *.pack.json file remains the canonical contract for a deliberately prepared UI asset, such as a small hand-authored HUD or settings panel. It owns exactly one public GUID and stores the final HTML/CSS payload directly, so loading it does not invoke an importer, generate DDC, or require an ImportTransport. A consumer-owned prepared pack belongs in that consumer's assets/ui directory; a template that needs readable multiline UI should keep the .ui.html / .ui.css source pair and generate this Pack instead.

| Concern | Owner | | --- | --- | | HTML and CSS payload | UI pack | | GUID and build transport | pack/import pipeline | | dynamic text, events, template clones | game module | | mount/dispose and ShadowRoot | engine-ui | | run root and cleanup order | host (apps/preview) |

Dynamic nodes and framework seam

Use ordinary DOM APIs inside the open shadow root. A popup can be cloned from a <template data-ui-template> and removed through UiInstance.signal. Frameworks may mount an island into an element that the asset exposes; the caller owns createRoot, unmount, and any portal target. The package does not ship React/Vue/Svelte adapters or claim framework certification.

const island = instance.host.shadowRoot?.querySelector('[data-framework-island]');
if (island) {
  const root = createRoot(island);
  instance.signal.addEventListener('abort', () => root.unmount(), { once: true });
}

Input, modal and lifecycle

Interactive asset elements opt in with pointer-events: auto. The input package decides UI ownership from event.composedPath() and clears held state when ownership changes, focus leaves the window, or the run is stopped. A single settings modal owns its own inert backdrop, focus trap, Escape close, and focus restore. Settings in the default template are memory-only for the current run; no localStorage authority is implied.

dispose() is idempotent: it aborts signal, removes listeners and then removes the host. Hosts should dispose instances in reverse registration order and remove the run-scoped uiRoot on Stop/pagehide.

Errors and prohibited fallbacks

Handle UiError by its closed code union and machine-readable detail; do not parse message strings. Do not silently mount to document.body, create a second UI manager, inject stable markup from TS, or hide a missing asset behind an empty root.

The independent Gallery/Preview Workbench, scenario matrix, visual baseline workflow, external design adapters, and AI iteration workbench remain follow-up design: 2026-07-21-html-css-ui-authoring-workflow-ecosystem-design.md.

Offline localization with i18next

Import createUiLocalization, bindUiLocalization, and refreshUiLocalization from @forgeax/engine/ui/localization. This opt-in entry uses native i18next instances; UI mounting and the Engine runtime root do not load its translation implementation.

| Boundary | Contract | | --- | --- | | Source | .ui.html.meta.json#importSettings.localization names a relative JSON companion. No remote URLs or parent traversal. | | JSON | { fallbackLng, defaultNS, resources }, with native i18next JSON v4 language/namespace bundles and string leaves. Arrays and nested keys are accepted; prototype keys, invalid fallback namespaces, sources over 1 MiB and trees beyond 16 levels are rejected. | | Delivery | Import tracks the JSON as a source dependency and embeds it in the same UI payload/GUID. Pack finalization, Catalog, prepared Packs, and offline loading use the existing route. | | Runtime | createUiLocalization(asset, { lng, onMissingKey? }) returns UiResult<i18n>. Each call creates independent language and resource state. Use native t, changeLanguage, exists, and dir. | | UI lifetime | bindUiLocalization(ui, i18n, render) renders immediately and on native language/resource events. UI abort detaches every subscription; its return value also detaches manually. | | Revision | Validate/load the newly published same-GUID asset, then await refreshUiLocalization(i18n, asset). It replaces the native resource snapshot, removes deleted keys, recalculates fallback resolution, and notifies bound UI once. Invalid data returns invalid-asset and preserves the previous snapshot. | | Text keys | Derive i18next CustomTypeOptions.resources from the same authored JSON import; do not maintain a second key union. |

flowchart LR
  json["Translation JSON + UI Meta"] --> importer["UI importer: validate + track dependency"]
  importer --> payload["One UI GUID / Pack payload"]
  payload --> instance["Project-owned i18next instance"]
  instance --> binding["Native events / UI AbortSignal"]
  binding --> dom["Game-owned textContent and attributes"]
import { createUiLocalization, bindUiLocalization } from '@forgeax/engine/ui/localization';

const localized = await createUiLocalization(asset, {
  lng: 'fr-CA',
  onMissingKey: (languages, namespace, key) => reportMissing({ languages, namespace, key }),
});
if (!localized.ok) return report(localized.error);
const i18n = localized.value;
const title = ui.host.shadowRoot?.querySelector('[data-ui-part="title"]');
bindUiLocalization(ui, i18n, (t) => {
  if (title) title.textContent = t('inventory', { count: items.length, name: player.name });
  ui.host.lang = i18n.resolvedLanguage ?? i18n.language;
  ui.host.dir = i18n.dir();
});
await i18n.changeLanguage('en');

For typed game code, add a project-local declaration using the authored resource:

import type messages from './hud.ui.i18n.json';
declare module 'i18next' {
  interface CustomTypeOptions {
    defaultNS: 'game';
    resources: typeof messages.resources.en;
  }
}

Enable TypeScript resolveJsonModule in that project. This declaration belongs to the consumer; Engine does not globally augment i18next with one game's keys.

[!IMPORTANT] Interpolation escaping is disabled because the consumer writes DOM text and attributes. Never put translated content into innerHTML. Consumers own plural counts, interpolation values, text direction, accessibility attributes, dynamic nodes, and game-specific diagnostics. No backend, language detector, network request, localStorage, or translation frame system is installed.

The game-3d control guide demonstrates packaged English/French resources and native language actions. The vase tool retains its own English business text. A consumer handling Catalog revisions must fence asynchronous loads with its UI signal before refreshing; refreshUiLocalization does not subscribe to Catalog or own asset lifetimes.

Run node packages/ui/scripts/bench-localization.mjs after building to retain hardware, sampling conditions, initialization/lookup/refresh times and bundle bytes in .forgeax-debug/g09/benchmark.json. The template smoke also measures actual language actions and saves English/French screenshots. The production probe additionally switches the browser offline after loading; the development smoke retains its live HMR transport. After forgeax project build --root templates/game-3d --out-dir .forgeax/g09-production, run node apps/preview/scripts/smoke-game3d-localization-production.mjs from the repository root. This contributor gate serves only the built files, rejects external requests, requires 60 completed frames and a nonblank canvas, and saves offline action evidence.