Skip to main content

Your own themes

A theme starts from the one it extends, Studio by default, and changes only what it sets. Try it here: change anything, and the editor and the code under it follow. A dot marks what you've changed, and ↺ puts it back.

Loading the theme builder…

The same in code. A brand colour is one line:

crosswalk.js
import { defineTheme } from "@crosswalkjs/canvas";

// Try another accent, `extends: "dracula"`, `links: "elbow"` or
// `shape: { node: "50%" }` for round tiles.
export const brand = defineTheme({
id: "brand",
label: "Brand",
extends: "nord",
colors: { accent: "#e5007d" },
});
main.js
import { createMapper } from "@crosswalkjs/canvas";
import { brand } from "./crosswalk.js";
import startingMap from "./order.map.json";

createMapper(document.getElementById("mapper"), { document: startingMap, theme: brand });
order.map.json
{
"version": 1,
"source": {
"type": "object",
"title": "Order",
"properties": {
"Number": {
"type": "string"
},
"Status": {
"type": "string"
},
"Billing": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Shipping": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Lines": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Sku": {
"type": "string"
},
"Date": {
"type": "string"
},
"Qty": {
"type": "number"
}
}
}
}
}
},
"target": {
"type": "object",
"title": "Invoice",
"properties": {
"Reference": {
"type": "string"
},
"Status": {
"type": "string"
},
"BillTo": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"ShipTo": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Products": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Sku": {
"type": "string"
},
"Units": {
"type": "number"
},
"FirstOrdered": {
"type": "string"
}
}
}
}
}
},
"loops": [
{
"source": "Lines[]",
"target": "Products[]",
"sortBy": [
{
"path": "Lines[].Date"
}
],
"groupBy": "Lines[].Sku"
}
],
"nodes": [
{
"id": "status",
"operator": "convert.lookup",
"constants": {
"1": {
"P": "Paid",
"S": "Shipped",
"C": "Cancelled"
},
"2": "Unknown"
},
"position": {
"x": 150,
"y": 50
}
},
{
"id": "units",
"operator": "aggregate.sum",
"scope": "Lines[]",
"position": {
"x": 150,
"y": 290
}
}
],
"links": [
{
"from": {
"source": "Number"
},
"to": {
"target": "Reference"
}
},
{
"from": {
"source": "Status"
},
"to": {
"node": "status",
"input": 0
}
},
{
"from": {
"node": "status"
},
"to": {
"target": "Status"
}
},
{
"from": {
"source": "Billing"
},
"to": {
"target": "BillTo"
},
"submap": "Address"
},
{
"from": {
"source": "Shipping"
},
"to": {
"target": "ShipTo"
},
"submap": "Address"
},
{
"from": {
"source": "Lines[].Sku"
},
"to": {
"target": "Products[].Sku"
}
},
{
"from": {
"source": "Lines[].Qty"
},
"to": {
"node": "units",
"input": 0
}
},
{
"from": {
"node": "units"
},
"to": {
"target": "Products[].Units"
}
},
{
"from": {
"source": "Lines[].Date"
},
"to": {
"target": "Products[].FirstOrdered"
}
}
],
"notes": [
{
"id": "note1",
"text": "One product per SKU, earliest order first. Units add up across the SKU's lines.",
"position": {
"x": 24,
"y": 22
},
"on": {
"node": "units"
}
}
],
"submaps": {
"Address": {
"source": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
},
"title": "Address"
},
"target": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
},
"title": "Address"
},
"nodes": [
{
"id": "postcode",
"operator": "text.upper",
"position": {
"x": 160,
"y": 90
}
}
],
"links": [
{
"from": {
"source": "Street"
},
"to": {
"target": "Line1"
}
},
{
"from": {
"source": "City"
},
"to": {
"target": "Town"
}
},
{
"from": {
"source": "Postcode"
},
"to": {
"node": "postcode",
"input": 0
}
},
{
"from": {
"node": "postcode"
},
"to": {
"target": "Postcode"
}
}
],
"notes": [
{
"id": "note1",
"text": "Both addresses on the order use this.",
"position": {
"x": 120,
"y": 190
}
}
]
}
}
}

