type: reference title: "Honua Web Components" description: "Framework-neutral custom elements for maps, layer lists, legends, tables, search, editing, charts and controls, plus the application shell, measurement, time slider, secure export, and the production feature table and editor." resource: "https://www.npmjs.com/package/@honua/sdk-js"

Honua Web Components

@honua/sdk-js/web-components registers framework-neutral custom elements for plain TypeScript applications:

Import the subpath once in a browser module. The module auto-registers the elements when customElements is available and also exports defineHonuaWebComponents() for scoped registries.

import {
  createHonuaWebComponentController,
  defineHonuaWebComponents,
} from "@honua/sdk-js/web-components";

defineHonuaWebComponents();

const controller = createHonuaWebComponentController({
  mapPackage,
  featuresBySource: {
    incidents: incidentFeatures,
  },
});

document.querySelector("honua-map")!.controller = controller;

Supported application shell

The canonical @honua/app-platform/web-components package exposes one supported, versioned application context for the whole component suite. The deprecated @honua/sdk-js/web-components forwarding entrypoint exposes the same contract during the transition through 0.2.x.

import { createHonuaApplicationContext, mountHonuaApplication } from "@honua/app-platform/web-components";

const context = createHonuaApplicationContext({
  binding: { source: incidents, sourceIdentity: "incidents", plan: acceptedPlan },
  authorization: { status: "authorized", principalId: "operator-17", scopes: ["incidents:read"] },
  locale: { locale: "ar", direction: "rtl" },
  theme: { accent: "#005ea8", colorScheme: "light dark" },
});

const application = await mountHonuaApplication({
  host: document.querySelector("main")!,
  context,
});

// Source or authorization replacement synchronously clears principal-bound
// selection/edit state and aborts work owned by the previous identity.
context.replaceBinding({ source: parcels, sourceIdentity: "parcels" });

// Release the observer, component bindings, listeners, and owned resources.
application.dispose();

mountHonuaApplication() registers the canonical suite once, binds existing and later honua-* descendants through their applicationContext property, and disconnects removed descendants. Components can implement the optional honuaApplicationContextConnected, honuaApplicationContextChanged, and honuaApplicationContextDisconnected lifecycle callbacks. The shell owns no result cache. runSharedRequest() only deduplicates in-flight work, and source, plan, or authorization invalidation aborts that work before the new identity is published.

The supported status vocabulary is idle, loading, ready, empty, stale, degraded, unauthorized, unsupported, offline, and failed. statusPresentation() gives every component the same localized label, role, aria-live, busy, and recovery semantics. Changes use the supported honua-application-context-change, honua-application-status-change, and honua-application-diagnostic events. Component-specific properties and events remain at the production or survival tier recorded in the catalog; experimental adapters remain explicitly opt-in.

Locale packs are scoped to a context and cover status messages plus Intl.NumberFormat / Intl.DateTimeFormat / unit defaults. The mount projects lang and dir (including RTL) on its host. Theme tokens map to the existing --honua-ui-* properties, while mount-local tokens override context tokens. Neither locale nor theme uses global mutable state, and disposal restores the host's original attributes and token values.

honua-map owns a MapLibre GL JS map for package and controller bindings. Import MapLibre's CSS in browser demos, then pass either an inline MapPackage, a hosted package URL, or a controller whose state contains a package. MapLibre 6 bundler hosts must configure the ESM worker before the element creates its map; for Vite:

import { setWorkerUrl } from "maplibre-gl";
import maplibreWorkerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url";

setWorkerUrl(maplibreWorkerUrl);
<honua-map id="map" package-url="/fixtures/response-map-package.json"></honua-map>
const map = document.querySelector("honua-map")!;
map.mapPackage = mapPackage;
map.controller = createHonuaWebComponentController({ mapPackage });

If an application already created a HonuaMapRuntime, adapt it through the existing controller bridge and bind that controller to the element:

import { createHonuaWebComponentControllerFromRuntime } from "@honua/sdk-js/web-components";

document.querySelector("honua-map")!.controller = createHonuaWebComponentControllerFromRuntime(runtime);

Sibling controls bind to the same controller with for="<map-id>".

