@x12i/ui-data-mapper
v0.8.0
Published
Map an existing HTML UI to object/property and semantic UI metadata, with interactive SVG connectors.
Maintainers
Readme
UI ↔ Metadata Mapper
Portable Vite + React + TypeScript project for mapping an existing HTML UI to two kinds of metadata and drawing interactive links between them. The included example is deliberately domain-neutral and uses a cross-functional work portfolio.
Install
npm install @x12i/ui-data-mapperThe published package includes the production build under dist/ (serve that folder with any static file server) and the scripts/build-ui-from-source.mjs helper. Source checkout is still the usual way to develop:
Start in VS Code
Requirements: Node.js 20 or newer.
npm install
npm run devOpen the local URL printed by Vite, normally:
http://localhost:5173Core mapping works fully offline (file import/export, bundled example). Optional Memorix persistence is documented in MEMORIX.md and enabled when VITE_MEMORIX_BASE_URL and VITE_MEMORIX_ORG_ID are set (see .env.example).
Docs (Docify)
Narrative guides live under docs-library/. Humans browse the Docify site; agents use the knowledge SDK.
npm run docs # build if needed, serve dist/web on :4173
npm run docs:rebuild # force rebuild + serve
npm run docs:bundle # → dist/bundle + dist/web
npm run docs:knowledge # embed packs into @x12i/ui-data-mapper-docs
npm run docs:publish # Cloudflare Pages → docs.ui-data-mapper.x12i.com
npm run docs:publish:previewProduction: https://docs.ui-data-mapper.x12i.com
npm i -D @x12i/ui-data-mapper-docsimport { uiDataMapperDocs } from "@x12i/ui-data-mapper-docs";
uiDataMapperDocs.getUseCaseMarkdown("map-a-rendered-ui");
uiDataMapperDocs.getBookMarkdown("01-the-mapper", "developers");The three-input contract
The bundled domain-neutral work portfolio example is under public/examples/operations/:
ui.html Existing UI, rendered without recreating it
data.json Runtime sample used only for validation and recurring-item resolution
mapping.json Object/property metadata, UI metadata, and selector rulesYou can also click Load 3-file package and select one .html file and two .json files.
When Memorix is configured, Load from Memorix / Save to Memorix store the same three documents as sibling content types on one recordId (ui-packages / uiHtml+data+mapping). File Import/Export stay available as the offline fallback.
Abstract mapping
mapping.json can carry the same abstract-name vocabulary as @x12i/io-matrix-schema. An abstract is an agreed canonical name (for example project_code) claimed by a model or UI-metadata node. The tree has a Raw / Abstract naming lens; coverage is mapped / total properties that have at least one abstract.
{
"abstractCatalog": ["project_code", "project_title"],
"model": {
"nodes": [
{ "path": "$.projects[0].code", "kind": "property", "abstracts": ["project_code"] }
]
}
}Helpers (catalogFor, coverageFor, mappingToObjectTypes, validateAbstracts) are exported from @x12i/ui-data-mapper/mapper. Abstracts are the join key used by @x12i/ui-data-mapper/binder to bind UI selectors to OpenAPI fields.
What works
- Independent Import and Export for
ui.html,data.json, andmapping.json. - Object and property definitions are shown as a collapsible metadata tree with branch guides, child counts, types, descriptions, and recurring-template badges.
- Selecting a container expands it to reveal its direct properties; selecting a mapped UI value automatically opens every ancestor needed to reveal the linked property.
- Runtime values from
data.jsonare never rendered or edited in the tree. They are used only to validate required/missing state and identify recurring UI instances. - Arrays expose one representative item definition. Every repeated rendered row links to the same property definitions instead of producing one model per runtime item.
- Configured UI atoms are clickable inside the embedded UI.
- Clicking either side highlights the corresponding item on the other side.
- Connections are bidirectional: selecting object metadata reveals every linked UI occurrence, while selecting any UI occurrence expands and selects its metadata definition.
- SVG curves connect the selected JSON leaf and rendered UI atom.
- Connection modes: Selected, Visible, and Off.
- Metadata search, layer filters, and mapped-only filtering.
- Explicit selector and recurring-template mappings are used; runtime value matching is disabled in metadata mode.
- Mutation observation re-instruments dynamic UI content, tabs, tables, and drawers.
- The left-side
Object metadata | UI metadatatoggle switches between two metadata scopes—never between metadata and runtime data. - Object metadata edits labels, types, descriptions, layers, status, required state, and schema definitions under
modelinmapping.json;data.jsonis unchanged. - UI metadata describes interface concepts such as titles, tabs, labels, headings, column names, and actions under
metadatainmapping.json. - Both metadata scopes use the same tree editor, status, required, missing, click-to-map, and connector behavior.
- Scan UI metadata discovers actions, tabs, headings, labels, navigation, and table headings, grouping repeated controls by meaning.
- Click-to-map creates either a precise selector mapping or a relative repeater field rule when the selected value belongs to a recurring UI item.
- Repeater mappings connect one UI item template to one JSON collection, then resolve every rendered row by stable key, visible identity field, or index.
- Detect recurring UI matches repeated sibling structures to array data and proposes relative field rules automatically.
- Required nodes remain visible when absent or null and can be repaired from the editor.
- Status metadata works on objects, arrays, properties, and explicit mappings.
- Generate starter metadata can bootstrap a mapping package from the rendered UI.
Status, required, and missing
Object/property metadata lives in mapping.json; data.json remains a separate runtime input:
{
"model": {
"statuses": [
{ "id": "verified", "label": "Verified", "color": "#22c55e" },
{ "id": "review", "label": "Review", "color": "#f59e0b" }
],
"nodes": [
{
"path": "$.projects[0].code",
"kind": "property",
"dataType": "string",
"description": "Stable project code",
"status": "verified",
"required": true,
"layer": "data"
}
]
}
}missing is computed from the runtime sample when a required path is absent, null, or an empty string. For recurring fields, the representative definition is marked missing if any runtime item lacks the required property. The runtime value itself is never displayed.
UI metadata
UI concepts that describe the interface rather than an object—actions such as Analysis and Raw facts, tabs, headings, and column labels—live in mapping.json:
{
"metadata": {
"values": {
"interface": {
"analysis": { "label": "Analysis", "role": "action" },
"rawFacts": { "label": "Raw facts", "role": "action" }
}
},
"mappings": [
{
"id": "metadata-analysis-action",
"dataPath": "$.interface.analysis.label",
"selector": "#list > .row .actions > .btn.primary"
}
]
}
}One metadata selector may resolve to many repeated UI controls. This expresses that every row's Analysis button has the same semantic meaning without creating one metadata object per row.
Main source files
src/core/ The reusable mapper engine — <MetadataMapper>, types, matching/selector logic
src/App.tsx Thin standalone-app wrapper: bundled-example fetch, "Load 3-file package"
src/main.tsx React entry pointBuilding ui.html from TSX/JSX source
The mapper only ever reads rendered DOM through CSS selectors (see
PROJECT-NOTES.md); it does not execute or understand JSX/TSX, and it does
not rebuild the supplied UI as React components. That means a .tsx/.jsx
file can't be imported as-is — it has to be turned into rendered HTML first.
scripts/build-ui-from-source.mjs does that offline, once, as a normal
Node build step. It bundles a React entry file — plus its local imports,
npm dependencies, CSS, and image/font assets — into one self-contained
ui.html, which you then import exactly like any hand-written UI package:
npm run build-ui-from-source -- path/to/entry.tsx path/to/output/ui.htmlTry it against the bundled smoke test:
npm run build-ui-from-source -- scripts/tsx-source-example/entry.tsx scripts/tsx-source-example/ui.htmlThe entry file must mount itself into #root, the same way src/main.tsx
does for this app:
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")!).render(<App />);The output has no CDN or network dependency — React itself is bundled in —
so it stays consistent with this project's "no backend, no hosting
dependency" design. This step only produces ui.html; data.json and
mapping.json are still separate inputs, either authored by hand or
generated from the rendered result with Generate starter metadata inside
the running app.
Capturing ui.html from a live page (Chrome extension)
build-ui-from-source.mjs needs the actual TSX/JSX source. When you don't
have it — a real running app, behind auth, wired to a live backend, or built
with tooling too complex to bundle standalone — use the Chrome extension
under extension/ instead. It captures whatever is
actually rendered in a live tab, any framework, into the same kind of
self-contained ui.html, ready for Load 3-file package:
npm run build-extensionThen load extension/ unpacked via chrome://extensions (Developer mode).
See extension/README.md for details, capture modes,
and permissions.
Embedding the mapper as a component
Everything under What works above runs the same way whether you use this
repo's standalone page or drop the mapper into another React app. The
reusable engine — src/core/ — is published separately from the standalone
app as @x12i/ui-data-mapper/mapper:
npm install @x12i/ui-data-mapperimport { useRef } from "react";
import { MetadataMapper, MetadataMapperHandle } from "@x12i/ui-data-mapper/mapper";
import "@x12i/ui-data-mapper/mapper.css";
function Embedded() {
const mapperRef = useRef<MetadataMapperHandle>(null);
return (
<div style={{ width: "100%", height: 600 }}>
<MetadataMapper
ref={mapperRef}
initialUiHtml={uiHtml}
initialData={data}
initialMapping={mapping}
onMappingChange={(next) => saveMappingSomewhere(next)}
confirm={(message) => myHostApp.confirm(message)}
/>
</div>
);
}- The component sizes itself to its parent (
width: 100%; height: 100%) and namespaces all of its CSS and custom properties under one root class, so it composes inside a host page's own layout instead of assuming it owns the whole viewport — mount it in a sized container, as above. ref.current.loadPackage({ uiHtml, data, mapping, packageName? })replaces the whole three-file package at once (what a file-picker, Memorix load, or a host's own "load" action would call);ref.current.getPackage()reads the current one back out, e.g. to implement your own export/save action instead of a file download.- Memorix helpers (
createMemorixClient,readMemorixConfigFromEnv, envelope mappers) are exported from the same@x12i/ui-data-mapper/mapperentry — see MEMORIX.md. onDataChange/onMappingChangefire whenever the runtime sample or mapping config changes for any reason (click-to-map, node editing, Scan UI metadata, Detect recurring UI, Generate starter metadata, or aloadPackage()call) — use them to persist changes your own way.confirmoverrides the handful of destructive confirmations (hiding metadata, replacing a generated scan); it defaults towindow.confirm.react/react-domare peer dependencies of this entry point — the host app's own React instance is reused rather than bundling a second one.
react and react-dom need to be installed in the host app already (they're
peer dependencies of the ./mapper entry point). See
src/core/index.ts for the full exported surface.
Embedding the binder
@x12i/ui-data-mapper/binder binds the intended UI to OpenAPI operations through the same abstract names. It does not call the API. Output is an IoMatrixDocument (iomatrix.json) plus resolved bindings (UI selector ↔ abstract ↔ API field).
import { UiApiBinder } from "@x12i/ui-data-mapper/binder";
import "@x12i/ui-data-mapper/mapper.css";
import "@x12i/ui-data-mapper/binder.css";
<UiApiBinder
uiHtml={uiHtml}
data={data}
mapping={mapping}
onMappingChange={setMapping}
openapi={[openApiDocument]}
/>- Bind — claim an API field and an abstract. The UI side is tagged in
mapping.json. - Issues — unresolved abstracts and pipeline problems from
@x12i/io-matrix-schema. - Matrix —
ObjectPropertyIOMatrixfrom@x12i/io-matrix-view. ref.getDocument()/ref.getBindings()return the current contract for a host to persist or codegen against.
The standalone app's Binder tab loads public/examples/operations/ (ui.html, mapping.json, openapi.json).
Production build
npm run build # standalone app -> dist/
npm run build:lib # embeddable mapper -> dist-lib/ (@x12i/ui-data-mapper/mapper)
npm run build:binder # embeddable binder -> dist-binder/ (@x12i/ui-data-mapper/binder)
npm run previewThe static standalone site is generated in dist/; the embeddable component
build is generated in dist-lib/.
Memorix persistence (optional)
See MEMORIX.md. Copy .env.example to .env,
point VITE_MEMORIX_BASE_URL at a running @x12i/memorix-service, and restart
npm run dev. The topbar gains Load from Memorix / Save to Memorix;
file Import/Export keep working as the offline path.
npm run validate-memorix # envelope round-trip sanity (no network)Recurring mapping shape
{
"version": "1.0",
"ui": {
"atomSelectors": ["[data-field]", ".metric", "table td"]
},
"auto": { "enabled": false },
"repeaters": [
{
"id": "project-row",
"itemSelector": "#list > .row",
"dataPath": "$.projects",
"match": {
"mode": "key",
"dataPath": "id",
"uiAttribute": "id",
"uiPattern": "row-{value}"
},
"fields": [
{
"id": "project-code",
"dataPath": "code",
"selector": ".code",
"layer": "data",
"required": true,
"status": "verified"
}
]
}
]
}This means .code is defined once relative to a project row. At runtime, row-p1 maps to the object whose id is p1, regardless of row order or filtering. The same collection can have additional templates, such as a table row, without duplicating per-object map definitions.