Or start from any built-in and go further:

crosswalk.js
import { defineTheme } from "@crosswalkjs/canvas";

// Try extends: "dracula", links: "straight", or another colour for acme.
export const aurora = defineTheme({
id: "aurora",
label: "Aurora",
extends: "nord",
colors: { accent: "#f5a3c7", accentSoft: "rgba(245, 163, 199, .18)" },
categories: { acme: "#a3be8c" }, // tile colours, by category id
links: "elbow",
shape: { node: "50%", chip: "50%" }, // round tiles
});
main.js
import { createMapper } from "@crosswalkjs/canvas";
import { createRegistry } from "@crosswalkjs/core";
import { aurora } from "./crosswalk.js";
import { initials } from "./initials.js";
import startingMap from "./person.map.json";

// An Acme operator, so the theme's acme colour has a tile to show on.
const registry = createRegistry({ operators: [initials] });

createMapper(document.getElementById("mapper"), { document: startingMap, registry, theme: aurora });
initials.js
import { defineOperator, toText } from "@crosswalkjs/core";

export const initials = defineOperator({
kind: "value",
id: "acme.initials",
label: "Initials",
glyph: "AB.",
category: "acme",
description: "The first letter of each word, in capitals.",
inputs: { min: 1, max: 1, labels: ["name"] },
run: ([name]) =>
toText(name ?? null)
.split(/\s+/)
.filter(Boolean)
.map((word) => word[0].toUpperCase())
.join(""),
});
person.map.json
{
"version": 1,
"source": { "type": "object", "title": "Person", "properties": { "Name": { "type": "string" } } },
"target": { "type": "object", "title": "Badge", "properties": { "Name": { "type": "string" }, "Initials": { "type": "string" } } },
"nodes": [{ "id": "initials1", "operator": "acme.initials", "position": { "x": 120, "y": 60 } }],
"links": [
{ "from": { "source": "Name" }, "to": { "target": "Name" } },
{ "from": { "source": "Name" }, "to": { "node": "initials1", "input": 0 } },
{ "from": { "node": "initials1" }, "to": { "target": "Initials" } }
]
}

Pass a definition straight to the theme option or setTheme(). To use it by id, or have other themes extend it, register it first:

main.js
import { createMapper, defineTheme, listThemes, registerTheme } from "@crosswalkjs/canvas";
import startingMap from "./order.map.json";

const aurora = defineTheme({ id: "aurora", label: "Aurora", extends: "nord", colors: { accent: "#f5a3c7" }, links: "elbow" });

// Registered, it can be used by id, listed, and extended by other themes.
registerTheme(aurora);
const mapper = createMapper(document.getElementById("mapper"), { document: startingMap, theme: "aurora" });

