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.
The same in code. A brand colour is one line:
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" },
});
import { createMapper } from "@crosswalkjs/canvas";
import { brand } from "./crosswalk.js";
import startingMap from "./order.map.json";
createMapper(document.getElementById("mapper"), { document: startingMap, theme: brand });
{
"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:
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
});
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 });
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(""),
});
{
"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:
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));
<p style="margin: 8px"><label>Theme <select id="theme"></select></label></p>
<div id="mapper" style="height: 380px"></div>
{
"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:
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:
- React
- Vue
- Svelte
- Angular
- Solid
- Astro
- Web Components
- Plain JavaScript
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:
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");
{
"id": "customer",
"label": "Customer",
"extends": "solarized-light",
"colors": { "accent": "#2aa198" },
"links": "elbow"
}
{
"id": "customer",
"label": "Customer",
"colors": { "accent": "red; } body { display: none" }
}
{
"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.
| Setting | What it changes |
|---|---|
id, label, description, group | How the theme is named. id starts with a letter and uses only letters, digits, - and _. group sorts themes in a picker. |
extends | The 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, darkColors | The editor's colours (below). |
categories | Operator 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, glyphs | The colour of the symbol on tiles, and per-category overrides where a tile is too light or too dark for it. |
tileBorder | An 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. |
tileShadow | A 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. |
fonts | ui 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. |
shape | Corner 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
| Colour | Used for |
|---|---|
background | The grid, and inputs. |
panel | The toolbox, trees and inspector. |
text, muted, faint | Main text, secondary text, and labels and hints. |
border | Dividers and outlines. |
grid | The grid's dots or lines. |
hover | Rows and buttons under the pointer. |
link | Lines between fields and operators. |
accent, accentSoft | Selection, highlighted lines and focus rings, and the soft fill behind them. |
error | Error badges and messages. |
shadow | A CSS box-shadow for tiles. |
note | The tint of notes, mixed with panel. |
Most of them at once:
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,
});
import { createMapper } from "@crosswalkjs/canvas";
import { tour } from "./crosswalk.js";
import startingMap from "./order.map.json";
createMapper(document.getElementById("mapper"), { document: startingMap, theme: tour });
{
"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:
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");
});
<p style="margin: 8px"><button id="switch">Switch light and dark</button></p>
<div id="mapper" style="height: 380px"></div>
{
"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.