@velo/ui (0.2.0)

Published 2026-10-29 13:17:53 +01:00 by r59q

Installation

@velo:registry=
npm install @velo/ui@0.2.0
"@velo/ui": "0.2.0"

About this package

@velo/ui

Game-neutral React command tiles, tooltips and the Debug Tools model-preview screen shared by Crownfall and Riftwake. Games supply their own model catalogues, preview factories and inspector content; the package never imports either game.

Install

The package is public on the Forgejo npm registry owned by velo. Add the scope to the consuming project's .npmrc:

@velo:registry=https://git.amhk.dk/api/packages/velo/npm/

React, ReactDOM and Babylon are peer dependencies, so the game owns exactly one copy of each. Declare them in the game itself:

npm install @velo/ui react@^19.3.0 react-dom@^19.3.0 @babylonjs/core@^9.29.0
npm install --save-dev @types/react@^19.3.0 @types/react-dom@^19.3.0

Every automatic release is a new 0.x.0 minor and ^0.N.0 accepts only 0.N.x, so adopting a release is a deliberate range and lockfile bump in the game.

Imports

Everything is exported from the package root:

import {
  CommandTile,
  CommandTooltipProvider,
  useTooltip,
  DebugTools,
  DebugModelPreview,
  PreviewAnimationControls,
  filterDebugModels,
  createInspectionPreview,
  framePreviewModel,
  PreviewPlayback,
  type CommandTileProps,
  type UseTooltipOptions,
  type TooltipAnchorProps,
  type DebugToolsProps,
  type DebugModel,
  type ModelPreview,
  type PreviewModel,
  type PreviewModelFactory,
  type PreviewAnimation,
  type PreviewPlaybackState,
} from '@velo/ui';

The built JavaScript is plain ES modules with .js specifiers, including Babylon deep imports such as @babylonjs/core/scene.js, so it loads in Node (for unit tests) as well as in bundlers. JSX consumers compile with "jsx": "react-jsx" under either NodeNext or Bundler resolution. Babylon's declarations reference WebGPU types, so consumers use skipLibCheck (as both games do) or install @webgpu/types.

Stylesheet

The Debug Tools styles are a separate file and are not imported by the JavaScript. Load @velo/ui/debug-tools.css where you mount DebugTools, for example next to its lazy import:

if (new URL(location.href).searchParams.get('debug') === 'tools') {
  const { DebugTools } = await import('@velo/ui');
  await import('@velo/ui/debug-tools.css');
  root.render(<DebugTools gameName="Game" gamePath={import.meta.env.BASE_URL} {...loaders} />);
}

CommandTile uses the class names command-tile, tile-button, tile-badge and tile-tooltip (with data-open) and ships no styles; each game styles its own palettes.

Tooltips for any element

useTooltip gives a single element a styled tooltip without CommandTile's palette conventions. Spread anchorProps on the element (its ref, hover and focus handlers and aria-describedby) and render tooltip next to it. The tooltip shows on hover and keyboard focus and hides on pointer leave, blur and Escape. The element keeps its own accessible name and should not also carry a title.

import '@velo/ui/tooltip.css';
import { useTooltip } from '@velo/ui';

function ToolButton({ panelRef }: { panelRef: RefObject<Element | null> }) {
  const id = useId();
  const { anchorProps, tooltip } = useTooltip({
    id,
    side: 'below', // 'above' | 'below' | 'right'; flips to the opposite side when it does not fit
    boundsRef: panelRef, // optional: stay inside this element instead of the whole viewport
    content: (
      <>
        <strong>Paint texture</strong>
        <span>Choose a texture, then hold the left mouse button to paint.</span>
      </>
    ),
  });
  return (
    <>
      <button type="button" aria-label="Paint texture" {...anchorProps} />
      {tooltip}
    </>
  );
}

The tooltip is a .ui-tooltip portal on document.body. @velo/ui/tooltip.css styles it from the custom properties --ui-tooltip-background, --ui-tooltip-border, --ui-tooltip-ink and --ui-tooltip-muted; set them on :root (per theme) because the tooltip is outside the game's containers. Unlike CommandTile, each useTooltip is independent and needs no provider.