<honua-map id="map"></honua-map>
<honua-layer-list for="map"></honua-layer-list>
<honua-legend for="map"></honua-legend>
<honua-feature-table for="map" source="incidents"></honua-feature-table>
<honua-search for="map" source="incidents"></honua-search>
<honua-basemap-control for="map"></honua-basemap-control>
<honua-bookmarks
  for="map"
  bookmarks='[{"id":"north","label":"North district","viewport":{"center":[-157.86,21.3],"zoom":12}}]'
></honua-bookmarks>
<honua-locate-control for="map"></honua-locate-control>

The controller contract is intentionally small so this package can integrate with the runtime today and a richer HonuaController later. Use createHonuaWebComponentControllerFromRuntime(runtime) when a HonuaMapRuntime already owns the MapPackage and layer operations.

Components dispatch typed custom events:

Styling is scoped to each component's shadow root and can be themed with CSS custom properties such as --honua-ui-bg, --honua-ui-fg, --honua-ui-border, and --honua-ui-accent.

Controller state drives the renderer after initialization. Layer visibility updates call the runtime layer API, viewport updates call setViewState, selection updates set the selected feature-state key, and controller.setFeatureState() / removeFeatureState() apply source-qualified feature-state patches to the MapLibre map.

Expanded Map Controls

The expanded controls are framework-neutral siblings of honua-map. They bind with the same for="<map-id>" convention and dispatch typed custom events so applications can connect richer adapters later without replacing markup.

<honua-map id="map"></honua-map>
<honua-basemap-control for="map"></honua-basemap-control>
<honua-bookmarks for="map"></honua-bookmarks>
<honua-locate-control for="map"></honua-locate-control>
<honua-measure-control for="map"></honua-measure-control>
<honua-sketch-control for="map"></honua-sketch-control>
<honua-print-export for="map" title="Operations map"></honua-print-export>
<honua-map-status for="map" attribution="Honua"></honua-map-status>
<honua-action-panel
  for="map"
  actions='[{"id":"refresh","label":"Refresh sources"}]'
></honua-action-panel>

honua-basemap-control lists background layers and layers with metadata.basemap === true. Selecting a basemap toggles those layer visibilities through the shared controller and emits honua-basemap-change.

honua-bookmarks always includes the package initialView as Home when it is available. Additional bookmarks can be supplied as a JSON array through the bookmarks attribute. Activating a bookmark calls controller.setViewport() and emits both honua-bookmark-change and honua-viewport-change.

honua-locate-control uses a configured latitude / longitude pair when present, otherwise it requests browser geolocation. If neither path is available it renders an explicit unsupported state and emits honua-locate-change with status: "unsupported".

honua-measure-control and honua-sketch-control expose accessible mode buttons and typed events (honua-measure-change, honua-sketch-change). Until a geometry editing or measurement adapter is attached to the shared controller, they render visible unsupported messaging instead of disappearing.

honua-print-export emits honua-export for print, snapshot, and JSON export actions. Browser print calls window.print() when available. Snapshot and state export fail closed until an application supplies an export adapter — see Secure export below.

honua-map-status displays an approximate scale, attribution text, and a fullscreen action. Fullscreen emits honua-fullscreen-change and renders an unsupported event when the browser does not expose requestFullscreen().

honua-action-panel renders application-defined action buttons from an actions JSON array and emits honua-action. Empty panels render a visible unsupported state so apps do not appear to lose configured controls.

Measurement

honua-measurement measures distance and area directly on a MapLibre map, with no drawing provider. Click (or tap) adds a vertex; double-click or Finish completes the sketch; Escape or Cancel discards it.

<honua-map id="map"></honua-map>
<honua-measurement
  for="map"
  unit="miles"
  area-unit="acres"
  precision="2"
  fidelity="planar"
  planar-crs="EPSG:3857"
></honua-measurement>
Attribute / property Values Default
unit / unit auto, meters, kilometers, feet, yards, miles, nauticalmiles auto (m / km by magnitude)
area-unit / areaUnit auto, square-meters, hectares, square-kilometers, square-feet, acres, square-miles auto (m² / ha / km²)
precision / precision integer 0–20 per-unit default
fidelity / fidelity geodesic, planar geodesic
planar-crs / planarCrs local (flat-earth meters around the sketch), EPSG:3857 (Web Mercator meters, not scale-corrected) local

