@coldsmirk/caliper-mantine
v0.1.0
Published
Mantine 9 visual builders for structured data: <SchemaBuilder> for the supported JSON Schema subset and <JsonBuilder> for arbitrary JSON values, editor-agnostic through an injectable JSON editor slot.
Maintainers
Readme
@coldsmirk/caliper-mantine
Mantine 9 visual builders for structured data: <SchemaBuilder> authors a shape (the supported JSON Schema subset), <JsonBuilder> authors an instance (any JSON value). Each pairs a typed row tree with the raw document behind one mode toggle, and the document stays the single source of truth — every visual edit serializes straight back through onChange, and anything the tree cannot host faithfully stays on the source face with the reason spelled out instead of being projected lossily.
Part of caliper; the tree model is @coldsmirk/caliper-core.
pnpm add @coldsmirk/caliper-mantine @mantine/core react react-dom@mantine/core (^9), react, and react-dom (>=19) are peer dependencies; render the builders inside your app's MantineProvider. @coldsmirk/caliper-core is a regular dependency and installs with the package — no editor or CodeMirror/Monaco package is in that graph, by design (see Custom JSON editors).
Styles
Import the stylesheet once at your app root, after Mantine's styles:
@import "@mantine/core/styles.css";
@import "@coldsmirk/caliper-mantine/styles.css";Every colour in it is a Mantine token, and every metric either scales from Mantine's variables or is a deliberate layout constant, so the builders follow your theme and colour scheme with nothing to configure. The rules live in the caliper cascade layer, so any unlayered declaration of yours overrides them without a specificity battle. See Theming for the three tokens caliper owns.
SchemaBuilder
<SchemaBuilder> edits the object-field subset of a JSON Schema as a field tree or as raw JSON. The field face round-trips the supported draft 2020-12 keywords — type, properties, required, items, description, plus the exact $schema declaration — and preserves whether root and nested object schemas explicitly declared type: "object". Unsupported keywords or shapes keep the document intact on the source face with a structured explanation, so the builder never projects them lossily; a document that arrives out of subset opens on the source face rather than on a dead notice. An array whose source omitted items exposes Add element type, materializing its element schema without retyping the field. Sibling name collisions are flagged inline on every colliding row, since duplicate keys silently overwrite each other in the serialized properties.
Generate from sample infers a schema from a pasted payload. It accepts a JSON object root only — arrays, scalars, and null stay in the modal with a localizable error — and the sample text survives close/reopen on purpose, because iterating on a sample is the common loop. Inference keeps observed field shapes but deliberately does not mark anything required.
import { SchemaBuilder } from "@coldsmirk/caliper-mantine";
import { useState } from "react";
function PayloadSchema() {
const [schema, setSchema] = useState("{\"type\":\"object\"}");
return <SchemaBuilder ariaLabel="Payload schema" value={schema} onChange={setSchema} />;
}Props
| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| value | string | — | The schema document, fully controlled (see The controlled contract). |
| onChange | (value: string) => void | — | Called with the serialized document on every edit, from either face. |
| valueVersion | number \| string | — | Monotonic marker for authoritative writes that re-assert the same string (see below). |
| labels | Partial<SchemaBuilderLabels> | English | Built-in text overrides, merged over the defaults. |
| typeLabels | Partial<Record<SchemaTreeFieldType, string>> | English | Display names for the field-type select. |
| placeholder | string | — | Placeholder for the source (JSON) face. |
| ariaLabel | string | — | Accessible name for both faces — the source editor's textbox and the field tree's ARIA group. |
| icons | SchemaBuilderIcons | none | generateFromSample / addField / generate nodes, rendered as each button's left section. |
| renderSourceEditor | JsonEditorSlot | monospace Textarea | The source (JSON) face. |
| renderSampleEditor | JsonEditorSlot | monospace Textarea | The sample input inside the generate-from-sample modal. |
JsonBuilder
<JsonBuilder> edits an arbitrary JSON document as a typed node tree or as raw JSON. Objects, arrays, strings, numbers, booleans, and null can be added, removed, renamed, or retyped; container children are kept aside across a scalar retype, so toggling the type back restores them, and a scalar payload carries over when it converts faithfully (string ↔ number, anything → string). Object keys are compared exactly — JSON permits blank and whitespace-only names — and a freshly added row stays an unserialized draft until its key is edited, so an empty document is never invented.
Invalid JSON stays intact on the source face instead of being rewritten. So do number tokens that cannot round-trip faithfully through a JavaScript number — unsafe integers, overflow, precision-folded decimals — because the alternative is silently changing a value the author typed. A blank document seeds an editable null root.
import { JsonBuilder } from "@coldsmirk/caliper-mantine";
import { useState } from "react";
function RuntimeConfig() {
const [value, setValue] = useState("{\"retries\":3}");
return <JsonBuilder ariaLabel="Runtime configuration" value={value} onChange={setValue} />;
}Props
| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| value | string | — | The JSON text, fully controlled (see The controlled contract). |
| onChange | (value: string) => void | — | Called with the serialized document (pretty-printed, trailing newline) on every edit. |
| valueVersion | number \| string | — | Monotonic marker for authoritative writes that re-assert the same string (see below). |
| labels | Partial<JsonBuilderLabels> | English | Built-in text overrides, merged over the defaults. |
| typeLabels | Partial<Record<JsonNodeType, string>> | English | Display names for the value-type select. |
| placeholder | string | — | Placeholder for the source (JSON) face. |
| ariaLabel | string | — | Accessible name for both faces — the source editor's textbox and the visual tree's ARIA group. |
| renderSourceEditor | JsonEditorSlot | monospace Textarea | The source (JSON) face. |
The controlled contract
Both builders are fully controlled on their document text, and both reconcile the same way. Update value synchronously from onChange: that exact parent echo retains blank draft rows and stable row ids, while any different value is authoritative and re-projects the tree immediately — so a host reset, a normalization, or a rejected edit always takes effect, and an intervening replacement can never resurrect a stale draft.
String identity cannot distinguish an ordinary same-value re-render from an authoritative write that happens to re-assert the current text (a form.reset() back to the initial document, a rejected edit). Bump the optional valueVersion on those writes: a changed version discards drafts and remounts a create-once source editor, while ordinary renders retain both. It has no effect unless it changes.
Custom JSON editors
Both builders default to a monospace Mantine Textarea for their source face. Inject CodeMirror, Monaco, or any other JSON editor through the shared JsonEditorSlot — JsonBuilder.renderSourceEditor, SchemaBuilder.renderSourceEditor, and SchemaBuilder.renderSampleEditor all take one. That injection point is why this package peers no editor at all: the editor is the host's choice, and its dependency graph is the host's.
import type { JsonEditorSlot } from "@coldsmirk/caliper-mantine";
const renderJsonEditor: JsonEditorSlot = props => (
<JsonCodeEditor
key={props.editorKey}
aria-label={props.ariaLabel}
placeholder={props.placeholder}
value={props.value}
onChange={props.onChange}
/>
);
<JsonBuilder renderSourceEditor={renderJsonEditor} value={json} onChange={setJson} />;
<SchemaBuilder
renderSampleEditor={renderJsonEditor}
renderSourceEditor={renderJsonEditor}
value={schema}
onChange={setSchema}
/>;Keep a controlled editor synchronized from value. For create-once editor wrappers, apply editorKey as the element key so a generated or replaced document remounts the editor with the new text instead of leaving its internal buffer stale.
Text & localization
Every built-in string is overridable per instance — labels for the chrome and typeLabels for the type-select display names — and both merge over the English defaults, so a partial object is enough:
<SchemaBuilder
labels={{ fieldsMode: "字段", sourceMode: "JSON", addField: "添加字段" }}
typeLabels={{ string: "文本", object: "对象" }}
value={schema}
onChange={setSchema}
/>labels members that format a payload are functions: unsupported(issue) renders a SchemaTreeIssue, sampleInvalid(message) the sample's JSON error, and removeFieldHint(n) / removeNodeHint(n) the remove button's blast radius. schemaTreeIssueText is exported as the English phrase behind the default unsupported, so a localized formatter can reuse or replace parts of it. defaultSchemaBuilderLabels, defaultSchemaTypeLabels, defaultJsonBuilderLabels, and defaultJsonTypeLabels are exported to build a catalog from.
The package ships no icons: SchemaBuilder's icons prop takes generateFromSample, addField, and generate nodes from your own icon set, keyed like the matching labels members.
Theming
Colours, spacing, radii, and borders come from Mantine's own CSS variables. The knobs caliper owns are these three, each read with its built-in fallback:
| Token | Default | What it sizes |
| ------------------------------ | -------------------------------------- | ---------------------------------------------------------------- |
| --caliper-schema-type-width | 7rem | The field-type select in <SchemaBuilder>. |
| --caliper-json-type-width | 7rem | The value-type select in <JsonBuilder>. |
| --caliper-font-family-code | var(--mantine-font-family-monospace) | The fallback source / sample textareas and array index labels. |
The type selects hold a fixed slot instead of flexing, so the name and description inputs beside them keep the free width. The default is sized for the English type labels; two-CJK-character labels sit well near 4.5rem:
:root {
--caliper-schema-type-width: 4.5rem;
--caliper-json-type-width: 4.5rem;
}Exports
- Components:
SchemaBuilderandJsonBuilder, with their props types, label types, default labels, type-label maps, and theJsonNodeTypeunion keyingJsonBuilder's;SchemaBuilderIcons;JsonEditorSlot/JsonEditorProps;schemaTreeIssueText. - The tree model re-exported from
@coldsmirk/caliper-core, so one import site is enough to name every type in the components' own props:parseSchemaTree,serializeSchemaTree,inferSchema,newSchemaTreeField,SCHEMA_DIALECT_2020_12,Json, and theSchemaTree*types.
License
UNLICENSED — proprietary. All rights reserved; no use, copying, or redistribution without the author's permission.
