@base-works/mapbox (0.2.1)

Published 2026-09-09 19:25:18 +00:00 by ahmadsholehin

Installation

@base-works:registry=https://git.muddlab.com/api/packages/ahmadsholehin/npm/
npm install @base-works/[email protected]
"@base-works/mapbox": "0.2.1"

About this package

BaseWorks shared Mapbox rendering runtime and integration engine.

@base-works/mapbox

Shared BaseWorks Mapbox runtime and integration engine. Phases 1–11 establish the map host, scoped runtime, independent viewport/status subscriptions, deterministic layer IDs, runtime layer bookkeeping and observed style generations, cleanup-safe imperative Mapbox effects, and an opt-in per-map Three.js bridge, demand-driven frame scheduling, and map-scoped image/GLTF asset management, centralized Mapbox/Three pointer interaction, and one map-scoped transient popup manager, error isolation and hardened host/WebGL lifecycle. Synthetic examples exercise the supported rendering modes together; domain packages remain separate. Tokenless testMode checks establish logic and lifecycle behavior, not pixels; the new graphics stories use an environment-provided public token for actual rendering verification.

Start here

The package is published in the private BaseWorks Gitea npm registry and linked from the workspace while developing the platform. A consuming package must supply the React, react-map-gl, Mapbox GL and Three peer versions declared in package.json.

Import mapbox-gl/dist/mapbox-gl.css, give the host container an explicit height, and compose layers as children of BaseMap. The application owns the style, token, camera state and domain state. Enable three only on hosts that render Three content; v0 Three integration requires Mercator projection.

Choose a path by responsibility:

Requirement Preferred path
Ordinary reactive Mapbox source/layer react-map-gl Source / Layer
Native Mapbox lifecycle or custom behavior useMapboxEffect()
Mesh/model rendering useThreeLayer() and optional useThreeFrame()
Mapbox hit target plus Three visual Deliberate hybrid composition
Feature/object pointer behavior useMapboxInteraction() / useThreeInteraction()
Transient anchored React content useMapPopups()

Detailed authoring guidance:

Intelligence catalogue

The package publishes dist/baseworks.catalogue.json through package.json#baseWorks.catalogue. The mapbox.base-map record is an L2 map.host that provides the scoped runtime, style lifecycle, optional Three, interaction, popup and asset capabilities. It accepts map.layer, map.control and map.tool composition roles and records all supported rendering backends.

Inside a derivative that installs this package, BaseWorks Intelligence can answer the contract without reading implementation source:

pnpm exec base-works search "map host three"
pnpm exec base-works describe mapbox.base-map
pnpm exec base-works describe mapbox.base-map --json

The JSON form is the compact machine-readable source for coding agents. A future layer declares kind: "map.layer", the matching fits slot, its rendering backend, and capabilities such as mapbox.runtime or mapbox.three under rendering.requires. Generic browser fixtures are intentionally not catalogued as reusable production components.

import "mapbox-gl/dist/mapbox-gl.css";
import { BaseMap, useMapRuntime, useNativeMap } from "@base-works/mapbox";

function RuntimeProbe() {
  const runtime = useMapRuntime();
  const map = useNativeMap(); // deliberate advanced Mapbox escape hatch

  return <button onClick={() => runtime.invalidate()}>{map.getZoom()}</button>;
}

export function ApplicationMap() {
  return (
    <BaseMap
      id="main-map"
      initialViewState={{ longitude: 103.8, latitude: 1.35, zoom: 9 }}
      mapStyle="/map/style.json"
    >
      <RuntimeProbe />
    </BaseMap>
  );
}

BaseMap preserves the react-map-gl/mapbox MapProps and forwards a MapRef. If id is omitted it creates a stable local ID shared by the react-map-gl host and BaseWorks runtime. Descendants use the nearest map; two maps never share mutable bindings.

The general MapRuntime intentionally exposes only id and invalidate(). Native access remains explicit through useNativeMap(). Both hooks throw a contextual developer error outside a mounted BaseMap. Applications still own view state, map styles, tokens and domain state; this package provides no geographic defaults or application store/event-bus integration.

