@velo/ui (0.1.0)
Installation
@velo:registry=npm install @velo/ui@0.1.0"@velo/ui": "0.1.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,
DebugTools,
DebugModelPreview,
PreviewAnimationControls,
filterDebugModels,
createInspectionPreview,
framePreviewModel,
PreviewPlayback,
type CommandTileProps,
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.
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 ondocument.bodywith the giventooltipId, 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.
loadModelsreturns the game's catalogue andloadPreviewModelreturns itsPreviewModelFactory; 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 aschildren. The screen never reads or writes session storage. Return to {game} links togamePathwithout thedebugparameter; 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 BabylonEngine,Scene, camera, light, resize observer, context-menu listener andPreviewPlayback. Calldispose()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.DebugModelPreviewdoes this for React. - Playback.
PreviewPlaybacksamples clips on an inspection clock in seconds, never the gameplay clock. Loops reload the clip so procedural state restarts. Afterdispose()it ignores every call. - Framing.
framePreviewModelfits 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 |