const picker = document.getElementById("theme");
for (const theme of listThemes()) picker.append(new Option(theme.label, theme.id, false, theme.id === "aurora"));
picker.addEventListener("change", () => mapper.setTheme(picker.value));
index.html
<p style="margin: 8px"><label>Theme <select id="theme"></select></label></p>
<div id="mapper" style="height: 380px"></div>
order.map.json
{
"version": 1,
"source": {
"type": "object",
"title": "Order",
"properties": {
"Number": {
"type": "string"
},
"Status": {
"type": "string"
},
"Billing": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Shipping": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Lines": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Sku": {
"type": "string"
},
"Date": {
"type": "string"
},
"Qty": {
"type": "number"
}
}
}
}
}
},
"target": {
"type": "object",
"title": "Invoice",
"properties": {
"Reference": {
"type": "string"
},
"Status": {
"type": "string"
},
"BillTo": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"ShipTo": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Products": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Sku": {
"type": "string"
},
"Units": {
"type": "number"
},
"FirstOrdered": {
"type": "string"
}
}
}
}
}
},
"loops": [
{
"source": "Lines[]",
"target": "Products[]",
"sortBy": [
{
"path": "Lines[].Date"
}
],
"groupBy": "Lines[].Sku"
}
],
"nodes": [
{
"id": "status",
"operator": "convert.lookup",
"constants": {
"1": {
"P": "Paid",
"S": "Shipped",
"C": "Cancelled"
},
"2": "Unknown"
},
"position": {
"x": 150,
"y": 50
}
},
{
"id": "units",
"operator": "aggregate.sum",
"scope": "Lines[]",
"position": {
"x": 150,
"y": 290
}
}
],
"links": [
{
"from": {
"source": "Number"
},
"to": {
"target": "Reference"
}
},
{
"from": {
"source": "Status"
},
"to": {
"node": "status",
"input": 0
}
},
{
"from": {
"node": "status"
},
"to": {
"target": "Status"
}
},
{
"from": {
"source": "Billing"
},
"to": {
"target": "BillTo"
},
"submap": "Address"
},
{
"from": {
"source": "Shipping"
},
"to": {
"target": "ShipTo"
},
"submap": "Address"
},
{
"from": {
"source": "Lines[].Sku"
},
"to": {
"target": "Products[].Sku"
}
},
{
"from": {
"source": "Lines[].Qty"
},
"to": {
"node": "units",
"input": 0
}
},
{
"from": {
"node": "units"
},
"to": {
"target": "Products[].Units"
}
},
{
"from": {
"source": "Lines[].Date"
},
"to": {
"target": "Products[].FirstOrdered"
}
}
],
"notes": [
{
"id": "note1",
"text": "One product per SKU, earliest order first. Units add up across the SKU's lines.",
"position": {
"x": 24,
"y": 22
},
"on": {
"node": "units"
}
}
],
"submaps": {
"Address": {
"source": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
},
"title": "Address"
},
"target": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
},
"title": "Address"
},
"nodes": [
{
"id": "postcode",
"operator": "text.upper",
"position": {
"x": 160,
"y": 90
}
}
],
"links": [
{
"from": {
"source": "Street"
},
"to": {
"target": "Line1"
}
},
{
"from": {
"source": "City"
},
"to": {
"target": "Town"
}
},
{
"from": {
"source": "Postcode"
},
"to": {
"node": "postcode",
"input": 0
}
},
{
"from": {
"node": "postcode"
},
"to": {
"target": "Postcode"
}
}
],
"notes": [
{
"id": "note1",
"text": "Both addresses on the order use this.",
"position": {
"x": 120,
"y": 190
}
}
]
}
}
}

In your framework​

Define the theme once, in a module of its own:

crosswalk.ts
import { defineTheme } from "@crosswalkjs/canvas";

export const brand = defineTheme({ id: "brand", label: "Brand", extends: "nord", colors: { accent: "#e5007d" } });

Each framework's editor component takes a theme id or a definition as theme, and switches when it changes:

import { MapEditor } from "./MapEditor";
import { brand } from "./crosswalk";

<MapEditor map={map} theme={brand} />

Define the theme outside the component, as here, so it isn't a new object on every render.

Themes from your server​

A theme is plain JSON, so your server can store one, such as a theme per customer, and send it down. Pass what it sends as the theme, as above.