Unknown values fall back to the default. Changing a unit or precision only reformats the canonical result; changing fidelity or the planar CRS recomputes it from the drawn vertices.

The honua-measure-change event carries plain data with no renderer types: result.coordinates are WGS84 [lng, lat] (the element's crs), distance is in meters and area in square meters, and result.crs names the frame they were measured in (EPSG:4326 for geodesic results). A line that crosses the antimeridian is measured the short way in every frame. Vertices that are not finite, or whose latitude is outside ±90°, are rejected. An area outline that crosses itself sets result.invalid to "self-intersecting-ring" and reports no area.

While a mode is active the element claims the map pointer: clicks that land on a feature still dispatch honua-map-click, but they do not change the shared selection that tables, inspectors, and editors are bound to. Removing the element releases every map listener, the sketch overlay, and the double-click zoom suspension. Double-click zoom is only turned back on if it was on before measuring started.

Custom Elements Manifest. @honua/app-platform publishes custom-elements.json, advertised through its customElements package key.

React. React 19 binds custom elements natively, so no wrapper is needed. Props that are element properties (map, unit, precision, planarCrs, …) are assigned as properties, and onhonua-measure-change becomes an event listener. For typed JSX, declare the tag once in your app:

import type { HonuaMeasureChangeDetail, HonuaMeasurementElement } from "@honua/app-platform/web-components";

declare module "react" {
  namespace JSX {
    interface IntrinsicElements {
      "honua-measurement": Partial<
        Pick<HonuaMeasurementElement, "map" | "unit" | "areaUnit" | "precision" | "fidelity" | "planarCrs">
      > & {
        ref?: React.Ref<HonuaMeasurementElement>;
        "onhonua-measure-change"?: (event: CustomEvent<HonuaMeasureChangeDetail>) => void;
      };
    }
  }
}

Time slider

honua-time-slider is a view over the headless temporal playback controller (createTemporalPlayback from @honua/sdk-js/map). It owns no window state and no timer of its own: every gesture becomes one controller call, and every rendered value comes back from the controller's own tick / play / pause / end events. Layered over a realtime source the controller keeps its snapshot-plus-delta semantics unchanged, because the element adds nothing between the controller and its filter sinks.

It binds through a property rather than for="<map-id>", because the thing it drives is the controller, not the map:

<honua-map id="map"></honua-map>
<honua-time-slider id="time" label="Incident time"></honua-time-slider>
import { createTemporalPlayback } from "@honua/sdk-js/map";
import "@honua/sdk-js/web-components";

const playback = createTemporalPlayback({
  handle: mountedSource, // or `layer: { map, layerIds }`
  timeField: "event_time",
  extent: ["2026-06-01T00:00:00Z", "2026-07-01T00:00:00Z"],
  windowMs: 24 * 60 * 60 * 1000,
  stepMs: 24 * 60 * 60 * 1000,
});

document.querySelector("honua-time-slider")!.playback = playback;

Attributes: label, speeds (comma-separated rate multipliers, default 0.5,1,2,4), and unavailable-reason. Set unavailable-reason when the mounted source has no usable temporal metadata: the panel still renders, names the reason, and disables every transport control rather than offering a scrubber that cannot move anything. The speed control is hidden entirely when the bound controller exposes no setSpeed.

The scrubber follows the WAI-ARIA slider pattern — role="slider" with aria-valuemin / aria-valuemax / aria-valuenow in epoch milliseconds, a readable aria-valuetext naming the window, and a single tab stop. Keyboard: Arrow keys step one window (the inline axis mirrors under dir="rtl"), Page Up / Page Down move ten steps, Home / End jump to the first / last window.

TimeSliderCompat from @honua/sdk-esri-compat mounts this element into its ArcGIS-style container once the application has called registerHonuaWidgetKit(); without a registered kit the shim stays state-model-only, exactly as before.

Secure export

Print, snapshot, and sanitized-state export all run through one explicit adapter contract. There is deliberately no ambient fallback for snapshot or state export: reading renderer pixels needs a canvas the application created with preserveDrawingBuffer: true and is authorized to read, and serializing "the current state" would otherwise carry the signed tile URLs, Authorization headers, SAS query strings, and OAuth scope detail that made the map work. With no adapter, both fail closed — no bytes at all, and a HonuaCapabilityNotSupportedError on the result — rather than degrading into a blank image or a partially credentialed JSON file. Browser print keeps working without an adapter because window.print() reads no pixels and serializes no state.

import {
  createHonuaExportAdapter,
  runHonuaExport,
} from "@honua/sdk-js/web-components";

const adapter = createHonuaExportAdapter({
  id: "operations-console",
  snapshot: { getCanvas: () => map.getCanvas() },
  state: true,
});

// Wire it to the element...
document.querySelector("honua-print-export")!.exportAdapter = adapter;

// ...or drive an export directly.
const result = await runHonuaExport({ kind: "snapshot", adapter, state });
if (result.status === "ready") {
  download(result.bytes!, result.filename!);
}
await result.release(); // always safe to call once; `bytes` stay valid

Every export is redacted by default in two independent layers: the state document is an allowlist projection (a field nobody allowlisted has no path into the output, so upstream state growth cannot silently widen an export), and everything that survives is then scrubbed and re-scanned. A credential reaching the final scan is a hard failure, not a silent scrub. The same treatment applies to download filenames, to the adapter-supplied media type (validated against a strict type/subtype grammar with an allowlisted parameter set, so text/plain; token=… cannot ride out on the result), and to every message placed on a honua-export event or a log line — a token disclosed through a filename is exactly as disclosed as one in the payload. Binary artifacts are scanned too: "binary" is not a security boundary, so printable runs are extracted from every byte payload and swept, catching a token inside a PNG tEXt chunk, a JPEG comment, or PDF/XMP metadata.

Attribution, licence notices, scale, export timestamp, data freshness, and fidelity warnings travel with every artifact. Whether the artifact's bytes carry the attribution is tracked separately as provenanceEmbedded, because conflating the two is how an unattributed image ships: reading a WebGL canvas captures the map's pixels and nothing else, since MapLibre renders attribution as DOM outside the canvas. An adapter that does not composite attribution must not claim it did, so the result reports provenanceEmbedded: false and carries an explicit fidelity warning naming what the caller has to present alongside the artifact. Pass requireEmbeddedProvenance: true to turn that into a hard failure when the artifact will travel on its own with no surrounding surface to carry the notice.

Production Feature Table (bounded query + linked state)

honua-feature-table has two lanes and renders the same accessible WAI-ARIA grid in both.

The controller lane is the original behavior: with no engine attached the element asks the shared controller for one bounded page and renders it. Nothing about that markup or its events changed.

The bounded lane is for operational applications. Assign the element's table property a bounded engine created with createHonuaFeatureTable. The engine owns remote paging, multi-column sort, typed filters, column semantics, budgets, virtualization, bounded export, and realtime reconciliation; the element only renders its snapshot and forwards input.

import {
  createHonuaFeatureTable,
  linkFeatureTableToExploration,
} from "@honua/sdk-js/web-components";
import { explainQuery } from "@honua/sdk-js/query-planner";

const source = dataset.source<Incident>("incidents")!;

const table = createHonuaFeatureTable({
  source,
  sourceId: "incidents",
  columns: [
    { field: "OBJECTID", label: "ID", type: "integer" },
    { field: "NAME", label: "Incident", type: "string" },
    { field: "SEVERITY", label: "Severity", type: "number", format: (v) => `P${v}` },
    { field: "INTERNAL_NOTE", visible: false },
  ],
  budgets: { pageSize: 200, maxCachedRows: 2_000, maxRequests: 64 },
  planner: (query) => explainQuery({ descriptor: source.descriptor, query }),
});

document.querySelector("honua-feature-table")!.table = table;
await table.refresh();

Bounded by construction

Every fetch is bounded by HonuaFeatureTableBudgets — pageSize, maxCachedRows, maxCachedBytes, maxRequests, maxExportRows, and windowOverscan. The engine loads only the pages the visible window needs, evicts least-recently-used pages to hold the row and byte ceilings, and stops issuing requests once maxRequests is reached. snapshot.ledger reports exact consumption and names the ceiling that was hit in ledger.exhausted. A window row whose page is not resident is undefined in snapshot.rows, and the grid paints a placeholder rather than inventing values.

The row and byte ceilings are hard. If a single page cannot fit under maxCachedBytes, that page is evicted too rather than left resident above the ceiling; ledger.exhausted reports bytes and snapshot.message explains it. A pageSize larger than maxCachedRows can never fit, so it is rejected up front as unsupported instead of failing per page.

maxRequests bounds one filter/sort/projection identity, so ledger.requests resets when that identity changes and an exhausted table can still load a new question. ledger.lifetimeRequests, ledger.rows, ledger.bytes, and ledger.evictedRows are lifetime totals and never reset.

table.export({ format: "csv", maxRows }) reuses the same paged query path. maxRows can only lower the ceiling — a request above budgets.maxExportRows is clamped and the result reports truncated: true with the limit that applied.

CSV serialization treats leading =, +, -, @, tab, and carriage return as spreadsheet control syntax and prefixes those cells with an apostrophe. This prevents source-returned text from becoming an executable formula when a bounded CSV is opened in a spreadsheet application.

budgets.maxFields bounds the projected and rendered column window, while budgets.maxExportBytes bounds the final UTF-8 artifact independently of the row ceiling. The column disclosure above the grid provides native checkbox controls for every declared field; hidden fields stay in the declarative model but leave the projection and DOM.

Columns are horizontally virtualized independently of rows. The engine's setColumnScroll(...) computes a bounded window capped by budgets.maxRenderedColumns; the grid materializes only that slice plus inert leading/trailing spacers. aria-colcount continues to name every admitted visible field and each rendered header/cell carries its absolute aria-colindex, so assistive technology retains the full grid coordinates. <honua-feature-table column-width="160"> controls the fixed width used for the horizontal window calculation.

For downloadable artifacts, use table.secureExport(...). Its required host adapter re-authorizes the current query, receives no credentials, and records a provenance envelope after the engine re-executes the accepted bounded query. The envelope names the host decision, adapter, authorization scope, accepted plan, row count, truncation, and timestamp. Aborting before authorization, during paging, or before audit recording prevents a successful result.

Result truth, never a manufactured count

snapshot.state is one of idle, loading, ready, partial, stale, cancelled, unsupported, or error, and snapshot.count names the evidence behind any number:

count.kind count.evidence Meaning
known result-total-count / exhausted-pages The source reported it, or paging drained
estimated plan-estimate The accepted plan estimated it
partial loaded-rows Only count.loaded is known
unknown none No evidence exists

count.value is absent for partial and unknown, the grid renders aria-rowcount="-1", and the live region says "at least N rows loaded; total unknown".

Stable row identity is required, not best-effort. A table with no identityField (or descriptor.schema.primaryKey) reports unsupported, and so does a table whose declared identity attribute is absent, null, or not a string/number on any returned row. Substituting a row's position in its page would key it by an offset that changes with every sort, filter, and page boundary, corrupting selection — so the engine refuses instead.

Paging modes

pagingMode: "offset" (default) issues random-access Query.pagination requests. pagingMode: "cursor" drives the source's stream() generator — the protocol-neutral cursor seam — and is therefore forward-only: a jump past the drained frontier reports unsupported instead of serving the wrong page. A source without stream reports unsupported for cursor paging.

Query evidence

snapshot.evidence carries the accepted plan's planId, planFingerprint, pushdown, and fidelity, plus a work list that attributes each unit of work to the server, worker, or client tier. Remote plan steps are server work, non-remote residual steps are attributed to residualExecution ("client" by default, "worker" when a worker executes them), filter degradation is recorded as a residual, and virtualization plus column formatting are always recorded as client presentation work. featureTableWorkByTier(evidence, tier) selects one tier.

The grid projects that same evidence into a compact, accessible Query execution badge list. A Server, Worker, or Client badge is rendered only when the accepted evidence records work at that tier; its tooltip names every concern and detail, so the UI never infers pushdown from a protocol name.

Page-cache identity (featureTablePageCacheKey) includes the source id and version, schema identity, accepted-plan fingerprint, filter, sort, projection, authorization scope, and freshness — so a stale page can never answer a different question.

The table consumes only canonical Source.query(), so protocol translation of its filter, sort, projection, pagination, spatial, and aggregate requests is the adapters' job; there is no table-specific protocol branch.

Linked exploration state

linkFeatureTableToExploration(table, view) binds the table to an ExplorationViewController in both directions. Table selection, sort, filters, and the virtualization window (as the shared page slice) publish outward; peer changes to selection, sort, visible fields, and filters apply inward. Row keys and exploration selection targets round-trip deterministically through table.keysForTargets() and table.selectionTargets(), so map-to-table and table-to-map selection agree on identity.

Extent linkage is explicit. A table starts in "independent" viewport mode; setViewportMode("linked") applies setViewportExtent(...) as a bounded spatial query. linkFeatureTableToExploration(table, view, { extent: true }) tracks the shared map extent, while omitting the option keeps map navigation from changing the table question.

table.aggregate({ groupBy, metrics }) executes a single bounded aggregate request through the same planner, filter, extent, cancellation, and evidence seams as row navigation. Aggregate rows never enter the page cache.

Selection is independent of cache residency: keysForTargets() resolves any target for the table's own source — computed from the target's id, not looked up in the page cache — so a map selecting a feature far outside the loaded window still selects the right row, and selecting it does not clear the shared selection. Targets for other sources resolve to nothing, and publishing the table's selection outward replaces only its own source's entries, leaving a multi-source workspace's peer selections intact.

const grid = context.connectView({ id: "grid", role: "grid" });
const unlink = linkFeatureTableToExploration(table, grid);
// ... later, on teardown:
unlink();

Realtime deltas preserve interaction state

table.applyRealtimeDiff(diff) accepts a reconciliation diff from @honua/sdk-js/realtime directly. Updates patch rows in place — a delta never reorders a materialized page behind the user — so the focused cell, selected rows, sort, and window survive. When state cannot be preserved the outcome announces a documented conflict instead of failing silently:

Conflict code Cause
sort-key-changed An update changed a sorted column's value
selection-invalidated Selected rows were deleted upstream
focused-row-deleted The focused row was deleted upstream
filter-membership-unknown An update may have moved a row into or out of the filtered result
snapshot-reset A replacement snapshot arrived
schema-changed The source schema changed

Conflicts appear in snapshot.conflicts, are read out by the grid's polite live region, and are dispatched as honua-table-conflict.

Keyboard and screen-reader contract

The grid follows the WAI-ARIA grid pattern for the WCAG 2.2 AA workflow: role="grid" with aria-rowcount / aria-colcount / aria-busy, role="row" and role="gridcell" carrying 1-based aria-rowindex / aria-colindex so a virtualized window still announces absolute positions, aria-sort per column header, aria-selected per row, and a role="status" aria-live="polite" region for result truth and conflicts. Exactly one cell is tabbable (roving tabindex).

Keys Action
Arrow keys Move the focused cell
Home / End First / last cell in the row
Ctrl/Cmd + Home/End First / last cell in the grid
PageUp / PageDown Move one window, loading the page focus lands on
Enter / Space Select the focused row

Clicking a column header toggles its sort (Shift-click adds a secondary sort key). The row-height attribute sizes the virtualization spacers; keep it in step with any custom row CSS.

Production-Tier Feature Editing (honua-feature-editor)

honua-feature-editor is the production-tier editing surface. It composes the public contract edit primitives — createEditSketchWorkflow, createEditSession, snapping, attachment staging, undo/redo, optimistic hooks — rather than reimplementing any protocol behavior, and it imports no protocol-specific adapter. The survival-tier honua-editor remains available as the simpler controller-bound widget.

Wire it to a Source through createFeatureEditorWorkflow:

import {
  createFeatureEditorWorkflow,
  defineHonuaWebComponents,
} from "@honua/sdk-js/web-components";

defineHonuaWebComponents();

const workflow = createFeatureEditorWorkflow({
  source: dataset.source("permits")!,
  // Optional. Subtypes are a presentation contract: one field carries the
  // subtype code, and each subtype narrows the valid choices per field.
  subtypes: {
    field: "permit_kind",
    defaultCode: 1,
    subtypes: [
      { code: 1, name: "Residential" },
      {
        code: 2,
        name: "Commercial",
        fieldOverrides: {
          status: { domain: { type: "coded-value", codedValues: [{ name: "Under review", code: "review" }] } },
          inspector: { required: true },
        },
      },
    ],
  },
  // Deny-only. An override can never grant an operation the source lacks.
  operations: { delete: { available: false, reason: "Permits are archived, never deleted." } },
});

const editor = document.querySelector("honua-feature-editor")!;
editor.workflow = workflow;
editor.selectedFeature = selectedPermit; // drives update / delete availability

workflow.begin("update");
const review = workflow.preflight();
// No adapter call occurs. The review contains source/feature identity, changed
// field names, geometry/attachment counts, and validation — never values,
// coordinates, attachment payloads, locators, or credentials.
if (!review.ready) console.warn(review.blockers, review.validation.errors);

editor.fieldPresentation can select the displayed attribute controls, set their order, and override their labels without altering the workflow's source metadata:

import type { HonuaFeatureEditorElement } from "@honua/sdk-js/web-components";

const editor = document.querySelector<HonuaFeatureEditorElement>("honua-feature-editor")!;
editor.fieldPresentation = [
  { name: "status", label: "Permit status" },
  { name: "permit_no", label: "Permit number" },
];

Names match fields exactly; a name absent from the current form renders no control. Set the property to undefined to restore the complete schema form. The element copies the configuration and escapes labels. Omitted fields keep their original required, nullable, default, domain and read-only constraints; their validation errors remain visible in the alert summary, and a missing required value still blocks submission without transport. This property does not interpret WebMap formInfo or Arcade, change permissions, or change which attributes the workflow validates and commits. Hosts must review and translate any external presentation definition separately.

What the element derives, and what it refuses:

Geometry can additionally be drawn on the map. createTerraDrawEditorSketch is the app-platform default: it constructs terra-draw (an optional peer) for a MapLibre map, wires SDK snapping, reports terra-draw's real tool support so rectangle / circle become enabled, and re-binds itself to each new draft.

import { createTerraDrawEditorSketch } from "@honua/sdk-js/web-components";

const sketch = await createTerraDrawEditorSketch(map, { workflow, snapping: { index } });
sketch.setTool("polygon"); // arms terra-draw and the workflow together
await sketch.ready(); // resolves once bound to the current draft

Apps that construct terra-draw themselves use bindEditorSketch(draw, { workflow }) and, for snapping, editorSnappingOptions(workflow, { index }). Route snapping through that helper rather than passing a model directly: terra-draw resolves its snapping hook once per mode, and the editor builds a new contract sketch model for every draft, so a directly-passed model would pin snapping to the first draft and make setSnapping(...) on later drafts a silent no-op.

Two lifecycle details worth knowing:

Events: honua-feature-edit-change (redacted snapshot on every state change) and honua-feature-edit-commit (the submit outcome, including transported).

Production qualification matrix

Production support for the component kit is tracked as a matrix rather than a claim in prose: every shipped tag against every gate that matters for accessibility, localization, visual behaviour, CSP, lifecycle cleanup and bundle cost. pass means an automated check backs the claim, pending means there is no verdict yet, and n/a carries an argument. The Tier column records the functional maturity a component has reached, which is a different axis from clearing every gate.

594 cells across 33 components x 18 gates: 393 passing, 40 failing, 91 pending, 70 not applicable.

2 of 33 components have cleared every gate. That is a different axis from the catalog's supportTier, which records the functional maturity a feature issue delivered: 2 component(s) are production-tier and still carry open gates, listed per component as openGates in the manifest.

Component Tier keyboard-behavior screen-reader-semantics focus-restoration reduced-motion high-contrast responsive-layout localization pseudo-locale rtl visual-regression strict-csp zero-console-error deterministic-disposal duplicate-listener memory-leak ssr-import secure-export bundle-budget
honua-basemap-switcher (controls) survival pass pass pass n/a pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-swipe-control (controls) survival pass pass pass n/a pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-legend (controls) survival n/a pass n/a n/a pass pass pass pass pass pending pass pass pass n/a pass pass n/a pass
honua-layer-list (controls) survival pass pass pass n/a pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-map (web-components) survival pass pass pass pass pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-layer-list (web-components) survival pass pass pass n/a pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-legend (web-components) survival n/a pass n/a n/a pass pass pass pass pass pending pass pass pass n/a pass pass n/a pass
honua-feature-table (web-components) survival pass pass pass n/a pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-feature-inspection (web-components) production pass pass pass n/a pass pass pass pass pass pass pass pass pass pass pass pass n/a pass
honua-search (web-components) survival pass pass pass pass pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-editor (web-components) survival pass pass pass n/a pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-feature-editor (web-components) production pass pass pass n/a pass pass pass pass pass pass pass pass pass pass pass pass n/a pass
honua-chart (web-components) survival n/a pass n/a n/a pass pass pass pass pass pending pass pass pass n/a pass pass n/a pass
honua-basemap-control (web-components) survival pass pass pass n/a pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-bookmarks (web-components) survival pass pass pass pass pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-locate-control (web-components) survival pass pass pass pass pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-measure-control (web-components) survival pass pass pass n/a pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-measurement (web-components) survival pass pass pass n/a pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-time-slider (web-components) survival pass pass pass n/a pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-sketch-control (web-components) survival pass pass pass n/a pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-print-export (web-components) survival pass pass pass n/a pass pass pass pass pass pending pass pass pass pass pass pass pass pass
honua-map-status (web-components) survival pass pass pass n/a pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-action-panel (web-components) survival pass pass pass n/a pass pass pass pass pass pending pass pass pass pass pass pass n/a pass
honua-zoom (web-components) survival pending pending pending n/a FAIL FAIL FAIL pass FAIL pending pass pending pending pending pass pass n/a pass
honua-home (web-components) survival pending pending pending n/a FAIL FAIL FAIL pass FAIL pending pass pending pending pending pass pass n/a pass
honua-scale-bar (web-components) survival pending pending pending n/a FAIL FAIL FAIL pass FAIL pending pass pending pending pending pass pass n/a pass
honua-compass (web-components) survival pending pending pending n/a FAIL FAIL FAIL pass FAIL pending pass pending pending pending pass pass n/a pass
honua-fullscreen (web-components) survival pending pending pending n/a FAIL FAIL FAIL pass FAIL pending pass pending pending pending pass pass n/a pass
honua-attribution (web-components) survival pending pending pending n/a FAIL FAIL FAIL pass FAIL pending pass pending pending pending pass pass n/a pass
honua-feature-pager (web-components) survival pending pending pending n/a FAIL FAIL FAIL pass FAIL pending pass pending pending pending pass pass n/a pass
honua-attachments (web-components) survival pending pending pending n/a FAIL FAIL FAIL pass FAIL pending pass pending pending pending pass pass n/a pass
honua-scale-range (web-components) survival pending pending pending n/a FAIL FAIL FAIL pass FAIL pending pass pending pending pending pass pass n/a pass
honua-directions (web-components) survival pending pending pending n/a FAIL FAIL FAIL pass FAIL pending pass pending pending pending pass pass n/a pass

Gate definitions and per-cell evidence and notes live in config/component-qualification.v1.json.

New feature default values

Opening a create draft preserves explicitly supplied feature/template attributes, including null, 0, false, and empty strings. For absent values, the workflow chooses the configured subtype defaultCode first, otherwise the subtype field's schema default. It then materializes writable field defaults from that resolved subtype, with subtype overrides taking precedence over schema defaults. Those values are part of the draft: displayed values, preflight and the mutation agree. Invalid defaults still fail normal validation before transport. Server-managed read-only fields receive no client-side default writes.

begin("create", feature) accepts an explicit template's prototype attributes; the workflow does not parse WebMap template definitions itself. Initialization applies only to a new create draft. It does not fill missing values in updates or deletes, and changing a subtype after opening a draft does not overwrite edits with another template's values.