@restless-dreams/orbit 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -2
  3. package/dist/bin/orbit.js +8907 -0
  4. package/dist/bin/orbit.js.map +48 -0
  5. package/dist/chunk-0gwh723s.js +408 -0
  6. package/dist/chunk-0gwh723s.js.map +10 -0
  7. package/dist/chunk-7n00wx0t.js +82 -0
  8. package/dist/chunk-7n00wx0t.js.map +10 -0
  9. package/dist/chunk-mtrnm0yc.js +16993 -0
  10. package/dist/chunk-mtrnm0yc.js.map +63 -0
  11. package/dist/core/auto-render.d.ts +85 -0
  12. package/dist/core/browser.d.ts +23 -0
  13. package/dist/core/create.d.ts +23 -0
  14. package/dist/core/diagnostics.d.ts +40 -0
  15. package/dist/core/dispatch.d.ts +23 -0
  16. package/dist/core/document-options.d.ts +18 -0
  17. package/dist/core/document-source.d.ts +128 -0
  18. package/dist/core/editing.d.ts +63 -0
  19. package/dist/core/engine/animation-clock.d.ts +40 -0
  20. package/dist/core/engine/camera.d.ts +70 -0
  21. package/dist/core/engine/colors.d.ts +51 -0
  22. package/dist/core/engine/display.d.ts +125 -0
  23. package/dist/core/engine/export.d.ts +32 -0
  24. package/dist/core/engine/glide.d.ts +34 -0
  25. package/dist/core/engine/labels.d.ts +106 -0
  26. package/dist/core/engine/node-drag.d.ts +49 -0
  27. package/dist/core/engine/scene.d.ts +159 -0
  28. package/dist/core/engine/spin.d.ts +62 -0
  29. package/dist/core/engine/surface-pattern.d.ts +20 -0
  30. package/dist/core/engine/text-measure.d.ts +116 -0
  31. package/dist/core/engine/views.d.ts +353 -0
  32. package/dist/core/extensions.d.ts +155 -0
  33. package/dist/core/format-upgrades.d.ts +33 -0
  34. package/dist/core/index.d.ts +36 -0
  35. package/dist/core/input-controller.d.ts +109 -0
  36. package/dist/core/languages.d.ts +199 -0
  37. package/dist/core/layout.d.ts +61 -0
  38. package/dist/core/map-tools/definitions.d.ts +71 -0
  39. package/dist/core/map-tools/input.d.ts +62 -0
  40. package/dist/core/map-tools/reading.d.ts +40 -0
  41. package/dist/core/map-tools/tools.d.ts +51 -0
  42. package/dist/core/markdown-writing.d.ts +87 -0
  43. package/dist/core/material.d.ts +25 -0
  44. package/dist/core/media.d.ts +42 -0
  45. package/dist/core/node-attributes.d.ts +31 -0
  46. package/dist/core/node-links.d.ts +25 -0
  47. package/dist/core/parser-markdown.d.ts +42 -0
  48. package/dist/core/parser-yaml.d.ts +8 -0
  49. package/dist/core/render-to-svg.d.ts +57 -0
  50. package/dist/core/serializer-markdown.d.ts +18 -0
  51. package/dist/core/serializer-yaml.d.ts +2 -0
  52. package/dist/core/source-patch.d.ts +24 -0
  53. package/dist/core/source.d.ts +27 -0
  54. package/dist/core/styles.d.ts +6 -0
  55. package/dist/core/suggest.d.ts +8 -0
  56. package/dist/core/svg-renderer.d.ts +729 -0
  57. package/dist/core/text-layout.d.ts +54 -0
  58. package/dist/core/themes.d.ts +62 -0
  59. package/dist/core/types.d.ts +463 -0
  60. package/dist/core/ui/actions.d.ts +75 -0
  61. package/dist/core/ui/controls.d.ts +61 -0
  62. package/dist/core/ui/detail-sheet.d.ts +88 -0
  63. package/dist/core/ui/frames.d.ts +11 -0
  64. package/dist/core/ui/icons.d.ts +14 -0
  65. package/dist/core/ui/inline-editor.d.ts +79 -0
  66. package/dist/core/ui/media-players.d.ts +65 -0
  67. package/dist/core/ui/media-viewer.d.ts +169 -0
  68. package/dist/core/ui/media-zoom.d.ts +51 -0
  69. package/dist/core/ui/note-markdown.d.ts +19 -0
  70. package/dist/core/ui/outline.d.ts +44 -0
  71. package/dist/core/url-guard.d.ts +40 -0
  72. package/dist/core/utils.d.ts +23 -0
  73. package/dist/core/web-page/page.d.ts +58 -0
  74. package/dist/core/web-page/runtime.d.ts +13 -0
  75. package/dist/core/web-page/start.d.ts +1 -0
  76. package/dist/core/yaml-mini.d.ts +54 -0
  77. package/dist/index.d.ts +1 -0
  78. package/dist/index.js +1448 -0
  79. package/dist/index.js.map +17 -0
  80. package/dist/marked/index.d.ts +31 -0
  81. package/dist/marked/index.js +110 -0
  82. package/dist/marked/index.js.map +10 -0
  83. package/dist/react/index.d.ts +55 -0
  84. package/dist/react/index.js +352 -0
  85. package/dist/react/index.js.map +1 -0
  86. package/dist/web-page.d.ts +2 -0
  87. package/dist/web-page.js +130 -0
  88. package/llms-full.txt +2585 -0
  89. package/llms.txt +35 -0
  90. package/package.json +118 -4
  91. package/skills/orbit/SKILL.md +140 -0
  92. package/skills/orbit/references/editing.md +194 -0
  93. package/skills/orbit/references/extending.md +247 -0
  94. package/skills/orbit/references/format.md +303 -0
  95. package/skills/orbit/references/guide.md +522 -0
  96. package/skills/orbit/references/languages.md +159 -0
  97. package/skills/orbit/references/map-tools.md +188 -0
  98. package/skills/orbit/references/media.md +160 -0
  99. package/skills/orbit/references/recipes.md +227 -0
  100. package/skills/orbit/references/theming.md +184 -0