Every value in a theme is checked before it becomes CSS, wherever the theme came from. Something that could end its rule, start a comment or load a file, such as ;, {, }, /* or url(, is refused with an error naming the setting. So a theme a customer has edited can't restyle the rest of your page or fetch anything. Catch the error where you use the theme, and fall back to a built-in one:

main.js
import { createMapper, defineTheme } from "@crosswalkjs/canvas";
import startingMap from "./order.map.json";

const mapper = createMapper(document.getElementById("mapper"), { document: startingMap });

async function useSavedTheme(url) {
const response = await fetch(url);
let theme = "studio";
try {
theme = defineTheme(await response.json());
} catch (error) {
console.warn("Ignoring the saved theme:", error);
}
mapper.setTheme(theme);
}

// Try "/api/tampered-theme.json", whose accent tries to add CSS of its own.
useSavedTheme("/api/customer-theme.json");
api/customer-theme.json
{
"id": "customer",
"label": "Customer",
"extends": "solarized-light",
"colors": { "accent": "#2aa198" },
"links": "elbow"
}
api/tampered-theme.json
{
"id": "customer",
"label": "Customer",
"colors": { "accent": "red; } body { display: none" }
}
order.map.json
{
"version": 1,
"source": {
"type": "object",
"title": "Order",
"properties": {
"Number": {
"type": "string"
},
"Status": {
"type": "string"
},
"Billing": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Shipping": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Lines": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Sku": {
"type": "string"
},
"Date": {
"type": "string"
},
"Qty": {
"type": "number"
}
}
}
}
}
},
"target": {
"type": "object",
"title": "Invoice",
"properties": {
"Reference": {
"type": "string"
},
"Status": {
"type": "string"
},
"BillTo": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"ShipTo": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Products": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Sku": {
"type": "string"
},
"Units": {
"type": "number"
},
"FirstOrdered": {
"type": "string"
}
}
}
}
}
},
"loops": [
{
"source": "Lines[]",
"target": "Products[]",
"sortBy": [
{
"path": "Lines[].Date"
}
],
"groupBy": "Lines[].Sku"
}
],
"nodes": [
{
"id": "status",
"operator": "convert.lookup",
"constants": {
"1": {
"P": "Paid",
"S": "Shipped",
"C": "Cancelled"
},
"2": "Unknown"
},
"position": {
"x": 150,
"y": 50
}
},
{
"id": "units",
"operator": "aggregate.sum",
"scope": "Lines[]",
"position": {
"x": 150,
"y": 290
}
}
],
"links": [
{
"from": {
"source": "Number"
},
"to": {
"target": "Reference"
}
},
{
"from": {
"source": "Status"
},
"to": {
"node": "status",
"input": 0
}
},
{
"from": {
"node": "status"
},
"to": {
"target": "Status"
}
},
{
"from": {
"source": "Billing"
},
"to": {
"target": "BillTo"
},
"submap": "Address"
},
{
"from": {
"source": "Shipping"
},
"to": {
"target": "ShipTo"
},
"submap": "Address"
},
{
"from": {
"source": "Lines[].Sku"
},
"to": {
"target": "Products[].Sku"
}
},
{
"from": {
"source": "Lines[].Qty"
},
"to": {
"node": "units",
"input": 0
}
},
{
"from": {
"node": "units"
},
"to": {
"target": "Products[].Units"
}
},
{
"from": {
"source": "Lines[].Date"
},
"to": {
"target": "Products[].FirstOrdered"
}
}
],
"notes": [
{
"id": "note1",
"text": "One product per SKU, earliest order first. Units add up across the SKU's lines.",
"position": {
"x": 24,
"y": 22
},
"on": {
"node": "units"
}
}
],
"submaps": {
"Address": {
"source": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
},
"title": "Address"
},
"target": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
},
"title": "Address"
},
"nodes": [
{
"id": "postcode",
"operator": "text.upper",
"position": {
"x": 160,
"y": 90
}
}
],
"links": [
{
"from": {
"source": "Street"
},
"to": {
"target": "Line1"
}
},
{
"from": {
"source": "City"
},
"to": {
"target": "Town"
}
},
{
"from": {
"source": "Postcode"
},
"to": {
"node": "postcode",
"input": 0
}
},
{
"from": {
"node": "postcode"
},
"to": {
"target": "Postcode"
}
}
],
"notes": [
{
"id": "note1",
"text": "Both addresses on the order use this.",
"position": {
"x": 120,
"y": 190
}
}
]
}
}
}

If your server runs Node, call defineTheme when the theme is saved as well, so a bad one is turned away then.

Settings​

Everything except id and label is optional.

SettingWhat it changes
id, label, description, groupHow the theme is named. id starts with a letter and uses only letters, digits, - and _. group sorts themes in a picker.
extendsThe theme to start from: an id or a definition.
scheme"light" or "dark" fixes the scheme. "auto" follows the system and applies darkColors over colors in dark mode.
colors, darkColorsThe editor's colours (below).
categoriesOperator tile colours, by category id. Works for your own categories too. "*" colours every category, including ones added with a colour of their own; a category named as well keeps its own. A colour can be a variable, such as var(--cw-text), so tiles can follow light and dark mode, as ink does.
glyph, glyphsThe colour of the symbol on tiles, and per-category overrides where a tile is too light or too dark for it.
tileBorderAn outline for every tile, a little heavier than the usual one. Left out, each tile is outlined a shade darker than itself. With tiles the panel's colour and the glyph and outline the text colour, tiles are outlined rather than filled.
tileShadowA shadow for every tile, as a CSS box-shadow, such as 2px 2px 0 #15171c for tiles stamped in ink, as ink has them. Hover and selection rings add to it.
fontsui is a CSS font shorthand for the whole editor, such as 13px/1.4 Inter, sans-serif. glyph and code are font families for tile symbols and code-like text.
shapeCorner radii: radius for panels, node for operators on the grid, chip for the toolbox. 50% makes them round.
links, linkWidth"curved", "straight" or "elbow" (right angles), and the line width.
grid, gridSize"dots", "lines", "drafting" (major and minor lines) or "none", and the spacing in pixels.
density"compact", "regular" or "roomy": the height of tree rows and the size of operators and toolbox chips. Text size is part of fonts.ui.

Colours​

ColourUsed for
backgroundThe grid, and inputs.
panelThe toolbox, trees and inspector.
text, muted, faintMain text, secondary text, and labels and hints.
borderDividers and outlines.
gridThe grid's dots or lines.
hoverRows and buttons under the pointer.
linkLines between fields and operators.
accent, accentSoftSelection, highlighted lines and focus rings, and the soft fill behind them.
errorError badges and messages.
shadowA CSS box-shadow for tiles.
noteThe tint of notes, mixed with panel.

Most of them at once:

crosswalk.js
import { defineTheme } from "@crosswalkjs/canvas";

// Most of the settings at once. Try grid: "dots" or "drafting", links:
// "curved", or other corner radii.
export const tour = defineTheme({
id: "tour",
label: "Settings tour",
scheme: "light",
colors: {
background: "#fbfaf7",
panel: "#ffffff",
accent: "#0f766e",
accentSoft: "rgba(15, 118, 110, .14)",
link: "#94a3b8",
},
categories: { text: "#be123c", math: "#1d4ed8", flow: "#7c3aed" },
glyph: "#ffffff",
fonts: { ui: "13px/1.4 Georgia, serif", code: "ui-monospace, monospace" },
shape: { radius: "14px", node: "50%", chip: "8px" },
links: "elbow",
linkWidth: "2px",
grid: "lines",
gridSize: 24,
});
main.js
import { createMapper } from "@crosswalkjs/canvas";
import { tour } from "./crosswalk.js";
import startingMap from "./order.map.json";

createMapper(document.getElementById("mapper"), { document: startingMap, theme: tour });
order.map.json
{
"version": 1,
"source": {
"type": "object",
"title": "Order",
"properties": {
"Number": {
"type": "string"
},
"Status": {
"type": "string"
},
"Billing": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Shipping": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Lines": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Sku": {
"type": "string"
},
"Date": {
"type": "string"
},
"Qty": {
"type": "number"
}
}
}
}
}
},
"target": {
"type": "object",
"title": "Invoice",
"properties": {
"Reference": {
"type": "string"
},
"Status": {
"type": "string"
},
"BillTo": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"ShipTo": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Products": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Sku": {
"type": "string"
},
"Units": {
"type": "number"
},
"FirstOrdered": {
"type": "string"
}
}
}
}
}
},
"loops": [
{
"source": "Lines[]",
"target": "Products[]",
"sortBy": [
{
"path": "Lines[].Date"
}
],
"groupBy": "Lines[].Sku"
}
],
"nodes": [
{
"id": "status",
"operator": "convert.lookup",
"constants": {
"1": {
"P": "Paid",
"S": "Shipped",
"C": "Cancelled"
},
"2": "Unknown"
},
"position": {
"x": 150,
"y": 50
}
},
{
"id": "units",
"operator": "aggregate.sum",
"scope": "Lines[]",
"position": {
"x": 150,
"y": 290
}
}
],
"links": [
{
"from": {
"source": "Number"
},
"to": {
"target": "Reference"
}
},
{
"from": {
"source": "Status"
},
"to": {
"node": "status",
"input": 0
}
},
{
"from": {
"node": "status"
},
"to": {
"target": "Status"
}
},
{
"from": {
"source": "Billing"
},
"to": {
"target": "BillTo"
},
"submap": "Address"
},
{
"from": {
"source": "Shipping"
},
"to": {
"target": "ShipTo"
},
"submap": "Address"
},
{
"from": {
"source": "Lines[].Sku"
},
"to": {
"target": "Products[].Sku"
}
},
{
"from": {
"source": "Lines[].Qty"
},
"to": {
"node": "units",
"input": 0
}
},
{
"from": {
"node": "units"
},
"to": {
"target": "Products[].Units"
}
},
{
"from": {
"source": "Lines[].Date"
},
"to": {
"target": "Products[].FirstOrdered"
}
}
],
"notes": [
{
"id": "note1",
"text": "One product per SKU, earliest order first. Units add up across the SKU's lines.",
"position": {
"x": 24,
"y": 22
},
"on": {
"node": "units"
}
}
],
"submaps": {
"Address": {
"source": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
},
"title": "Address"
},
"target": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
},
"title": "Address"
},
"nodes": [
{
"id": "postcode",
"operator": "text.upper",
"position": {
"x": 160,
"y": 90
}
}
],
"links": [
{
"from": {
"source": "Street"
},
"to": {
"target": "Line1"
}
},
{
"from": {
"source": "City"
},
"to": {
"target": "Town"
}
},
{
"from": {
"source": "Postcode"
},
"to": {
"node": "postcode",
"input": 0
}
},
{
"from": {
"node": "postcode"
},
"to": {
"target": "Postcode"
}
}
],
"notes": [
{
"id": "note1",
"text": "Both addresses on the order use this.",
"position": {
"x": 120,
"y": 190
}
}
]
}
}
}

Fixing light or dark​

Studio follows the operating system. If your app has its own light and dark switch, make a fixed version of each and switch between them:

main.js
import { createMapper, registerTheme, resolveTheme } from "@crosswalkjs/canvas";
import startingMap from "./order.map.json";

const studio = resolveTheme("studio");
registerTheme({ id: "app-light", label: "Light", scheme: "light" });
registerTheme({ id: "app-dark", label: "Dark", scheme: "dark", colors: studio.darkColors });

const mapper = createMapper(document.getElementById("mapper"), { document: startingMap, theme: "app-light" });

// Your app's own switch.
let isDark = false;
document.getElementById("switch").addEventListener("click", () => {
isDark = !isDark;
mapper.setTheme(isDark ? "app-dark" : "app-light");
});
index.html
<p style="margin: 8px"><button id="switch">Switch light and dark</button></p>
<div id="mapper" style="height: 380px"></div>
order.map.json
{
"version": 1,
"source": {
"type": "object",
"title": "Order",
"properties": {
"Number": {
"type": "string"
},
"Status": {
"type": "string"
},
"Billing": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Shipping": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Lines": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Sku": {
"type": "string"
},
"Date": {
"type": "string"
},
"Qty": {
"type": "number"
}
}
}
}
}
},
"target": {
"type": "object",
"title": "Invoice",
"properties": {
"Reference": {
"type": "string"
},
"Status": {
"type": "string"
},
"BillTo": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"ShipTo": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
}
},
"Products": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Sku": {
"type": "string"
},
"Units": {
"type": "number"
},
"FirstOrdered": {
"type": "string"
}
}
}
}
}
},
"loops": [
{
"source": "Lines[]",
"target": "Products[]",
"sortBy": [
{
"path": "Lines[].Date"
}
],
"groupBy": "Lines[].Sku"
}
],
"nodes": [
{
"id": "status",
"operator": "convert.lookup",
"constants": {
"1": {
"P": "Paid",
"S": "Shipped",
"C": "Cancelled"
},
"2": "Unknown"
},
"position": {
"x": 150,
"y": 50
}
},
{
"id": "units",
"operator": "aggregate.sum",
"scope": "Lines[]",
"position": {
"x": 150,
"y": 290
}
}
],
"links": [
{
"from": {
"source": "Number"
},
"to": {
"target": "Reference"
}
},
{
"from": {
"source": "Status"
},
"to": {
"node": "status",
"input": 0
}
},
{
"from": {
"node": "status"
},
"to": {
"target": "Status"
}
},
{
"from": {
"source": "Billing"
},
"to": {
"target": "BillTo"
},
"submap": "Address"
},
{
"from": {
"source": "Shipping"
},
"to": {
"target": "ShipTo"
},
"submap": "Address"
},
{
"from": {
"source": "Lines[].Sku"
},
"to": {
"target": "Products[].Sku"
}
},
{
"from": {
"source": "Lines[].Qty"
},
"to": {
"node": "units",
"input": 0
}
},
{
"from": {
"node": "units"
},
"to": {
"target": "Products[].Units"
}
},
{
"from": {
"source": "Lines[].Date"
},
"to": {
"target": "Products[].FirstOrdered"
}
}
],
"notes": [
{
"id": "note1",
"text": "One product per SKU, earliest order first. Units add up across the SKU's lines.",
"position": {
"x": 24,
"y": 22
},
"on": {
"node": "units"
}
}
],
"submaps": {
"Address": {
"source": {
"type": "object",
"properties": {
"Street": {
"type": "string"
},
"City": {
"type": "string"
},
"Postcode": {
"type": "string"
}
},
"title": "Address"
},
"target": {
"type": "object",
"properties": {
"Line1": {
"type": "string"
},
"Town": {
"type": "string"
},
"Postcode": {
"type": "string"
}
},
"title": "Address"
},
"nodes": [
{
"id": "postcode",
"operator": "text.upper",
"position": {
"x": 160,
"y": 90
}
}
],
"links": [
{
"from": {
"source": "Street"
},
"to": {
"target": "Line1"
}
},
{
"from": {
"source": "City"
},
"to": {
"target": "Town"
}
},
{
"from": {
"source": "Postcode"
},
"to": {
"node": "postcode",
"input": 0
}
},
{
"from": {
"node": "postcode"
},
"to": {
"target": "Postcode"
}
}
],
"notes": [
{
"id": "note1",
"text": "Both addresses on the order use this.",
"position": {
"x": 120,
"y": 190
}
}
]
}
}
}

That's what these docs do, so the editors here follow the site's own toggle.

How themes reach the page​

A theme becomes CSS custom properties (--cw-*) under .cw-root[data-cw-theme="<id>"], added to the page the first time the theme is used. resolveTheme() returns a theme with everything filled in from what it extends, and themeStyles() returns its CSS, if you'd rather ship it in a stylesheet yourself.