Phase 2 hooks

  • useMapStatus() subscribes to map/style readiness and style generations.
  • useMapViewport() subscribes to the complete camera snapshot; useMapViewport("zoom") subscribes to one scalar.
  • useMapLayerIds() creates deterministic source and layer IDs for a component instance and diagnoses namespace collisions.
  • useMapLayerRegistration() records renderer, semantic stage and native IDs in private map-scoped bookkeeping, then unregisters on unmount.

Style changes remain ordinary mapStyle prop updates. The lifecycle manager observes their consequences and never calls native setStyle().

Imperative integration

Use declarative react-map-gl Source and Layer when they fit. For advanced native integration, useMapboxEffect(effect, dependencies, options) waits for style readiness by default, runs one cleanup for every installation, and can restore after React changes mapStyle:

useMapboxEffect(
  ({ map }) => {
    map.addSource(sourceId, source);
    map.addLayer(layer);
    return () => {
      if (map.getLayer(layerId)) map.removeLayer(layerId);
      if (map.getSource(sourceId)) map.removeSource(sourceId);
    };
  },
  [sourceId, layerId],
  { componentId: "domain.component", restoreAfterStyleReload: true },
);

BaseMap.onRuntimeError receives structured install and cleanup failures. The package contains a synthetic imperative source/layer proof for tests, but it is deliberately not exported.

Three.js integration

Enable Three only on maps that need it:

function DomainLayer() {
  const layer = useThreeLayer({ id: "domain.objects", stage: "world-3d" });

  useEffect(() => {
    const mesh = createDomainMesh();
    layer.group.add(mesh);
    layer.invalidate();
    return () => {
      layer.group.remove(mesh);
      disposeDomainMesh(mesh);
    };
  }, [layer]);

  return null;
}

<BaseMap three mapStyle="/map/style.json">
  <DomainLayer />
</BaseMap>;

Each enabled BaseMap owns one private scene, camera, raycaster and renderer using Mapbox's canvas/context. useThreeLayer() returns an owned group that is attached and detached automatically; the layer remains responsible for the geometry, material and texture resources it creates. The public useThreeRuntime() facade exposes only ready and invalidate()—not the root scene or rendering internals. The v0 bridge supports Mercator projection and reports a structured runtime error rather than rendering incorrectly with an unsupported projection. Style replacement restores the physical custom layer without discarding logical layer groups.

Demand-driven Three frames

Static layers call layer.invalidate() only after mutating their owned objects. That requests one Mapbox repaint and does not create a render loop. Animated layers opt into continuous frames explicitly:

useThreeFrame(
  ({ camera, delta, elapsed }) => {
    mesh.rotation.z += delta;
    updateLevelOfDetail(mesh, camera, elapsed);
  },
  { enabled: isAnimating, priority: 0 },
);

delta and elapsed are seconds. Lower priorities execute first and equal priorities retain registration order. Callback changes are read through a ref, so ordinary React rerenders do not tear down the subscription. The first active subscriber starts repaint demand; every rendered frame requests the next one; and disabling or unmounting the last subscriber stops the loop. Frame callback errors are isolated and reported through BaseMap.onRuntimeError with the three subsystem. The frame API deliberately omits the native map and root scene: use useNativeMap() separately when native access is justified and mutate objects owned by useThreeLayer().

Asset ownership

Mapbox images are runtime-owned desired state:

const assets = useMapAssets();
await assets.ensureImage("domain-alert", "/icons/alert.png", {
  pixelRatio: 2,
  sdf: true,
});

Concurrent requests for the same ID, source and options share one load. Decoded URL data is retained so a style replacement can restore the image without another network request. The image remains registered when a requesting layer unmounts and is removed only when its BaseMap runtime is disposed. Reusing an ID with different input is rejected rather than silently changing another layer's asset.

GLTF assets require a Three-enabled map and use explicit handles:

const assets = useThreeAssets();
const instance = await assets.acquireGltf("/models/object.glb");
layer.group.add(instance.scene);
layer.invalidate();

// Cleanup: detach first, then release.
layer.group.remove(instance.scene);
instance.release();

Each URL is loaded once per map runtime. Instances use Three's skeleton-safe clone utility, clone materials and animation clips for consumer mutation, and share source geometry and textures. Treat shared geometry/textures as immutable; clone them explicitly when a layer must mutate them. release() is idempotent, disposes the instance-owned material clones, and does not invalidate another consumer. GLTF source resources remain cached until the map runtime is disposed. Cross-map caching is deliberately absent.