@@ -0,0 +1,522 @@
1
+ <!-- A copy of docs/guide.md, made by scripts/build-llms.ts, with links to other files pointing at the repository. Edit the original. -->
2
+
3
+ # Using Orbit
4
+
5
+ How to draw maps on any page, in React, with the `<orbit-map>` element or in a markdown pipeline, and everything a map offers: its options and events, how people move around it, pictures and videos, editing, export and theming. [The map format](format.md) is the reference for writing maps, and [Recipes](recipes.md) shows Orbit in each framework.
6
+
7
+ ## Drawing a map
8
+
9
+ ```ts
10
+ import { createOrbitMap } from '@restless-dreams/orbit';
11
+
12
+ const map = createOrbitMap(document.getElementById('map')!, `# 🚀 Project
13
+
14
+ ## Research
15
+ ## Build
16
+ ## Launch`);
17
+
18
+ map.on('focus-change', ({ node }) => console.log('Now looking at', node.text));
19
+ ```
20
+
21
+ The map fills its container, so give the container a size. `createOrbitMap` returns the renderer: call `map.render(newMarkdown)` to draw a new version (the reader keeps their place), and `map.destroy()` when you remove it. Options passed in win over the document's frontmatter; `documentToRendererOptions(doc.options)` shows what a document asks for. `new OrbitSVGRenderer(element, options)` plus `renderer.render(source)` is the same thing in two steps.
22
+
23
+ ## React
24
+
25
+ ```tsx
26
+ import { OrbitMindMap } from '@restless-dreams/orbit/react';
27
+
28
+ const markdown = `# Project
29
+
30
+ ## Research
31
+ ## Build`;
32
+
33
+ export const ProjectMap = () => {
34
+ return (
35
+ <div style={{ height: 480 }}>
36
+ <OrbitMindMap source={markdown} onNodeClick={(node) => console.log(node.text)} />
37
+ </div>
38
+ );
39
+ };
40
+ ```
41
+
42
+ `OrbitMindMap` fills its parent, so give the parent a size. Pass the map as `source` (markdown or YAML), as a parsed `document`, or as a node tree in `data`. Inline callbacks and inline `theme` / `palette` objects are fine: the map only redraws when their values change.
43
+
44
+ - **Drive it from code** with a ref: `ref.current.flyTo(id)`, `overview()`, `setViewMode(mode)`, `zoomBy(factor)`, `fit()`, and `ref.current.renderer` for everything else.
45
+ - **Control the focus** with `focusedId` and `onFocusChange`, like a controlled input.
46
+ - **Server rendering:** the component is marked `"use client"` for React Server Components. On the server and in the first render it draws a static SVG of the map, so the page shows it before JavaScript runs; the live map replaces it on mount (`staticFallback={false}` turns that off).
47
+
48
+ ## Web component
49
+
50
+ ```ts
51
+ import { defineOrbitMapElement } from '@restless-dreams/orbit';
52
+ defineOrbitMapElement();
53
+ ```
54
+
55
+ ```html
56
+ <orbit-map height="420" view-mode="focus">
57
+ # 🚀 Project
58
+
59
+ ## Research <!-- orbit status: active -->
60
+ ## Build
61
+ </orbit-map>
62
+ ```
63
+
64
+ Indent the content however you like, and `<!-- orbit … -->` comments and frontmatter work inside the element. The map renders in a shadow root, so the raw markdown is never shown. You can also put the source in a `<script type="text/orbit">` child or a `source` attribute.
65
+
66
+ - **Size:** the element is a block, 420 px tall by default. Use the `height` attribute, `height` in the frontmatter, or plain CSS (`orbit-map { height: 60vh }`, which wins over both).
67
+ - **Attributes:** `view-mode`, `theme`, `palette` (comma-separated), `max-depth`, `interactive`, `animation`, `editable`. Changing one updates the map in place.
68
+ - **Updates:** editing the element's content or its `source` property redraws the map, and the reader keeps their place. `element.flyTo(id)` and `element.overview()` move the map, `element.openMedia(id)` shows a node's picture or plays its video (see [Pictures and videos](#pictures-and-videos)), and `element.renderer` is the underlying `OrbitSVGRenderer`.
69
+ - **Events:** the map's events bubble out of the element (see Events below).
70
+ - **Errors:** a map that can't be drawn shows an error card with the line number and fires an `orbit-error` event (`event.detail.message`, `.line`, `.diagnostics`).
71
+
72
+ To avoid a flash of raw markdown before the script loads, hide undefined elements: `orbit-map:not(:defined) { visibility: hidden; }`.
73
+
74
+ ## Markdown pipelines (marked)
75
+
76
+ ```ts
77
+ import { Marked } from 'marked';
78
+ import { orbitMarked, hydrateOrbitBlocks } from '@restless-dreams/orbit/marked';
79
+
80
+ const markdown = new Marked(orbitMarked());
81
+ container.innerHTML = await markdown.parse(article); // orbit fences become map placeholders
82
+ hydrateOrbitBlocks(container); // turns them into live maps
83
+ ```
84
+
85
+ Fences can carry display options: ```` ```orbit height=480 theme=glass view=all ````. Each map fills its placeholder, which is 420 px tall unless the fence's `height` or the map's frontmatter says otherwise; CSS that gives the placeholder a height wins over both. Until a placeholder hydrates (or when JavaScript is off), it contains a nested list of the map, so the content stays readable and indexable. `renderOrbitMarkdown(markdown)` renders with a private instance in one call; `configureMarked(instance)` registers the extension on an existing instance (the global `marked` by default). Neither touches the global `marked` unless you pass it.
86
+
87
+ ## Format
88
+
89
+ Orbit reads two flavors: orbit-md, its markdown, and YAML. The dispatcher (`parseOrbit`) tells them apart. [The map format](format.md) has every rule, attribute, option and diagnostic; `npx @restless-dreams/orbit example` prints a map that uses all of them.
90
+
91
+ ### orbit-md, the markdown flavor (recommended for maps people write)
92
+
93
+ Every heading and every list item is a node; everything else belongs to the node above it. Headers (`#`–`######`, or lines underlined with `===` and `---`) nest by level, and every list under a heading is its children, as deep as you indent. Checkboxes set a status: `- [ ] Draft` is `todo`, `- [x] Ship` is `done`, `- [/] Build` is `active`. Hashtags at the end of a title are tags: `- Offline sync #frontend`.
94
+
95
+ A document can also be just a list. A single top item becomes the root; with several, the root is named after the frontmatter `title`, a one-line paragraph above the list, or "Untitled map". Several `#` headings share a root the same way:
96
+
97
+ ```markdown
98
+ Project plan
99
+ - Research
100
+ - Build
101
+ - Launch
102
+ ```
103
+
104
+ ```markdown
105
+ # 🚀 Project Launch
106
+
107
+ ## Research
108
+ - Market analysis
109
+ - User interviews
110
+
111
+ ## Build
112
+
113
+ ### Frontend
114
+ ### Backend
115
+
116
+ ## Launch
117
+ ```
118
+
119
+ Image, video, and embed:
120
+
121
+ ```markdown
122
+ ## Mountain Trip
123
+
124
+ ![Sunrise from the summit](photos/sunrise.jpg)
125
+
126
+ We left at 5am to catch the sunrise.
127
+ ```
128
+
129
+ Text under a heading or a list item is its note: paragraphs, quotes, code. The
130
+ list after it is still the children. Under a list item, indent the note, a
131
+ picture or an attribute block, as you would under a heading. To keep a bullet
132
+ list inside a note, quote it with `>`:
133
+
134
+ ```markdown
135
+ ## Build
136
+ Currently focused on the frontend. Starts when [Research](#research) is done.
137
+
138
+ - Frontend <!-- orbit status: active -->
139
+ Vite + React. Ships behind the beta flag.
140
+ - Backend
141
+ ![Backend diagram](diagrams/backend.png)
142
+ > - Billing
143
+ > - Search
144
+
145
+ ## Research
146
+ ```
147
+
148
+ A `#id` link, like `[Research](#research)` above, flies to that node of the map.
149
+
150
+ Per-node attributes go in HTML comments that start with `orbit`. Other comments are hidden, as markdown hides them:
151
+
152
+ ```markdown
153
+ ## Build
154
+ <!-- orbit
155
+ color: "#F5A3A8"
156
+ status: active
157
+ priority: 1
158
+ tags: [frontend, sprint-3]
159
+ -->
160
+ ```
161
+
162
+ Document-level frontmatter sets renderer options:
163
+
164
+ ```markdown
165
+ ---
166
+ view: focus
167
+ palette: ["#F8D978", "#F5A3A8", "#A8DDB5"]
168
+ baseUrl: /docs/assets/
169
+ ---
170
+ # Project
171
+ ```
172
+
173
+ ### YAML flavor (for app-generated maps)
174
+
175
+ ```yaml
176
+ title: My Project
177
+ view: focus
178
+ nodes:
179
+ - text: Project
180
+ icon: 🚀
181
+ children:
182
+ - text: Research
183
+ tags: [discovery]
184
+ children: [Market analysis, User interviews] # scalar shorthand
185
+ - text: Build
186
+ status: active
187
+ note: |
188
+ Currently focused on the frontend.
189
+ Targeting end-of-quarter ship.
190
+ ```
191
+
192
+ Plain strings in `children` are shorthand for `{ text: ... }` leaf nodes.
193
+
194
+ ### Document options
195
+
196
+ Frontmatter (markdown) or top-level keys (YAML): `title` (used as the SVG's
197
+ accessible label), `view`, `theme`, `showRings`, `interactive`, `animation`,
198
+ `speed`, `palette`, `height`, `maxDepth` (levels below the root to render;
199
+ `1` = root + direct children), `className`, `baseUrl`, `schemaVersion` (the
200
+ format version the document is written for, `1`). Keys Orbit doesn't know are
201
+ kept and written back, and saving keeps a file as it was written: only the
202
+ ideas an edit changed are written again (see
203
+ [What saving keeps](format.md#what-saving-keeps)).
204
+
205
+ ### Problems in a document
206
+
207
+ Parsing is forgiving. When something is wrong but recoverable (an unknown frontmatter key, a second `#` root, an attribute block that isn't valid YAML, an image without a URL), the parser works around it and records a diagnostic with a code, the line it's on and a message that says how to fix it (`Unknown frontmatter key "rings" (ignored). Did you mean "showRings"?`):
208
+
209
+ ```ts
210
+ const doc = parseOrbit(source);
211
+
212
+ for (const problem of doc.diagnostics ?? []) {
213
+ console.log(problem.line, problem.code, problem.message);
214
+ }
215
+ ```
216
+
217
+ Pass `{ strict: true }` to throw an `OrbitParseError` (with `code` and `line`) on the first problem instead. `orbit validate` lists every problem and fails until there are none. When a map rendered through `<orbit-map>` or `renderOrbitMarkdownBlocks` can't be drawn at all, it shows an error card and dispatches an `orbit-error` event; the other maps on the page still render.
218
+
219
+ ### Round-tripping
220
+
221
+ `serializeOrbitMarkdown` / `serializeOrbitYAML` produce documents that parse back to the same tree. The markdown reads the way people write maps:
222
+ - **Headings and lists:** the root and its children are headings, and everything below them is a nested list.
223
+ - **Titles:** statuses on list items are checkboxes (`[ ]`, `[/]`, `[x]`) and simple tags are hashtags; titles are escaped only where markdown would misread them.
224
+ - **Attributes:** short ones go on the node's line, `<!-- orbit color: "#E8A33D", priority: 2 -->`: up to three, or more while the comment stays under 80 characters (`<!-- orbit type: goal, target: 5, current: 2, unit: apples -->`). The rest go in a comment block below it.
225
+ - **Notes and media:** notes are plain paragraphs, quoted only when their lines would read as nodes, and pictures and videos are lines of their own.
226
+
227
+ A map written in that style comes back unchanged.
228
+
229
+ ```ts
230
+ import { parseOrbit, serializeOrbitMarkdown, serializeOrbitYAML } from '@restless-dreams/orbit';
231
+
232
+ const doc = parseOrbit(source);
233
+ const asYaml = serializeOrbitYAML(doc);
234
+ const backToMarkdown = serializeOrbitMarkdown(parseOrbit(asYaml));
235
+ ```
236
+
237
+ ## Action API
238
+
239
+ Wire your own behavior on click:
240
+
241
+ ```ts
242
+ const renderer = new OrbitSVGRenderer(host, {
243
+ onAction: ({ node, action }) => {
244
+ if (action === 'image') {
245
+ openMyLightbox(node.image!.src);
246
+ }
247
+
248
+ if (action === 'note') {
249
+ openMyDrawer(node.note!);
250
+ }
251
+
252
+ return false; // skip Orbit's own viewer and detail sheet
253
+ },
254
+ });
255
+ ```
256
+
257
+ The focused node shows a button for each kind of content it has, then your own [actions](extending.md#actions). They sit under it, or, where its planets crowd it (as they do on a phone), under the whole orbit, which is then fitted to leave them room: they never cover a planet. When `onAction` isn't provided (or doesn't return `false`), "View image" and "Play video" open the [media viewer](#pictures-and-videos), and the other buttons the built-in detail sheet: a panel along the right edge on wide screens and a bottom sheet on phones (swipe down to close), with the node's media, note, status and tags, and arrows to step through its siblings. YouTube and Vimeo links play through their embeddable players (`youtube-nocookie.com/embed/…`, `player.vimeo.com/video/…`). Links open in a new tab.
258
+
259
+ Notes are markdown and are rendered safely (no raw HTML). Opt-out flags: `showActionButtons: false`, `showDetailSheet: false` and `mediaViewer: false`. Custom note rendering: `noteRenderer: (note, ctx) => '<p>...</p>'`. The HTML it returns is used as is, so sanitize it if the note content is untrusted.
260
+
261
+ ## Pictures and videos
262
+
263
+ "View image" and "Play video" open the media viewer, over the whole window, even when the map sits in a small box. It follows the map's theme, light or dark.
264
+
265
+ - **Pictures** fit the window. The wheel, a pinch, a double click or tap, the `+`, `−` and `0` keys and the zoom buttons zoom around the point under the pointer, and a zoomed picture pans with a drag.
266
+ - **Videos** start at once: files in the browser's own player, YouTube and Vimeo in theirs.
267
+ - **Every picture and video of the map,** in reading order: the previous and next buttons, the arrow keys or a swipe step through them, and a counter says where you are.
268
+ - **Around them,** the node's title, the caption and the picture's description, with a link to the picture or to the video's page. Esc, a click around the media or a swipe down closes the viewer, and focus goes back where it was.
269
+
270
+ The detail sheet's picture opens the viewer too, and its video has a button that moves it there. `mediaViewer: false` keeps pictures and videos in the detail sheet, and `onAction` returning `false` (or the `action` event's `preventDefault()`) skips both, as for any button.
271
+
272
+ A page can drive the viewer itself, to show a picture big or play a video on a screen of its own, such as an audience's while someone presents:
273
+
274
+ ```ts
275
+ map.openMedia('summit'); // a node's picture, or its video
276
+ map.openMedia('trail', { kind: 'video', autoplay: false }); // its video, ready to play
277
+ map.openMedia('trail', { kind: 'video', startAt: 42 }); // its video, playing from 0:42
278
+ map.playMedia();
279
+ map.seekMedia(42);
280
+ map.pauseMedia();
281
+ map.unmuteMedia(); // from a press, for a video a browser started muted
282
+ map.closeMedia();
283
+ map.on('media-state', ({ node, playing, time }) => tellThePresenter(node.id, playing, time));
284
+ ```
285
+
286
+ `openMedia` works whatever `mediaViewer` and `showDetailSheet` say. On a map that isn't interactive, the viewer is the page's to drive: no buttons, no video controls, and no input of its own. [Pictures and videos](media.md) covers the keys, YouTube and Vimeo, sound and autoplay, and the events.
287
+
288
+ ## Extending
289
+
290
+ Give nodes a type, add your own buttons, name your own themes and status words. Pass them to one map, or register them once for every map, `<orbit-map>` elements and markdown pages included:
291
+
292
+ ```ts
293
+ import { createOrbitMap, registerTheme } from '@restless-dreams/orbit';
294
+
295
+ registerTheme('brand', { preset: 'night', tokens: { 'color-root': '#FFD166' } });
296
+
297
+ createOrbitMap(element, '# Launch\n## v1.0 beta <!-- orbit type: milestone, status: review -->', {
298
+ theme: 'brand',
299
+ nodeTypes: {
300
+ milestone: { label: 'Milestone', icon: '🏁', color: '#F59E0B', details: (node, element) => { element.textContent = 'Due Friday'; } },
301
+ },
302
+ actions: [{ id: 'assign', label: 'Assign', icon: '👤', run: (node) => openAssignDialog(node) }],
303
+ statuses: { review: 'active' },
304
+ });
305
+ ```
306
+
307
+ A node type sets its nodes' icon, color and theme tokens, can draw inside them (`render`), add a section to the detail sheet (`details`) and bring its own actions. `nodeTypeFor` gives nodes that name no type one to be drawn as, for display only: it's never written to the map. [Extending](extending.md) covers every option.
308
+
309
+ ## Editing on the map
310
+
311
+ Let people build a map on the map itself. With `editable`, a **+** in the orbit adds an idea, typed right inside its new planet. Enter adds it and opens the next one, so ideas go in one after another. A long press, a right click or F2 edits a node's text where it is, and a node held still lifts, to be dragged onto another node or to a new place in the orbit:
312
+
313
+ ```ts
314
+ const map = createOrbitMap(element, markdown, {
315
+ editable: true,
316
+ onEdit: () => save(map.toMarkdown()),
317
+ });
318
+ ```
319
+
320
+ With `editingKeys` too, the keyboard builds maps as in mind-map apps: Tab adds an idea under the node with keyboard focus, Shift+Enter beside it, and Delete removes it; while typing, Tab and Shift+Tab go a level down and up.
321
+
322
+ For a menu of your own, `onNodeContext` reports the long press or right click and where it happened, and `onNodeDrag` when the press moves on to drag the node, the moment to close the menu. `editNode(id)`, `editNewNode(parentId, index?)`, `addNode`, `updateNode`, `removeNode` and `moveNode` then change the map, animating each edit and firing `edit`. `toMarkdown()` writes the edited map as orbit-md. [Editing](editing.md) covers menus, dragging, keys, ids, focus, saving, React and `<orbit-map editable>`.
323
+
324
+ ## Interaction
325
+
326
+ A map has two views. **Focus** puts one node at the center (the sun) with its children around it (planets) and their children as small moons. The **overview** lays out the whole tree.
327
+
328
+ - **Moving around:** click or tap a planet (or a moon) to fly into it; click the sun, the back chip or a breadcrumb to go back up. The camera flies between views along a smooth zoom path, and nodes that both views share move into place.
329
+ - **Zooming:** the wheel, a trackpad pinch or a two-finger pinch zooms around the pointer. In focus mode, zooming into a planet enters it once it's big enough to read, so a crowded orbit can be zoomed into and read first; zooming out leaves the current node, and a small zoom out springs back. The overview zooms and pans freely, with momentum.
330
+ - **Spinning:** drag around the sun to turn the orbit. A hard flick keeps it spinning and slowing down like a fidget spinner; a touch catches it. `spin: { snap: 'planet' }` brings the nearest planet to the top as it stops, and `onSpinSettle` tells you which one.
331
+ - **Pages that scroll:** when the page around the map scrolls, the wheel scrolls the page and Ctrl (⌘ on a Mac) + wheel zooms the map; on touch screens, one finger scrolls the page except on the orbit itself. `zoom: { wheel: 'always' }` or `'modifier'` changes this.
332
+ - **Keyboard:** the map is one tab stop. In focus mode, `←`/`→` step through the planets (turning the orbit so the chosen one is on top), `↓` enters the orbit, `↑` returns to the sun, `Enter`/`Space` flies into a node, `Escape`/`Backspace` goes back. In the overview, the arrow keys follow the tree. `+`, `−` and `0` zoom and fit.
333
+ - **Assistive technology:** the list button shows the whole map as a nested list, which screen readers can also browse at any time, and moves are announced. When the reader asks for reduced motion, views cross-fade instead of flying and a flicked orbit doesn't coast.
334
+ - **Pictures and videos:** "View image" and "Play video" open them over the whole window, to zoom into a picture (the wheel, a pinch, a double click or tap) and to play a video; the arrows and swipes step through every picture and video of the map. See [Pictures and videos](#pictures-and-videos).
335
+ - **Editing:** on `editable` maps, the + in the orbit adds an idea and a long press, a right click or F2 edits a node's text. A long press, a right click, or the context menu key (or Shift+F10) also fires `node-context`, for a menu of your own. A node held still lifts: drag it onto another node to move it there, or along the orbit to a new place. With `editingKeys`, Tab, Shift+Enter and Delete add and remove ideas from the keyboard. See [Editing on the map](#editing-on-the-map).
336
+ - **Programmatic:** `renderer.flyTo(id)` (in focus mode), `renderer.overview()`, `renderer.focusNode(id)` (keeps the current view), `renderer.setViewMode(mode)`, `renderer.zoomBy(factor)`, `renderer.fit()`, `renderer.spin(degreesPerSecond)`, `renderer.getFocusPath()`, `renderer.getFocusedNode()`, `renderer.getViewMode()`, `renderer.getNodeColor(id)` for the color a node is drawn with, to match a list or legend beside the map, and `renderer.openMedia(id)`, `closeMedia()`, `playMedia()`, `pauseMedia()`, `seekMedia(seconds)` and `unmuteMedia()` for the media viewer.
337
+
338
+ ## Events
339
+
340
+ Listen on the renderer, or for DOM events that bubble from the map's container (and out of `<orbit-map>`):
341
+
342
+ ```ts
343
+ const stop = map.on('focus-change', ({ node, path }) => {});
344
+ element.addEventListener('orbit-focus-change', (event) => event.detail.node);
345
+ ```
346
+
347
+ | Event | DOM event | Detail |
348
+ | --- | --- | --- |
349
+ | `focus-change` | `orbit-focus-change` | `{ node, path }`: the focused node and the path from the root |
350
+ | `view-mode-change` | `orbit-view-mode-change` | `{ mode }`: `'focus'` or `'all'` |
351
+ | `node-click` | `orbit-node-click` | `{ node }`, before the map reacts |
352
+ | `action` | `orbit-action` | `{ node, action, sourceEvent }`; `preventDefault()` skips the built-in viewer, sheet or link |
353
+ | `spin-settle` | `orbit-spin-settle` | `{ planet }`: the planet on top when a spun orbit stops |
354
+ | `node-context` | `orbit-node-context` | `{ node, x, y }`: a long press, right click or context menu key on a node, and where; `preventDefault()` takes the press |
355
+ | `edit` | `orbit-edit` | `{ kind, node, tree }`: an edit, made on the map or with `addNode`, `updateNode`, `removeNode` or `moveNode` |
356
+ | `editor-open`, `editor-close` | `orbit-editor-open`, `orbit-editor-close` | `{ node, parent }`: the field for typing on the map opened or closed, on `node` or for a new idea under `parent` |
357
+ | `node-drag` | `orbit-node-drag` | `{ phase, node, parent, index }`: a node dragged somewhere else, as it leaves its place (`start`), when where it would go changes (`over`), and when it's let go (`end`) |
358
+ | `media-open`, `media-close` | `orbit-media-open`, `orbit-media-close` | `{ node, kind, index, total }`: the media viewer opened (or stepped) on a node's picture (`kind: 'image'`) or video, `index` of `total` in reading order, or closed |
359
+ | `media-state` | `orbit-media-state` | `{ node, playing, time, duration, ended, muted }`: the viewer's video started, paused, moved on, ended or was muted; while it plays, its time a few times a second |
360
+ | | `orbit-error` | `{ message, line, diagnostics }`, on elements whose map couldn't be drawn |
361
+
362
+ The `on…` options (`onFocusChange`, `onAction`, …) receive the same information.
363
+
364
+ ## Server rendering and export
365
+
366
+ `renderToSVGString` draws a map without a browser, for static sites, emails, docs and print. Colors are resolved to plain values and labels are SVG text, so the file looks the same anywhere:
367
+
368
+ ```ts
369
+ import { renderToSVGString } from '@restless-dreams/orbit';
370
+
371
+ const svg = renderToSVGString(markdown, { width: 1200, height: 800, theme: 'night', viewMode: 'all' });
372
+ ```
373
+
374
+ Options: `width` and `height` (positive, up to a million pixels), `viewMode`, `focus` (a node id), `theme`, `palette`, `maxDepth`, `showRings`, `title`, `baseUrl`, `responsive` (100 % wide and tall instead of pixels) and `idPrefix` (when several SVGs share a page). In the browser, `renderer.toSVGString()` exports what's on screen.
375
+
376
+ ## A map as a web page
377
+
378
+ `orbit export --html` makes one HTML file with Orbit, the map and its pictures inside. It opens in any browser, offline: explore the map with Orbit's own controls, or present it, one idea at a time in reading order with its note as a caption (the Present button or P; the arrows, Space or a swipe step, Escape stops). It's the way to share a map for review: send the file to someone, or to your own phone.
379
+
380
+ ```bash
381
+ npx @restless-dreams/orbit export --html map.md --out map.html --theme auto
382
+ ```
383
+
384
+ Pictures on your computer that the map shows (its nodes' pictures, videos' thumbnails and pictures in notes) go inside the page; pictures on the web stay links, and so do videos. `--title`, `--theme`, `--view`, `--focus` and `--language` work as for `render`; with `--theme auto` the page follows the reader's light or dark mode.
385
+
386
+ In code, `exportWebPage` returns the same page. It needs Orbit's web page script, `@restless-dreams/orbit/web-page.js`, as text, to put inside:
387
+
388
+ ```ts
389
+ import { readFileSync } from 'node:fs';
390
+ import { exportWebPage } from '@restless-dreams/orbit';
391
+
392
+ const script = readFileSync(new URL(import.meta.resolve('@restless-dreams/orbit/web-page.js')), 'utf8');
393
+ const html = exportWebPage(markdown, { script, theme: 'night', media: { 'photos/summit.jpg': 'data:image/jpeg;base64,…' } });
394
+ ```
395
+
396
+ Options: `title`, `theme`, `viewMode`, `focus`, `language`, `strings` and `media`, the pictures to put inside the page by the address the map gives them.
397
+
398
+ ## Map tools for AI
399
+
400
+ `createMapTools` gives a language model tools to read a map and change it: read it as an outline with each node's id, search it, add branches written as markdown, rename, mark, move, sort and remove nodes. `tools.for('anthropic')`, `'openai'`, `'openai-chat'` or `'gemini'` lists them in the shape that provider's API takes, `tools.definitions` as MCP lists tools, and `tools.run(name, input)` carries out each call. On a map on screen, each change animates and fires `edit`; on a document, it changes the document in place. A call a model gets wrong changes nothing, and its result says what to send instead. [Map tools for AI](map-tools.md) has the loop for each provider.
401
+
402
+ ## Notes in your own UI
403
+
404
+ `renderNoteMarkdown` renders a note the way the detail sheet does, for a page that shows notes itself, such as captions under a presented map. It builds DOM nodes rather than HTML text, so a note can't add markup, and its links and pictures go through the same URL guard as the map:
405
+
406
+ ```ts
407
+ import { renderNoteMarkdown } from '@restless-dreams/orbit';
408
+
409
+ caption.replaceChildren(renderNoteMarkdown(node.note ?? '', { baseUrl }));
410
+ ```
411
+
412
+ Options: `baseUrl`, where relative links and pictures resolve from, and `resolveLink(href)`, whether a `#id` link points at a node of the map (links to missing nodes show as plain text).
413
+
414
+ ## Renderer options
415
+
416
+ | Option | Default | |
417
+ | --- | --- | --- |
418
+ | `viewMode` | `'focus'` | `'focus'` or `'all'` (the overview). |
419
+ | `focus` | the root | The node in focus when the map first draws, by id: to open a map where the reader left it. |
420
+ | `theme` | `'paper'` | A theme name (see Theming) or `{ preset, tokens }`. |
421
+ | `palette` | theme colors | Node colors: the first for the root, the rest cycle through the branches. |
422
+ | `maxDepth` | all levels | Levels below the root to draw. |
423
+ | `showRings` | `true` | Dashed orbit rings. |
424
+ | `interactive` | `true` | `false` draws a still picture. |
425
+ | `showControls` | `true` | Breadcrumbs, view switch, list button and zoom buttons. |
426
+ | `showActionButtons` / `showDetailSheet` | `true` | Content buttons under the focused node / the built-in detail sheet. |
427
+ | `mediaViewer` | `true` | Pictures and videos open in the media viewer, over the whole window; `false` keeps them in the detail sheet. |
428
+ | `editable` | `false` | A + in the orbit to add ideas, text edited inside nodes (long press, right click or F2), and nodes dragged somewhere else. |
429
+ | `editingKeys` | `false` | Mind-map keys on editable maps: Tab adds an idea under a node, Shift+Enter beside it, and Delete removes it; while typing, Tab and Shift+Tab go a level down and up. |
430
+ | `longPressDelay` | `500` | Milliseconds a touch holds still on a node before it is a long press; apps can pass their platform's own. |
431
+ | `insets` | | `{ top, right, bottom, left }` in px: the parts of the map your page covers with its own controls, such as a floating toolbar. Views are fitted into what's left, and the map still draws under them. |
432
+ | `orbitAnimation` / `orbitSpeed` | `false` / `90` | Let the orbit drift slowly, one turn every `orbitSpeed` seconds; it pauses while you point at it, tab through it, zoom or pan. |
433
+ | `spin` | | `{ decay: 1.4, maxSpeed: 1440, snap: false, haptics: false }`: seconds for a flick to lose two thirds of its speed, fastest spin in degrees per second, snapping a planet to the top, and a light vibration as planets pass the top. |
434
+ | `zoom` | | `{ wheel: 'auto', minimum: 0.4, maximum: 6 }`: wheel behavior and the zoom range relative to the fitted view. |
435
+ | `transitionDuration` | `'auto'` | Flight length in milliseconds, scaled to the distance travelled when `'auto'`; `0` turns flights off. |
436
+ | `ariaLabel` | document title | The map's accessible name. |
437
+ | `language` | the page's | The language of the map's buttons, sheets and announcements: a tag like `'es'` ([Languages](languages.md)). By default, the closest `lang` attribute around the map. |
438
+ | `strings` | | Words to change on this map only: `{ zoomIn: 'Closer' }`. |
439
+ | `baseUrl` | | Resolves relative image, video and link URLs. |
440
+ | `onNodeClick`, `onFocusChange`, `onViewModeChange`, `onAction`, `onSpinSettle`, `onNodeContext`, `onEdit`, `onEditorOpen`, `onEditorClose`, `onMediaOpen`, `onMediaClose`, `onMediaState` | | Callbacks. |
441
+ | `noteRenderer` | | Renders note markdown to HTML yourself. |
442
+
443
+ ## CLI
444
+
445
+ ```bash
446
+ npx @restless-dreams/orbit validate my-map.md # every problem as my-map.md:line: severity: message [code]
447
+ npx @restless-dreams/orbit validate --json my-map.md # the same as JSON, for scripts and coding agents
448
+ npx @restless-dreams/orbit validate --tree my-map.md # the map as Orbit reads it, a node a line with its #id
449
+ npx @restless-dreams/orbit render my-map.md --out my-map.svg # draw a map to SVG (--theme, --view, --focus, --width, --height, --language)
450
+ npx @restless-dreams/orbit convert --to yaml my-map.md # markdown ↔ YAML (either direction)
451
+ npx @restless-dreams/orbit example > my-map.md # a map with the main features (--yaml for YAML)
452
+ npx @restless-dreams/orbit --version # the installed version
453
+ ```
454
+
455
+ Use `-` as the file to read from stdin, and `--help` after any command for its options. `validate` exits with 1 while the map has a problem (`--loose` accepts the ones the map works around, but never a map it can't read); every command exits with 2 for a mistake in the command itself. `render` links pictures on your computer from where the SVG goes, so it can move with them. The command itself is `orbit`: where the package is installed, scripts can call `orbit validate my-map.md` directly.
456
+
457
+ ## Languages
458
+
459
+ Maps speak the language of the page they're on: the buttons, the detail sheet, the media viewer and everything screen readers hear. English and Spanish are built in; `registerLanguage` adds others, and the `language` and `strings` options pick a language or change single words for one map.
460
+
461
+ ```ts
462
+ import { createOrbitMap, registerLanguage } from '@restless-dreams/orbit';
463
+
464
+ createOrbitMap(element, markdown, { language: 'es' }); // or <html lang="es">, or <orbit-map lang="es">
465
+ registerLanguage('fr', { zoomIn: 'Zoomer', overview: 'Vue d’ensemble' });
466
+ ```
467
+
468
+ [Languages](languages.md) lists every word, and how to translate them.
469
+
470
+ ## Theming
471
+
472
+ Pick a theme with the `theme` option, the `theme` attribute or `theme:` in the frontmatter:
473
+
474
+ ![The fourteen themes: paper, night, glass, glass night, cosmos, blueprint, ink, contrast, flat, flat night, material, material night, liquid and liquid night](https://raw.githubusercontent.com/restless-dreams/orbit/main/docs/media/themes.webp)
475
+
476
+ - `paper` (the default): pastel planets with soft shadows, on your page's own background.
477
+ - `night`: planets that glow on a deep navy sky.
478
+ - `glass` and `glass-night`: frosted, translucent planets over a soft backdrop of their own.
479
+ - `cosmos`: jewel-toned planets around a golden star, in a field of stars.
480
+ - `blueprint`: outlined planets, construction lines and monospaced labels on a gridded blue sheet.
481
+ - `ink`: black line art with serif lettering, on your page's own background.
482
+ - `contrast`: black outlines, solid colors and bold labels, for the most legible map.
483
+ - `flat` and `flat-night`: solid, saturated planets with no shading, shadows or glow.
484
+ - `material` and `material-night`: Material 3's tonal containers and surfaces. `materialTheme('#0B57D0')` builds the same look from any color, the way Material You colors an app from its wallpaper.
485
+ - `liquid` and `liquid-night`: liquid glass, clear planets that catch the light along their rims, over a vivid wallpaper of their own.
486
+ - `auto`: `paper` or `night`, following the reader's light or dark preference.
487
+
488
+ Labels keep at least 4.5:1 contrast with their node in every theme.
489
+
490
+ Every color, size and finish is a CSS custom property, so you can restyle a map from your own stylesheet ([the full list](theming.md)). Properties set by the page win over the theme:
491
+
492
+ ```css
493
+ orbit-map,
494
+ .my-map {
495
+ --orbit-sans-font: Inter, sans-serif; /* your font, in every sans-serif theme */
496
+ --orbit-surface: #fbf8f3; /* background behind the map */
497
+ --orbit-color-root: #f8d978; /* the root, then one color per branch */
498
+ --orbit-color-1: #f5a3a8;
499
+ --orbit-color-2: #a8ddb5;
500
+ --orbit-node-text: #243042;
501
+ --orbit-node-shadow-opacity: 0; /* planets without a shadow */
502
+ --orbit-ring-dash: none; /* solid orbits */
503
+ --orbit-sheet-background: #ffffff; /* the detail sheet and the media viewer */
504
+ --orbit-focus-ring: #2563eb;
505
+ }
506
+ ```
507
+
508
+ Names are measured in the font they're drawn with, so they fill their nodes without spilling out of them, in any font, and fit again when a web font arrives ([Fonts](theming.md#fonts)).
509
+
510
+ The full list is exported as `THEME_TOKENS`, and `themeTokens(name)` returns a theme's values (for example to style your own UI to match). A `theme` object sets values in code (`theme: { preset: 'night', tokens: { surface: '#0b1020', 'ring-dash': 'none' } }`), and `palette: ['#…', '#…']` replaces the node colors. Children share their branch's color in lighter and darker shades, and labels switch to light text on dark nodes on their own.
511
+
512
+ ## Browsers
513
+
514
+ Orbit is made for current browsers, and draws in older ones down to Chromium 99, which some phones' WebViews still run. A few effects need newer CSS, and older browsers get plainer versions instead of broken ones:
515
+
516
+ - without `color-mix()` (Chromium 111, Safari 16.2, Firefox 113), sibling ideas keep their branch's color instead of a shade of it, and rims, rings, shadows and halos take the theme's own colors;
517
+ - without the `translate` property (Chromium 104), overlays are centered through their transform;
518
+ - without the `dvh` unit (Chromium 108), the detail sheet sizes by `vh`.
519
+
520
+ ## Cleanup
521
+
522
+ If you mount via `renderOrbitElement` and later remove the host from the DOM, call `OrbitSVGRenderer.unmount(host)` to release RAF + ResizeObserver resources. The `<orbit-map>` element does this automatically.