Lifecycle obligations

  • Tooltips. Wrap every palette that should share one open tooltip in a single CommandTooltipProvider; a tile outside a provider never opens its tooltip. Each tooltip is a portal on document.body with the given tooltipId, so ids must be unique in the document. An open tooltip listens for window resize and capture-phase scroll to stay inside the viewport and removes those listeners when it closes or unmounts. A tile whose palette is collapsed (no layout) closes its tooltip.
  • Debug Tools. loadModels returns the game's catalogue and loadPreviewModel returns its PreviewModelFactory; both may lazily import game code. Keep both functions stable (module-level or memoised), because a new identity reloads the catalogue or preview. Game-specific inspectors are passed as children. The screen never reads or writes session storage. Return to {game} links to gamePath without the debug parameter; when the screen is a popup opened from that game page on the same origin, the link focuses the opener and closes the popup instead of navigating, so a popup that inherited the player's session never starts a second session.
  • Model preview. createInspectionPreview(canvas, modelId, factory, onError, onPlaybackChange) owns one Babylon Engine, Scene, camera, light, resize observer, context-menu listener and PreviewPlayback. Call dispose() exactly when the preview is no longer shown; it is safe during loading and idempotent. The factory must create everything inside the supplied scene, so scene disposal releases it; it must not touch gameplay state. DebugModelPreview does this for React.
  • Playback. PreviewPlayback samples clips on an inspection clock in seconds, never the gameplay clock. Loops reload the clip so procedural state restarts. After dispose() it ignores every call.
  • Framing. framePreviewModel fits enabled, visible meshes under the root and throws a readable error for empty or degenerate geometry.

Browser checks for the canvas, camera and data attributes (data-camera-*, data-animation-time) live in each game's debug-tools browser spec; the shared scenario helper stays in the repository tests/ and drives the screen through its public DOM only.

Local development

From this directory:

npm ci
npm run typecheck      # sources and tests
npm run lint
npm test               # playback, framing, tooltip and Debug Tools contracts (happy-dom)
npm run build          # dist/ with JavaScript, declarations, source maps and debug-tools.css
npm run test:consumer  # pack, install into a clean project, type-check JSX and load JS + CSS

test:consumer installs the tarball with only the declared peers and type packages, fails on any out-of-range peer or second React/ReactDOM/Babylon copy, and rejects built imports of CSS, undeclared modules or repository source. Pass -- --keep to keep the temporary project.

Optional local linking

To try unpublished changes in a game that already depends on @velo/ui, build here and install the directory into the game without saving it:

npm run build
cd ../../games/<game> && npm install --no-save ../../packages/ui

Until each game migrates, the games still import this package's src/ directly. Their Vite configs dedupe React and Babylon for that, but TypeScript and tsx resolve those imports from packages/ui/node_modules when it exists, which yields duplicate Babylon/React types in root checks. Remove packages/ui/node_modules before running root type-checks or game unit tests.

Publishing

The Publish UI workflow (.forgejo/workflows/publish-ui.yml) runs on pushes to main that change this directory and on manual dispatch. It runs the checks and the packed-consumer check before it configures the registry token, then picks the next 0.x.0 above the registry's versions, builds and publishes to https://git.amhk.dk/api/packages/velo/npm/. The committed version is not the release version. The PACKAGE_REGISTRY_TOKEN Actions secret must belong to an account with package write access.

Dependencies

Development dependencies

ID Version
@babylonjs/core ^9.29.0
@happy-dom/global-registrator ^21.0.1
@types/node ^26.6.4
@types/react ^19.3.0
@types/react-dom ^19.3.0
eslint ^10.12.0
react ^19.3.0
react-dom ^19.3.0
tsx ^4.23.15
typescript ^5.9.3
typescript-eslint ^8.71.1

Peer dependencies

ID Version
@babylonjs/core ^9.29.0
react ^19.3.0
react-dom ^19.3.0
Details
npm
2026-10-29 13:17:53 +01:00
67
UNLICENSED
latest
26 KiB
Assets (1)
ui-0.2.0.tgz 26 KiB
Versions (2) View all
0.2.0 2026-10-29
0.1.0 2026-10-15