For developers

Developing plugins

A plugin is a single .js file. It changes the look or features of mind3D without touching the core of the app. Plugins can be switched on and off in the app via the 🧩 button on the start screen – without restarting. Everything a plugin registers through the API is removed automatically when it is switched off.

Applies to both versions (Android and USB/desktop) – the plugin code is identical.

Template to copy: plugin-vorlage.js (download) Detailed examples in the app assets: plugins/farbschema.js (design), plugins/projekte.js (own views, data in the mind map's plugin area), plugins/ansicht3d.js (full-screen view with a lazily loaded library) and plugins/markdown-import.js (import format)

Skeleton

mind3D.plugins.register({
  id: "my-plugin",            // unique, only a-z 0-9 . _ -
  name: "My plugin",
  version: "1.0.0",
  author: "Name",
  icon: "✨",                  // emoji in the plugin list
  description: "What the plugin does.",
  apiVersion: 1,              // API version the plugin was written for

  settings: [ /* optional, see below */ ],

  activate(api) {
    // Register everything here. Runs when switched on and on every page start.
  },

  deactivate(api) {
    // Optional. Registrations made through the API are removed automatically.
  },
});

activate runs on both pages of the app. api.page tells you where:

api.pagePage
"menu"start screen / project list
"map"open mind map

Functions that make no sense on a page (e.g. a toolbar button on the menu) simply do nothing there.

API overview

All functions that register something return a function that removes the registration early. Normally you don't need it.

Appearance

CallEffect
api.theme.set({ accent, accent2, colors, css, nodeTypes, palette })change colours, calling again = replace. Details below.
api.theme.clear()remove own colours
api.ui.addStyle(css)insert own CSS (e.g. buttons, dialogs)
api.background.replace(factory)replace the background (see below)

Theme in detail

api.theme.set({
  accent:  "#a07cff",   // colours the ENTIRE design: buttons, logo, borders, nodes, connections
  accent2: "#ff4fd8",   // optional: gradient accent → accent2 (null = single colour)
  colors:  { text: "#ffffff" },              // individual canvas colours from COLORS
  css:     { "--accent-border": "#333" },     // individual CSS variables
  nodeTypes: { text: { card: "#101010", hdr: "#181818" } },
  palette: ["#ff0000", "#00ff00"],          // colour picker for borders (replaces it)
  paletteExtra: ["#a07cff"],                // extra colours at the front of the picker
});

From accent the app automatically derives all shades (bright, muted, dark, tinted borders/hover areas) and picks black or white as the text colour on accent buttons. The CSS variables for this are --accent, --accent-rgb, --accent-bright, --accent-deep, --accent-border, --on-accent, --accent-2 and --accent-gradient – your own plugin CSS can use them too. The values set last are applied immediately on the next start (no flash).

Drawing on the mind map

api.render.before((ctx, info) => { … });          // before everything else
api.render.after((ctx, info) => { … });           // on top of everything
api.render.nodeBefore((ctx, node, rect, info) => { … });  // behind each node
api.render.nodeAfter((ctx, node, rect, info) => { … });   // on top of each node
api.render.redraw();                               // request a redraw
  • ctx – canvas 2D context in CSS pixels (state is saved/restored automatically)
  • info – { cssW, cssH, viewport, nodes, selectedId }
  • rect – screen rectangle of the node { x1, y1, x2, y2, z, selected } (z = zoom)

The mind map only redraws when something changes. For animations, call api.render.redraw() regularly (e.g. with api.every(33, …)).

Own background

api.background.replace((canvas, { viewport, page }) => {
  const ctx = canvas.getContext("2d");      // or "webgl"
  return {
    resize(w, h, dpr) { … },   // required: size in CSS pixels + pixel density
    start() { … },             // start the animation
    destroy() { … },           // end the animation (or stop())
  };
});

The canvas is fresh on every replacement. viewport (zoom, panX, panY) only exists on the mind map page. When switched off, the default space background returns.

Own node types

api.nodes.registerType({
  type: "checklist",                // not: mindmap, text, image, sound, document
  label: "Checklist",               // name in the ➕ menu and in the editor
  icon: "☑",
  colors: { card: "#0a1a10", hdr: "#0d2818" },
  create(node) { api.data.set(node, { items: [] }); },   // when created
  draw(ctx, node, rect) { … },      // draw the content area (rect as above, without header)
  open(node) { return false; },     // double-click; true = handled, otherwise editor
  editor: {
    render(container, node) { … },  // add own fields to the node editor
    save(container, node) { … },    // take over values on save
  },
});

The type appears in the ➕ menu automatically. If the plugin is switched off later, these nodes are kept along with their data and appear as a “Plugin not active” placeholder.

Data

CallStored
api.data.get(node) / api.data.set(node, obj)on the node, in the project (travels with export/import). After set, commit with api.map.save().
api.storage.get(key, fallback) / api.storage.set(key, value)per device, across projects
api.settings.get(key) / api.settings.set(key, value)settings (see below)

Security mode (api.security, read only): mode() → "standard" | "global" | "tresor", protectsMindmaps() → true in “Encrypt everything” mode, isUnlocked(). api.storage and api.settings live unencrypted in localStorage. If protectsMindmaps() returns true, no content or names of mind maps may end up there. Keep caches in memory only in that case (example: plugins/projekte.js). Always read and write mind maps themselves through api.mindmaps/api.map: these paths encrypt automatically. Vault mind maps are not visible to plugins.

The open mind map (api.map, only on "map", otherwise null)

Call
api.map.nodes()nodes of the current level
api.map.allNodes()all nodes of the project
api.map.selected() / api.map.select(node)selection
api.map.addNode(type, text, x?, y?)create a node (without position: centre of the screen)
api.map.save()commit a change: undo step + autosave + redraw
api.map.viewport(){ zoom, panX, panY, worldToScreen(), screenToWorld() }
api.map.navigateTo(id)jump straight to a node's level and select it (zoom hysteresis is set correctly)
api.map.suspend() / resume()pause/resume the 2D view (for full-screen views like the 3D view)
api.map.state / api.map.appinternal access – no stable contract, emergencies only

Managing mind maps (api.mindmaps, on both pages)

Reading and writing through the app's normal saving logic – in the mind map JSON format.

Call
await api.mindmaps.list()mind maps of the signed-in user ([{ name, last_modified, saves }])
await api.mindmaps.load(name) / save(name, data)load / save mind map JSON
await api.mindmaps.create(name)new mind map, returns the actual (possibly numbered) name
api.mindmaps.open(name)open in the editor (start screen only)
api.mindmaps.root(data) / children(data, id)find nodes
api.mindmaps.addNode(data, { text, parent_id, node_type, body_text, plugin_data })create a node (position automatic)
api.mindmaps.removeNode(data, id)remove a node with its child nodes and connections
api.mindmaps.meta(data) / setMeta(data, obj)plugin area in the JSON: data.plugins[pluginId]

Basic rule for data: everything that should be visible without the plugin belongs in normal nodes (text, child nodes). Plugin-specific extra information belongs in the plugin area plugins.<id>. The app passes this area through unchanged when loading and saving, even while the plugin is switched off. So every mind map stays readable without the plugin, and nothing is lost with it.

In the open mind map: api.map.meta() / api.map.setMeta(obj), then api.map.save().

Events

api.events.on("map:loaded",    (state) => …);
api.events.on("map:changed",   (state) => …);  // after every change
api.events.on("map:saved",     (state) => …);
api.events.on("node:created",  (node) => …);
api.events.on("node:deleted",  (node) => …);
api.events.on("node:selected", (nodeOrNull) => …);
api.events.on("level:changed", (contextNodeOrNull) => …);  // level entered/left
api.events.on("menu:projectsShown", (username) => …);      // project list shown

User interface

Call
api.ui.toast(text, "ok" | "error")short notice
api.ui.dialog({ title, message, content, buttons })dialog → Promise with the button's value. buttons: [{ label, value, primary?, danger? }], content: own DOM element
api.ui.confirm(title, message, okLabel)yes/no → Promise<boolean>
api.ui.addToolbarButton({ icon, title, onClick })button in the mind map toolbar
api.ui.addStartButton({ icon, title, onClick })round button on the start screen
api.ui.addProjectTab({ id, label, icon, render(el, { username }) })own tab next to “Mindmaps” in the project list
api.ui.selectProjectTab(id)select a tab (null = “Mindmaps”)
api.ui.decorateMindmapCard((cardEl, entry, username) => …)add to the cards of the mind map list
api.ui.addImportOption({ icon, label, accept, directory, onFile(file, files) })own import format in the “New project” dialog; onFile returns { name, data } (mind map JSON). directory: true = folder selection (all files in files, hidden automatically on Android)
api.ui.onBack(() => true/false)intercept the Android back key; true = handled
api.ui.addElement(el, parent?)attach any element
api.keys.add("ctrl+k", fn)keyboard shortcut (ctrl, shift, alt; not while typing text)

Automatic clean-up

Call
api.every(ms, fn) / api.after(ms, fn)timers that are stopped when switched off
api.listen(target, type, fn)DOM listener that is removed when switched off
api.onDispose(fn)own clean-up

Important for switching on/off without restarting: don't use your own setInterval, addEventListener etc. directly, use the variants above – otherwise they keep running after the plugin is switched off.

Language

The app speaks English and German. The current language is in document.documentElement.lang ("en" or "de"). Give your plugin's texts in both languages and pick them by this value; the app only translates its own texts, not those of your plugin.

Settings

The app builds the settings dialog (⚙ in the plugin list) automatically:

settings: [
  { key: "colour",  type: "color",   label: "Colour",      default: "#00d4ff" },  // picker + hex field
  { key: "preset",  type: "swatches", label: "Preset",     default: "blue",       // colour swatches
    options: [{ value: "blue", label: "Blue", color: "#4d8dff" },
              { value: "red",  label: "Red",  color: "#ff4d5e" }] },
  { key: "strength", type: "range",  label: "Strength",    default: 0.5, min: 0, max: 1, step: 0.05 },
  { key: "count",   type: "number",  label: "Count",       default: 3, min: 1 },
  { key: "on",      type: "boolean", label: "Enabled",     default: true },          // switch
  { key: "mode",    type: "select",  label: "Mode",        default: "a",
    options: [{ value: "a", label: "Variant A" }, { value: "b", label: "Variant B" }] },
  { key: "text",    type: "text",    label: "Caption",     default: "", hint: "Optional hint" },
]

Changes take effect immediately: api.settings.onChange((key, value, all) => …).

  • color shows a colour picker and a hex field (#rrggbb, also the short rgb form; invalid input is marked and not applied).
  • swatches shows rounded colour swatches; the value of the chosen swatch is stored.
  • boolean appears as a switch.

Show a field only under a condition: { key: "colour2", type: "color", label: "Second colour", showIf: { gradient: true } }. If the plugin changes a setting itself (api.settings.set), the open dialog follows.

The app's security rules (apply to plugins too)

  • No network: the Content Security Policy blocks any loading from or sending to the internet (fetch, images, scripts from servers). Plugins only work with local data and files from plugins/. Images/sounds as data: or blob: work.
  • Allowed are own scripts, eval/new Function and inline styles (api.ui.addStyle).
  • Never insert user text as HTML unchecked: use textContent or escape it – including quotes if the value ends up in an attribute (value="…").
  • Don't store mind map data past the core (no own localStorage/IndexedDB for content). Otherwise it ends up on the device as plain text in high-security mode.
  • Own import formats hand their data to the core; there it goes through the import check (js/map-validate.js) and images through the metadata clean-up (js/media-clean.js).

Stability

  • Every call into a plugin is guarded. An error does not crash the app.
  • After 5 errors a plugin is switched off automatically (with a notice).
  • If activate fails, everything already registered is rolled back.
  • apiVersion higher than the app's → the plugin is shown as “requires a newer version”.

Installing and distributing

  • From a file: 🧩 → “Install plugin from file” → choose the .js. Installing the same id again = update.