The ownership rule is therefore:

  • runtime-owned: Mapbox image registrations and GLTF source caches;
  • shared cached: decoded Mapbox image data and GLTF geometry/textures;
  • layer-owned: Three groups, acquired instance transforms/materials, and any resources created or replaced by the layer.

Central interactions

Register rendered Mapbox features or objects below an owned Three group without installing native listeners in the domain layer:

useMapboxInteraction({
  id: "domain.aircraft",
  layerIds: [aircraftLayerId],
  priority: 20,
  mapFeature: (feature) => parseAircraft(feature),
  getKey: (aircraft) => aircraft.id,
  cursor: "pointer",
  onClick: ({ data }) => onSelect(data.id),
});

useThreeInteraction({
  id: "domain.models",
  root: threeLayer.group,
  priority: 30,
  mapIntersection: (intersection) => mapModel(intersection.object),
  getKey: (model) => model.id,
  onClick: ({ data }) => onSelect(data.id),
});

The runtime owns one mousemove, click and mouseleave stream per map. It performs at most one Mapbox feature query and one Three raycast per native event, then passes the already-calculated results to consumer mappers. Higher priority wins; equal priority retains registration order. Return "continue" from a click callback to offer the same event to the next candidate. Cursor and hover enter/move/leave are also centrally arbitrated. Use useMapBackgroundInteraction() for a background fallback rather than attaching another map click listener.

These hooks carry no aviation meaning. Domain layers supply mapped data and callbacks, and remain responsible for an accessible non-map path to critical workflows.

Transient popups

Mapbox and Three interaction callbacks share the nearest map's popup controller:

const popups = useMapPopups();

useMapboxInteraction({
  // ...feature mapping...
  onEnter: ({ data }) =>
    popups.open({
      id: `domain.${data.id}`,
      position: data.position,
      content: <DomainSummary data={data} />,
      priority: 20,
      hoverPolicy: "retain",
      hideDelayMs: 200,
    }),
  onLeave: (data) => popups.close(`domain.${data.id}`),
});

One popup and one React root are owned per BaseMap. Opening the same ID updates it in place; a different lower-priority popup cannot displace the current one. close(id) ignores stale IDs and observes the configured delay, while closeAll() closes immediately. Retained popup content cancels a pending close while hovered. Cleanup removes timers, DOM listeners, React content and the Mapbox popup instance. Persistent inspectors and panels remain application UI.

Error and lifecycle boundaries

Wrap the host when losing the whole map should produce application fallback UI, and wrap optional layers independently when the rest of the map can continue:

<MapErrorBoundary
  onError={reportMapError}
  fallback={({ reset }) => <MapFailure onRetry={reset} />}
>
  <BaseMap three>{layers}</BaseMap>
</MapErrorBoundary>

<MapLayerErrorBoundary
  layerId="domain.weather"
  onError={reportLayerError}
  fallback={<div role="alert">Weather layer unavailable</div>}
>
  <WeatherLayer />
</MapLayerErrorBoundary>

The map-level default is a minimal unstyled alert with retry; the layer-level default is null. resetKeys can deliberately retry after configuration or data changes. React unmounts a failed layer subtree, so its hooks relinquish runtime registrations normally. Callback/lifecycle failures outside React rendering continue through BaseMap.onRuntimeError.

Mapbox remove disposes the entire scoped binding once. WebGL context loss releases the stale Three renderer and requests reconstruction on restoration; layers retain their owned groups and do not install context listeners.

Performance characteristics

The runtime carries internal lifetime diagnostics for validation without adding a public telemetry API. They count scheduled Three repaints and completed animated frames, active frame subscribers, owned Three groups/objects, shared Mapbox queries and Three raycasts, runtime layer registrations, and Mapbox/GLTF asset-cache hits and misses. These counters are read only by package tests and development internals; applications do not need to configure or ship a metrics service.

Phase 11 workload tests establish the intended invariants: static Three content does not request an endless repaint; many animated layers share one repaint chain; many interaction registrations share one query and one raycast per pointer event; repeated assets share one load; and separate maps retain separate registries and diagnostics. These are deterministic architectural checks, not machine-dependent timing benchmarks.

Reusable graphics layers

CurveRibbonLayer and InstancedSymbolLayer are experimental L2 map layers. Both compose below <BaseMap three projection="mercator">. They own no domain data or application time. Ribbons expand camera-facing in their vertex shader, so they stay visibly wide at any camera pitch or bearing instead of collapsing to a hairline at oblique/side-on angles; width remains physical metres. Ribbons offer explicit smoothing, directional pulses, central picking and shared map-owned glow; symbols update one instanced batch. Picking follows the same camera-facing surface through the centralized Three interaction path (no extra camera plumbing). The flight and shipping examples reuse the same ribbon renderer.

See graphics composition for contracts, default appearance, rendering limits, examples and verification. Actual graphics stories require the existing local public token even when using a synthetic basemap: Mapbox testMode tests logic but does not produce pixels.

Package commands

pnpm --filter @base-works/mapbox build
pnpm --filter @base-works/mapbox test
pnpm --filter @base-works/mapbox storybook
pnpm --filter @base-works/mapbox storybook:build
pnpm --filter @base-works/mapbox example
pnpm --filter @base-works/mapbox test:browser

Explore components in Storybook

storybook starts the primary interactive discovery surface at http://localhost:6008. Open Mapbox / Capabilities to inspect the rendering overview, interactions, Three motion, and runtime-lifecycle scenarios individually. The stories use real BaseMap instances and the same internal scenario implementations as the standalone gallery.

No token is required: Storybook defaults to a local style and synthetic data. To review the examples over a hosted Mapbox basemap, copy examples/browser/.env.example to examples/browser/.env.local and set PUBLIC_MAPBOX_TOKEN, then restart Storybook. Both Storybook and the standalone gallery read this ignored local file. The value is browser-visible by design: use a public token with minimum scopes and URL restrictions, never a secret token.

example starts the standalone browser capability gallery at http://127.0.0.1:4173. This is the stable host used by Playwright for whole-runtime, WebGL, scenario-switching, and cleanup verification. It is not a replacement for Storybook and is not normally needed merely to discover or review a component.

Without a configured token—or with ?local=1—the gallery uses its local style and synthetic data without requesting Mapbox-hosted resources. Its selectable scenarios match the individual Storybook stories:

Example Direct URL Demonstrates
Overview ?example=overview Declarative and imperative Mapbox, Three, hybrid rendering and React popups
Interactions ?example=interactions GeoJSON expressions, centralized hover/click, cursor and background handling
Three motion ?example=three Owned groups, demand-driven frames, shared raycasting and Three popups
Runtime ?example=lifecycle Effect installation/cleanup, viewport subscriptions and native camera access

Append &local=1 to any direct URL for its tokenless form. All displayed data is synthetic. Shared Phase 10 fixtures live under src/examples/; focused gallery scenarios live under examples/browser/src/examples/. Neither set is part of the package's public export surface.

example:build type-checks and builds the gallery. test:browser starts it in tokenless mode and checks API readiness, the interactive popup, scenario switching, Three lifecycle readiness and imperative-layer cleanup in Chromium. These readiness checks are not evidence of actual Three drawing: Mapbox testMode does not render pixels. Use test:graphics for the token-enabled local-style visual checks described in graphics composition. storybook:build verifies the component-discovery surface independently.

Dependencies

Dependencies

ID Version
@base-works/contracts ^0.2.1

Development Dependencies

ID Version
@playwright/test ^1.58.0
@storybook/addon-a11y ^10.2.0
@storybook/react-vite ^10.2.0
@testing-library/react ^16.3.0
@types/react ^19.2.0
@types/react-dom ^19.2.0
@types/three ^0.185.4
@vitejs/plugin-react ^6.0.4
jsdom ^29.1.1
mapbox-gl ^3.27.0
react ^19.2.8
react-dom ^19.2.8
react-map-gl ^8.1.2
storybook ^10.2.0
three ^0.185.1
vite ^8.1.5

Peer Dependencies

ID Version
mapbox-gl ^3.27.0
react ^19.2.8
react-dom ^19.2.8
react-map-gl ^8.1.2
three ^0.185.1
Details
npm
2026-09-09 19:25:18 +00:00
3
latest
75 KiB
Assets (1)
Versions (3) View all
0.2.1 2026-09-09
0.2.0 2026-09-09
0.1.0 2026-09-09