@restless-dreams/orbit 0.0.0-stage → 0.2.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +168 -2
  3. package/dist/bin/orbit.js +11666 -0
  4. package/dist/bin/orbit.js.map +60 -0
  5. package/dist/chunk-fkdfgtx9.js +17295 -0
  6. package/dist/chunk-fkdfgtx9.js.map +75 -0
  7. package/dist/chunk-y748hzcr.js +82 -0
  8. package/dist/chunk-y748hzcr.js.map +10 -0
  9. package/dist/chunk-zze4tc9n.js +408 -0
  10. package/dist/chunk-zze4tc9n.js.map +10 -0
  11. package/dist/core/auto-render.d.ts +85 -0
  12. package/dist/core/branch-colors.d.ts +57 -0
  13. package/dist/core/browser.d.ts +23 -0
  14. package/dist/core/create.d.ts +23 -0
  15. package/dist/core/diagnostics.d.ts +40 -0
  16. package/dist/core/dispatch.d.ts +23 -0
  17. package/dist/core/document-options.d.ts +18 -0
  18. package/dist/core/document-source.d.ts +128 -0
  19. package/dist/core/editing.d.ts +66 -0
  20. package/dist/core/engine/animation-clock.d.ts +40 -0
  21. package/dist/core/engine/camera.d.ts +70 -0
  22. package/dist/core/engine/colors.d.ts +51 -0
  23. package/dist/core/engine/display.d.ts +125 -0
  24. package/dist/core/engine/export.d.ts +32 -0
  25. package/dist/core/engine/glide.d.ts +34 -0
  26. package/dist/core/engine/labels.d.ts +106 -0
  27. package/dist/core/engine/node-drag.d.ts +49 -0
  28. package/dist/core/engine/scene.d.ts +160 -0
  29. package/dist/core/engine/spin.d.ts +62 -0
  30. package/dist/core/engine/surface-pattern.d.ts +20 -0
  31. package/dist/core/engine/text-measure.d.ts +116 -0
  32. package/dist/core/engine/views.d.ts +361 -0
  33. package/dist/core/extensions.d.ts +155 -0
  34. package/dist/core/format-upgrades.d.ts +33 -0
  35. package/dist/core/index.d.ts +46 -0
  36. package/dist/core/input-controller.d.ts +116 -0
  37. package/dist/core/languages.d.ts +201 -0
  38. package/dist/core/layout.d.ts +61 -0
  39. package/dist/core/map-tools/definitions.d.ts +71 -0
  40. package/dist/core/map-tools/input.d.ts +62 -0
  41. package/dist/core/map-tools/reading.d.ts +40 -0
  42. package/dist/core/map-tools/tools.d.ts +51 -0
  43. package/dist/core/markdown-writing.d.ts +87 -0
  44. package/dist/core/material.d.ts +25 -0
  45. package/dist/core/media.d.ts +42 -0
  46. package/dist/core/node-attributes.d.ts +31 -0
  47. package/dist/core/node-links.d.ts +25 -0
  48. package/dist/core/other-apps/freemind.d.ts +8 -0
  49. package/dist/core/other-apps/icons.d.ts +16 -0
  50. package/dist/core/other-apps/ideas.d.ts +76 -0
  51. package/dist/core/other-apps/import.d.ts +28 -0
  52. package/dist/core/other-apps/inflate.d.ts +22 -0
  53. package/dist/core/other-apps/markup.d.ts +51 -0
  54. package/dist/core/other-apps/notes.d.ts +80 -0
  55. package/dist/core/other-apps/opml.d.ts +26 -0
  56. package/dist/core/other-apps/reading.d.ts +36 -0
  57. package/dist/core/other-apps/xmind.d.ts +14 -0
  58. package/dist/core/other-apps/zip.d.ts +37 -0
  59. package/dist/core/parser-markdown.d.ts +42 -0
  60. package/dist/core/parser-yaml.d.ts +8 -0
  61. package/dist/core/print-layout.d.ts +70 -0
  62. package/dist/core/render-to-svg.d.ts +57 -0
  63. package/dist/core/renderer/content-painter.d.ts +30 -0
  64. package/dist/core/renderer/dom.d.ts +15 -0
  65. package/dist/core/renderer/families.d.ts +35 -0
  66. package/dist/core/renderer/label-painter.d.ts +93 -0
  67. package/dist/core/renderer/node-painter.d.ts +113 -0
  68. package/dist/core/renderer/numbers.d.ts +2 -0
  69. package/dist/core/renderer/options.d.ts +12 -0
  70. package/dist/core/renderer/rings-and-links.d.ts +23 -0
  71. package/dist/core/renderer/scene-helpers.d.ts +33 -0
  72. package/dist/core/renderer/trees.d.ts +22 -0
  73. package/dist/core/serializer-markdown.d.ts +18 -0
  74. package/dist/core/serializer-yaml.d.ts +2 -0
  75. package/dist/core/source-patch.d.ts +24 -0
  76. package/dist/core/source.d.ts +27 -0
  77. package/dist/core/styles.d.ts +6 -0
  78. package/dist/core/suggest.d.ts +8 -0
  79. package/dist/core/svg-renderer.d.ts +626 -0
  80. package/dist/core/text-layout.d.ts +54 -0
  81. package/dist/core/themes.d.ts +62 -0
  82. package/dist/core/types.d.ts +471 -0
  83. package/dist/core/ui/actions.d.ts +75 -0
  84. package/dist/core/ui/controls.d.ts +61 -0
  85. package/dist/core/ui/detail-sheet.d.ts +88 -0
  86. package/dist/core/ui/frames.d.ts +11 -0
  87. package/dist/core/ui/icons.d.ts +14 -0
  88. package/dist/core/ui/inline-editor.d.ts +79 -0
  89. package/dist/core/ui/media-players.d.ts +65 -0
  90. package/dist/core/ui/media-viewer.d.ts +169 -0
  91. package/dist/core/ui/media-zoom.d.ts +51 -0
  92. package/dist/core/ui/note-markdown.d.ts +19 -0
  93. package/dist/core/ui/outline.d.ts +44 -0
  94. package/dist/core/url-guard.d.ts +40 -0
  95. package/dist/core/utils.d.ts +23 -0
  96. package/dist/core/web-page/page.d.ts +59 -0
  97. package/dist/core/web-page/runtime.d.ts +13 -0
  98. package/dist/core/web-page/start.d.ts +1 -0
  99. package/dist/core/yaml-mini.d.ts +54 -0
  100. package/dist/index.d.ts +1 -0
  101. package/dist/index.js +4357 -0
  102. package/dist/index.js.map +29 -0
  103. package/dist/marked/index.d.ts +31 -0
  104. package/dist/marked/index.js +110 -0
  105. package/dist/marked/index.js.map +10 -0
  106. package/dist/react/index.d.ts +55 -0
  107. package/dist/react/index.js +352 -0
  108. package/dist/react/index.js.map +1 -0
  109. package/dist/web-page.d.ts +2 -0
  110. package/dist/web-page.js +130 -0
  111. package/llms-full.txt +2785 -0
  112. package/llms.txt +36 -0
  113. package/package.json +118 -4
  114. package/skills/orbit/SKILL.md +140 -0
  115. package/skills/orbit/references/editing.md +195 -0
  116. package/skills/orbit/references/extending.md +247 -0
  117. package/skills/orbit/references/format.md +331 -0
  118. package/skills/orbit/references/guide.md +546 -0
  119. package/skills/orbit/references/import-export.md +115 -0
  120. package/skills/orbit/references/languages.md +160 -0
  121. package/skills/orbit/references/map-tools.md +188 -0
  122. package/skills/orbit/references/media.md +160 -0
  123. package/skills/orbit/references/recipes.md +236 -0
  124. package/skills/orbit/references/theming.md +199 -0
@@ -0,0 +1,115 @@
1
+ <!-- A copy of docs/import-export.md, made by scripts/build-llms.ts, with links to other files pointing at the repository. Edit the original. -->
2
+
3
+ # Maps from other apps
4
+
5
+ Orbit reads the maps people already have in other apps: OPML, which outliners and most mind map apps write, FreeMind and Freeplane's `.mm` files, and XMind's `.xmind` workbooks. They become ordinary Orbit maps, with their notes, links, folded branches and pictures. Orbit writes OPML too, so a map moves out as easily as it came in.
6
+
7
+ ## Bring a map in
8
+
9
+ `importMap` reads a file's bytes (OPML and `.mm` files work as text too) and tells the format from what the file holds, then from its name:
10
+
11
+ ```ts
12
+ import { importMap, serializeOrbitMarkdown } from '@restless-dreams/orbit';
13
+
14
+ const bytes = new Uint8Array(await file.arrayBuffer());
15
+ const { format, maps } = importMap(bytes, { name: file.name });
16
+
17
+ for (const map of maps) {
18
+ const markdown = serializeOrbitMarkdown(map.document);
19
+
20
+ for (const [path, data] of Object.entries(map.media)) {
21
+ // Save each picture at this path next to the map: `media/sunrise.png`.
22
+ }
23
+
24
+ for (const warning of map.warnings) {
25
+ console.warn(`${warning.message} [${warning.code}]`);
26
+ }
27
+ }
28
+ ```
29
+
30
+ - `maps` holds one map, or one for each sheet of an XMind workbook.
31
+ - Each map's `document` is what `parseOrbit` would give for the markdown `serializeOrbitMarkdown` writes for it, ids included: draw it, edit it or save it as you would any map.
32
+ - `media` holds the pictures inside the file, by the address the map gives them. Only XMind workbooks carry pictures; the others point at files on the computer they were made on, which can't come along.
33
+ - `warnings` say what the other app had that Orbit keeps another way, or couldn't keep (see [Warnings](#warnings)).
34
+ - Options: `name`, the file's name, which also names a map with several top ideas and no title; `format`, to skip telling the format (`opml`, `freemind` or `xmind`); and `untitled`, the name for a map with several top ideas and nothing to name it after (default "Untitled map").
35
+
36
+ `detectImportFormat(bytes, name)` tells the format without reading the map: `opml`, `freemind`, `xmind`, or `null` for anything else, Orbit's own markdown, YAML and zips included. `importOPML`, `importFreeMind` and `importXMind` read one format each and take the same options.
37
+
38
+ A file Orbit can't read at all throws an `OrbitImportError`, whose `code` says why:
39
+
40
+ | Code | Why |
41
+ | --- | --- |
42
+ | `unknown-format` | It's none of the formats Orbit reads. |
43
+ | `unreadable` | It's damaged, or not what its name says: the message says where. |
44
+ | `empty` | It holds no ideas. |
45
+ | `password-protected` | An XMind workbook with a password. Open it in XMind and save it without one first. |
46
+ | `too-large` | More than 100,000 ideas, or more than 500 levels deep. |
47
+
48
+ ## From the command line
49
+
50
+ ```bash
51
+ npx @restless-dreams/orbit convert --to md Garden.xmind --out garden.md # every sheet, its pictures in media/
52
+ npx @restless-dreams/orbit convert --to md outline.opml > outline.md
53
+ npx @restless-dreams/orbit convert --to opml my-map.md --out my-map.opml
54
+ ```
55
+
56
+ Warnings go to stderr. With `--out`, a workbook's other sheets go next to the first (`garden-2.md`, `garden-3.md`) and its pictures in `media/` beside them; without it, Orbit writes the first sheet and says what it left out.
57
+
58
+ ## What comes over
59
+
60
+ Everything a reader would miss comes over, as Orbit says it. Titles keep every word on one line, and an emoji at the start of a title becomes its icon, as in Orbit's own markdown. Notes in HTML become markdown: paragraphs, headings, lists, quotes, code, bold, italics, strikethrough, links and pictures. When markdown would show a note's words any other way (a `*` or `_` in it, or a line that starts like a list), the note stays plain text instead, exactly as written, without its formatting.
61
+
62
+ | | OPML | FreeMind and Freeplane | XMind |
63
+ | --- | --- | --- | --- |
64
+ | Note | `_note` (also `note`, and a feed's `description`) | the note and Freeplane's details, in HTML or plain text, and attributes as a list | the note, in HTML or plain text; XMind 8's comments, with their authors |
65
+ | Link | `type="link"`'s or an include's `url`, a feed's `htmlUrl` or `xmlUrl` | `LINK` | the topic's link |
66
+ | Links between ideas | `#id` | links to other nodes, and arrow links | links to other topics, and relationships |
67
+ | Status | `_status` (`checked`, `unchecked`, `indeterminate`), `_complete`, `complete` | Freeplane's progress icons and checkboxes | task markers |
68
+ | Priority | | priority icons (`full-1` to `full-9`) | priority markers |
69
+ | Icon | an emoji at the start of the text | built-in icons, as emoji, and Freeplane's emoji | markers and stickers, as emoji |
70
+ | Tags | `category` | | labels |
71
+ | Color | | the background color | the fill color a topic was given |
72
+ | Folded | | `FOLDED` | folded branches |
73
+ | Picture | | a picture on the web | pictures inside the workbook, and on the web |
74
+
75
+ An idea with a link of its own keeps it, and a second link, or a link between ideas with a label, becomes a line of its note: `→ [builds on](#chapter-2)`.
76
+
77
+ ## Warnings
78
+
79
+ What has no place in a tree of ideas keeps its content where it can, and each kind gets one warning that counts the places and names a few:
80
+
81
+ | Code | What changed |
82
+ | --- | --- |
83
+ | `floating-ideas-moved` | XMind's floating topics are ideas under the center. |
84
+ | `summaries-moved` | XMind's summaries are ideas beside the ideas they sum up. |
85
+ | `callouts-moved` | XMind's callouts are ideas under the topic they point at. |
86
+ | `boundaries-in-notes` | A titled boundary's title is in the note of the idea whose ideas it surrounds. |
87
+ | `links-in-notes` | A link beyond an idea's first, or a labeled link between ideas, is a line of its note. |
88
+ | `missing-link-targets` | A link to an idea that isn't in the map (another sheet's, or one deleted) was left out. |
89
+ | `icons-not-kept` | An icon with no emoji like it, or one past an idea's first, was left out. |
90
+ | `pictures-not-kept` | A picture that isn't in the file was left out. |
91
+ | `attachments-not-kept` | A file attached to an idea was left out; the idea keeps its name. |
92
+ | `local-links-not-kept` | A link to a file on the computer the map was made on was left out. |
93
+ | `encrypted-branches-not-read` | A branch FreeMind or Freeplane encrypted with a password couldn't be read; its ideas were left out. |
94
+ | `formatting-not-kept` | A note is plain text, without its formatting, because markdown would have shown its words differently. |
95
+
96
+ Styles (fonts, shapes, line colors) and positions are left out without a warning: Orbit draws every map its own way.
97
+
98
+ ## Write OPML
99
+
100
+ `exportOPML` writes a map, as markdown or YAML text or a document, as OPML 2.0:
101
+
102
+ ```ts
103
+ import { exportOPML } from '@restless-dreams/orbit';
104
+
105
+ const opml = exportOPML(markdown, { title: 'Launch plan' });
106
+ ```
107
+
108
+ - The root is the one top outline, and each node an outline with its icon and text in `text`.
109
+ - A note goes in `_note`, as its markdown or plain text.
110
+ - A link is `type="link"` with its `url`, a link between ideas included (`#research`).
111
+ - A status is `_status`: `checked`, `unchecked` and `indeterminate` for `done`, `todo` and `active`, which outliners show as checkboxes; any other word as it is. Done statuses add `_complete="true"`, which Workflowy reads.
112
+ - Tags are `category`, separated by commas.
113
+ - `title` names the outline in its head. Default: the map's `title`, or its root's text.
114
+
115
+ Two attributes only Orbit reads say what the text alone can't: `_noteFormat` when a note would read the other way (a markdown note with a line break, which OPML readers show as two lines, or a plain one that looks like markdown), and `_textFormat="plain"` when a title looks like HTML. With them, a map Orbit writes reads back the same: its text, icons, notes, statuses, links, tags and tree. Pictures, videos, colors, priorities and folds have no place in OPML; the map's own file, or a zip of it and its pictures, keeps them.
@@ -0,0 +1,160 @@
1
+ <!-- A copy of docs/languages.md, made by scripts/build-llms.ts, with links to other files pointing at the repository. Edit the original. -->
2
+
3
+ # Languages
4
+
5
+ Orbit speaks the language of the page it's on: its buttons, the detail sheet, the media viewer, the list of the map, its messages and everything screen readers hear about the map. English and Spanish are built in. You can add any other language, change single words, or pick a language for one map.
6
+
7
+ Only Orbit's own words change. The text of a map is the author's, and so are the status words written in it (`status: done` shows as "done"). Problems Orbit reports about a map's source (in the console, `orbit validate` and diagnostics) stay in English: they're for the map's author and their tools.
8
+
9
+ ## Which language a map speaks
10
+
11
+ A map speaks the language of the element it's drawn in: the closest `lang` attribute around it, through shadow roots, and then the page's own `<html lang>`. A page in Spanish gets its maps in Spanish with nothing to set:
12
+
13
+ ```html
14
+ <html lang="es">
15
+ …
16
+ <orbit-map># Viaje
17
+ ## Comida
18
+ ## Lugares</orbit-map>
19
+ ```
20
+
21
+ The `language` option picks a language for one map, whatever the page says:
22
+
23
+ ```ts
24
+ createOrbitMap(element, markdown, { language: 'es' });
25
+ ```
26
+
27
+ ```tsx
28
+ <OrbitMindMap source={markdown} language="es" />
29
+ ```
30
+
31
+ On `<orbit-map>`, set its `lang` attribute, as on any element: `<orbit-map lang="es">`.
32
+
33
+ Languages are [BCP 47 tags](https://www.rfc-editor.org/info/bcp47). A regional tag speaks its language with the region's numbers and plurals: `es-MX` and `es-ES` both speak Spanish, writing 24,000 and 24.000. A language Orbit doesn't have speaks English, and so does a map on a page that names none.
34
+
35
+ Changing the language later, with `setOptions({ language })`, the `language` prop or the element's `lang` attribute, writes the map's words again where they are.
36
+
37
+ ## Adding a language
38
+
39
+ `registerLanguage` adds a language for every map, by its tag. Words it leaves out are English, or, for a regional tag like `pt-BR`, its base language's (`pt`) when Orbit has it:
40
+
41
+ ```ts
42
+ import { registerLanguage } from '@restless-dreams/orbit';
43
+
44
+ registerLanguage('fr', {
45
+ focus: 'Focus',
46
+ overview: 'Vue d’ensemble',
47
+ zoomIn: 'Zoomer',
48
+ zoomOut: 'Dézoomer',
49
+ ideaCount: { one: '{count} idée', many: '{count} d’idées', other: '{count} idées' },
50
+ // …the rest of the words in the table below.
51
+ });
52
+ ```
53
+
54
+ It returns a function that takes the language away again. Registering a language Orbit already has changes its words for every map: `registerLanguage('es', { video: 'Vídeo' })`. `languageTags()` lists the languages maps can speak.
55
+
56
+ ### Placeholders, plurals and lists
57
+
58
+ - **Placeholders:** words in braces (`{name}`, `{count}`) are filled in for you. Keep them in a translation, in whatever order the language needs.
59
+ - **Plurals:** text with a number is an object of plural forms, one for each [plural category](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/select) the language uses (`zero`, `one`, `two`, `few`, `many`) and `other`, which is required and covers any form left out.
60
+ - **Numbers and lists:** numbers are written the way the language writes them, and lists like "has note, image, and link" are joined the language's way ("tiene nota, imagen y enlace").
61
+
62
+ ## Changing single words
63
+
64
+ The `strings` option changes words on one map, over its language's:
65
+
66
+ ```ts
67
+ createOrbitMap(element, markdown, { strings: { zoomIn: 'Closer', zoomOut: 'Further' } });
68
+ ```
69
+
70
+ A word Orbit doesn't have, or one of the wrong kind, is skipped with a warning in the console.
71
+
72
+ ## The root Orbit makes up
73
+
74
+ A map that's just a list, or has several top headings, and has no title gets a root Orbit makes up, named "Untitled map" in the map's language. When you parse maps yourself, `parseOrbit(source, { untitled: 'Mapa sin título' })` gives it that name. Its id is `untitled-map` in every language, so links and saved places keep working, and saving the map writes it as the list it was.
75
+
76
+ ## Without a browser
77
+
78
+ `renderToSVGString(markdown, { language: 'es' })` writes the drawing's few words in a language: an untitled idea's name, the empty map's message and the map's title when it has none. On the command line: `orbit render map.md --language es`.
79
+
80
+ ## Every word
81
+
82
+ | Key | English | Spanish |
83
+ | --- | --- | --- |
84
+ | `mindMap` | Mind map | Mapa mental |
85
+ | `mindMapRole` | mind map | mapa mental |
86
+ | `untitled` | Untitled | Sin título |
87
+ | `untitledMap` | Untitled map | Mapa sin título |
88
+ | `emptyMap` | This map is empty | Este mapa está vacío |
89
+ | `emptyMapHint` | Start it with a # heading or a bullet list | Empiézalo con un encabezado # o una lista con viñetas |
90
+ | `mapNotDrawn` | This map couldn’t be drawn | No se pudo dibujar este mapa |
91
+ | `mapNotDrawnReason` | The map could not be drawn. | No se pudo dibujar el mapa. |
92
+ | `problemOnLine` | Line {line}: {message} | Línea {line}: {message} |
93
+ | `mapPath` | Map path | Ruta del mapa |
94
+ | `back` | Back | Atrás |
95
+ | `backTo` | Back to {name} | Volver a {name} |
96
+ | `backToParent` | Back to the parent node | Volver a la idea superior |
97
+ | `view` | View | Vista |
98
+ | `focus` | Focus | Enfoque |
99
+ | `overview` | Overview | Vista general |
100
+ | `showOutline` | Show the map as a list | Mostrar el mapa como lista |
101
+ | `zoom` | Zoom | Zoom |
102
+ | `zoomIn` | Zoom in | Acercar |
103
+ | `zoomOut` | Zoom out | Alejar |
104
+ | `fitMap` | Fit the map | Ajustar el mapa |
105
+ | `scrollToZoom` | Use {key} + scroll to zoom the map | Usa {key} + desplazamiento para acercar o alejar el mapa |
106
+ | `outline` | Map outline | Esquema del mapa |
107
+ | `overviewAnnouncement` | Overview of the whole map | Vista general de todo el mapa |
108
+ | `ideaCount` | one: {count} item; other: {count} items | one: {count} idea; many: {count} ideas; other: {count} ideas |
109
+ | `added` | Added {name} | Se agregó {name} |
110
+ | `removed` | Removed {name} | Se eliminó {name} |
111
+ | `moved` | Moved {name} to {parent} | Se movió {name} a {parent} |
112
+ | `movedToTop` | Moved {name} to the top | Se movió {name} al nivel superior |
113
+ | `anIdea` | a node | una idea |
114
+ | `status` | status {status} | estado {status} |
115
+ | `statusLabel` | Status: {status} | Estado: {status} |
116
+ | `has` | has {list} | tiene {list} |
117
+ | `hasDetails` | details | detalles |
118
+ | `hasNote` | note | nota |
119
+ | `hasImage` | image | imagen |
120
+ | `hasVideo` | video | video |
121
+ | `hasEmbed` | embed | contenido insertado |
122
+ | `hasLink` | link | enlace |
123
+ | `textOf` | Text of {name} | Texto de {name} |
124
+ | `newIdeaFor` | New idea for {name} | Idea nueva en {name} |
125
+ | `newIdea` | New idea | Idea nueva |
126
+ | `addIdeaTo` | Add an idea to {name} | Agregar una idea a {name} |
127
+ | `moveHere` | Move {name} here | Mover {name} aquí |
128
+ | `thisIdea` | this node | esta idea |
129
+ | `theIdea` | the idea | la idea |
130
+ | `ideaActions` | {name} actions | Acciones de {name} |
131
+ | `ideaActionsUntitled` | Node actions | Acciones de la idea |
132
+ | `showDetails` | Show details | Ver detalles |
133
+ | `viewImage` | View image | Ver imagen |
134
+ | `viewImageOf` | View image: {description} | Ver imagen: {description} |
135
+ | `playVideo` | Play video | Reproducir video |
136
+ | `openEmbed` | Open embed | Abrir contenido insertado |
137
+ | `readNote` | Read note | Leer nota |
138
+ | `openLink` | Open link | Abrir enlace |
139
+ | `goTo` | Go to {name} | Ir a {name} |
140
+ | `seeIdea` | See {name} | Ver {name} |
141
+ | `details` | Details | Detalles |
142
+ | `close` | Close | Cerrar |
143
+ | `previous` | Previous | Anterior |
144
+ | `next` | Next | Siguiente |
145
+ | `present` | Present | Presentar |
146
+ | `priority` | Priority {priority} | Prioridad {priority} |
147
+ | `expand` | Expand | Ampliar |
148
+ | `expandVideo` | Expand the video | Ampliar el video |
149
+ | `picture` | Picture | Imagen |
150
+ | `video` | Video | Video |
151
+ | `position` | {index} of {total} | {index} de {total} |
152
+ | `fitPicture` | Fit the picture | Ajustar la imagen |
153
+ | `pictureUnavailable` | This picture can’t be shown. | No se puede mostrar esta imagen. |
154
+ | `videoUnavailable` | This video can’t be played. | No se puede reproducir este video. |
155
+ | `videoUnavailableHere` | This video can’t be played here. | Este video no se puede reproducir aquí. |
156
+ | `openPictureInNewTab` | Open the picture in a new tab | Abrir la imagen en una pestaña nueva |
157
+ | `openVideoInNewTab` | Open the video in a new tab | Abrir el video en una pestaña nueva |
158
+ | `watchOn` | Watch on {provider} | Ver en {provider} |
159
+
160
+ `OrbitStrings` in the [API reference](https://github.com/restless-dreams/orbit/blob/main/docs/api/README.md) says where each one shows.
@@ -0,0 +1,188 @@
1
+ <!-- A copy of docs/map-tools.md, made by scripts/build-llms.ts, with links to other files pointing at the repository. Edit the original. -->
2
+
3
+ # Map tools for AI
4
+
5
+ Map tools let a language model read a map and change it: add ideas and whole branches, rename them, mark them done, move, sort and remove them. They come in the shape each provider's API takes, so an app can offer an assistant with whichever model its people choose. They work on a map on screen, where every change animates into place, or on a document on a server. A call a model gets wrong changes nothing, and its result says what to send instead.
6
+
7
+ ## Add an assistant to a map
8
+
9
+ `createMapTools(map)` gives the tools for one map. `tools.for(provider)` lists them for the model, and `tools.run(name, input)` carries out each call it makes. With Anthropic's API:
10
+
11
+ ```js
12
+ import Anthropic from '@anthropic-ai/sdk';
13
+ import { createOrbitMap, createMapTools } from '@restless-dreams/orbit';
14
+
15
+ const map = createOrbitMap(element, markdown, { onEdit: () => save(map.toMarkdown()) });
16
+ const tools = createMapTools(map);
17
+ const client = new Anthropic({ apiKey });
18
+
19
+ const ask = async (request) => {
20
+ const messages = [{ role: 'user', content: request }];
21
+
22
+ while (true) {
23
+ const response = await client.messages.create({ model, max_tokens: 4096, system: tools.instructions, tools: tools.for('anthropic'), messages });
24
+ messages.push({ role: 'assistant', content: response.content });
25
+ const calls = response.content.filter((block) => block.type === 'tool_use');
26
+
27
+ if (calls.length === 0) {
28
+ return response.content.find((block) => block.type === 'text')?.text;
29
+ }
30
+
31
+ messages.push({
32
+ role: 'user',
33
+ content: calls.map((call) => {
34
+ const result = tools.run(call.name, call.input);
35
+ return { type: 'tool_result', tool_use_id: call.id, content: result.text, is_error: !result.ok };
36
+ }),
37
+ });
38
+ }
39
+ };
40
+
41
+ await ask('Add three day trips from Kyoto, each with a one-sentence note');
42
+ ```
43
+
44
+ The map changes as the model works: each new idea flies out of its parent, and `edit` fires for each change, so the map saves as it does when people edit it. Keep API keys off public pages: call the model from your server, or let each person bring their own key.
45
+
46
+ ## Other providers
47
+
48
+ **OpenAI's Responses API:**
49
+
50
+ ```js
51
+ const input = [{ role: 'user', content: request }];
52
+
53
+ while (true) {
54
+ const response = await openai.responses.create({ model, instructions: tools.instructions, tools: tools.for('openai'), input });
55
+ const calls = response.output.filter((item) => item.type === 'function_call');
56
+
57
+ if (calls.length === 0) {
58
+ return response.output_text;
59
+ }
60
+
61
+ input.push(...response.output);
62
+
63
+ for (const call of calls) {
64
+ input.push({ type: 'function_call_output', call_id: call.call_id, output: tools.run(call.name, call.arguments).text });
65
+ }
66
+ }
67
+ ```
68
+
69
+ `run` takes the arguments as OpenAI sends them, as JSON text. The tools are written without strict mode, which would make the model send every field of every call.
70
+
71
+ **Chat Completions,** which most other providers and local servers (OpenRouter, Ollama and many more) offer as well as OpenAI:
72
+
73
+ ```js
74
+ const messages = [{ role: 'system', content: tools.instructions }, { role: 'user', content: request }];
75
+
76
+ while (true) {
77
+ const response = await client.chat.completions.create({ model, messages, tools: tools.for('openai-chat') });
78
+ const message = response.choices[0].message;
79
+ messages.push(message);
80
+
81
+ if (!message.tool_calls?.length) {
82
+ return message.content;
83
+ }
84
+
85
+ for (const call of message.tool_calls) {
86
+ messages.push({ role: 'tool', tool_call_id: call.id, content: tools.run(call.function.name, call.function.arguments).text });
87
+ }
88
+ }
89
+ ```
90
+
91
+ **Gemini,** with `@google/genai`:
92
+
93
+ ```js
94
+ const contents = [{ role: 'user', parts: [{ text: request }] }];
95
+
96
+ while (true) {
97
+ const response = await ai.models.generateContent({ model, contents, config: { systemInstruction: tools.instructions, tools: tools.for('gemini') } });
98
+ const calls = response.functionCalls ?? [];
99
+
100
+ if (calls.length === 0) {
101
+ return response.text;
102
+ }
103
+
104
+ contents.push(response.candidates[0].content);
105
+ contents.push({
106
+ role: 'user',
107
+ parts: calls.map((call) => {
108
+ const result = tools.run(call.name, call.args);
109
+ return { functionResponse: { name: call.name, response: result.ok ? { output: result.text } : { error: result.text } } };
110
+ }),
111
+ });
112
+ }
113
+ ```
114
+
115
+ **MCP:** `tools.definitions` are tools as MCP lists them (`name`, `description`, `inputSchema` and `annotations`). A server returns them for `tools/list`, and answers `tools/call` with `{ content: [{ type: 'text', text: result.text }], isError: !result.ok }`.
116
+
117
+ For anything else, such as the Vercel AI SDK or LangChain, build the tools from `tools.definitions`: each has a name, a description, and its input as JSON Schema.
118
+
119
+ ## The tools
120
+
121
+ | Tool | What it does | Takes |
122
+ | --- | --- | --- |
123
+ | `read_map` | The map as an outline: each node's title, id and attributes, and the start of its note. A big map is cut short, whole levels first, with what's left out counted. | `node_id` (to read one branch), `depth` |
124
+ | `find_nodes` | Searches titles, notes and tags, in any case and with or without accents, and statuses by what they mean: `done` finds `shipped` too. | `query`, `status`, `tag` |
125
+ | `read_node` | Everything about one node: its whole note, its picture, video or page, where it is, its children, and the nodes that link to it. | `node_id` |
126
+ | `add_nodes` | Adds nodes, or whole branches, written as a markdown list. | `parent_id`, `outline`, `index` |
127
+ | `update_node` | Changes a title or attributes, and `clear` removes attributes. | `node_id`, `text`, `note`, `status`, `priority`, `tags`, `icon`, `color`, `link`, `image`, `video`, `embed`, `collapsed`, `clear` |
128
+ | `move_node` | Moves a branch under another node, or to another place among its siblings. | `node_id`, `parent_id`, `index` |
129
+ | `reorder_children` | Puts all the children of a node in a new order at once: to sort them, or to group them. | `parent_id`, `order` |
130
+ | `remove_node` | Removes a branch. Never the root. | `node_id` |
131
+ | `show_node` | Flies the map to a node. Only on a map on screen. | `node_id` |
132
+
133
+ New ideas come as orbit-md, the way models write any outline: each list item is a node, the items indented under it are its children, at any depth, and indented text that isn't an item is its note. Checkboxes, an emoji first, hashtags at the end and `<!-- orbit … -->` comments set statuses, icons, tags and other attributes, as in any map:
134
+
135
+ ```json
136
+ {
137
+ "parent_id": "marketing",
138
+ "outline": "- Social media\n - [ ] Instagram #social\n - TikTok\n- Newsletter\n A weekly email with what shipped."
139
+ }
140
+ ```
141
+
142
+ ## What a call returns
143
+
144
+ `run` returns `{ ok, text, changed }`:
145
+
146
+ - **`text`** is for the model: send it back as the tool's result. It names nodes with their ids (`"Instagram" #instagram`), gives the new id when a new title changes one, and adds notes worth knowing, such as an id the model wrote as a title, a field the tool doesn't take, or two ideas with the same title side by side.
147
+ - **`ok`** is false when the call couldn't be carried out. Nothing changed, and `text` says why and what to send instead:
148
+
149
+ ```text
150
+ No node has the id "lanch". Did you mean "Launch" #launch? read_map and find_nodes give every node's id.
151
+
152
+ "Build" #build can't go under "Frontend" #frontend, which is inside it. Move "Frontend" #frontend out of it first, or choose another parent. Nothing changed.
153
+
154
+ outline doesn't read cleanly. Fix this, and send it again:
155
+ - Line 2: Unknown attribute "colour". Did you mean "color"?
156
+ Lines count from the outline's first line. Nothing changed.
157
+ ```
158
+
159
+ - **`changed`** says whether the map changed.
160
+
161
+ `run` never throws over a call a model gets wrong. It forgives what models often send: input as JSON text, `null` for a field left out, a number as text, tags as words separated by commas, an id with its `#`, a title only one node has instead of its id, and a field under the name models often give it (`title` for `text`, `parent` for `parent_id`). An id from before a rename still finds its node, and the id of a removed node says it was removed. A misspelled field, or a value the map can't use, changes nothing and says what it takes.
162
+
163
+ ## On screen, or on a server
164
+
165
+ - **A map on screen:** `createMapTools(map)` takes what `createOrbitMap` returns, the `renderer` of an `<orbit-map>` element, or the `renderer` of `OrbitMindMap`'s ref. Each change goes through `addNode`, `updateNode`, `moveNode` and `removeNode` (see [Editing](editing.md)): it animates, Orbit announces it, and `edit` fires. `read_map` says which node the reader is looking at, and `show_node` takes them to one.
166
+ - **A document:** `createMapTools(markdown)` or `createMapTools(parseOrbit(markdown))`, on a server or anywhere without a map on screen. The tools change the document in place, and `serializeOrbitMarkdown(tools.document)` saves it, the way it was written apart from what changed.
167
+
168
+ ```js
169
+ import { writeFile } from 'node:fs/promises';
170
+ import { createMapTools, serializeOrbitMarkdown } from '@restless-dreams/orbit';
171
+
172
+ const tools = createMapTools(markdown);
173
+ tools.run('add_nodes', { parent_id: 'research', outline: '- Interviews\n- Survey' });
174
+ await writeFile('map.md', serializeOrbitMarkdown(tools.document));
175
+ ```
176
+
177
+ Options:
178
+
179
+ - **`readOnly: true`** offers only the tools that read (and `show_node`, on screen): for an assistant that answers questions about a map.
180
+ - **`readLimit`**: the most nodes `read_map` shows at once. Default 250.
181
+
182
+ ## Good to know
183
+
184
+ - **Instructions.** `tools.instructions` says how to use the tools, and asks for short titles with the detail in notes, ideas in the map's language, and nodes removed only when asked: put it in the system prompt. To save the model a first call, put `tools.run('read_map').text` in the first message too.
185
+ - **Undo.** Keep `map.toMarkdown()` from before a request: `map.render(saved)` puts the map back, animating to it.
186
+ - **Asking first.** Each definition's `annotations` say whether the tool only reads (`readOnlyHint`) and whether it can overwrite or remove something (`destructiveHint`). An app can run the tools that read at once and ask before the others, as MCP clients do.
187
+ - **Schemas every provider reads.** The inputs use plain JSON Schema, without references or unions, and new branches come as markdown rather than nested objects, which some providers' schemas can't describe.
188
+ - **Newer maps.** A map written for a newer version of the format can be read but not changed, so nothing this version doesn't understand is lost.
@@ -0,0 +1,160 @@
1
+ <!-- A copy of docs/media.md, made by scripts/build-llms.ts, with links to other files pointing at the repository. Edit the original. -->
2
+
3
+ # Pictures and videos
4
+
5
+ A node's picture shows inside its planet, and a video shows its thumbnail with a play badge ([Media](format.md#media) has how to write them). Under the node in focus, "View image" and "Play video" open them in the media viewer, over the whole window.
6
+
7
+ - [The media viewer](#the-media-viewer)
8
+ - [Keys](#keys)
9
+ - [Choosing what the buttons do](#choosing-what-the-buttons-do)
10
+ - [From code](#from-code)
11
+ - [Events](#events)
12
+ - [YouTube and Vimeo](#youtube-and-vimeo)
13
+ - [Sound and autoplay](#sound-and-autoplay)
14
+ - [Maps that aren't interactive](#maps-that-arent-interactive)
15
+ - [Looks](#looks)
16
+
17
+ ## The media viewer
18
+
19
+ The viewer covers the whole window, even when the map sits in a small box on a page, and follows the map's theme, light or dark.
20
+
21
+ - **Pictures** fit the window, never past twice their own size. The wheel, a pinch, a double click or tap, the `+`, `−` and `0` keys and the zoom buttons zoom around the point under the pointer or between the fingers, and a zoomed picture pans with a drag. Zooming goes to four times the fitted size, or until the picture shows at twice its own size if that's further.
22
+ - **Videos** start at once: files (`.mp4`, `.webm`, `.mov`) in the browser's own player, YouTube and Vimeo in theirs. The player is as big as the window allows, at the video's shape once it's known (16:9 until then, upright for YouTube Shorts). Stepping away from a video stops it.
23
+ - **Every picture and video of the map,** in reading order (the order the outline lists the map, a node's picture before its video): the previous and next buttons, the arrow keys and, on touch screens, a swipe step through them, starting from the one opened. A counter says where you are ("2 of 5"), and screen readers hear it.
24
+ - **Around them,** the node's title, the caption, and the picture's description (its alt text, which is also the picture's name for screen readers), with a link to open the picture on its own, or the video on its site.
25
+ - **Closing:** Esc, the close button, a click around the media or, on touch screens, a swipe down while the picture isn't zoomed. Focus goes back where it was, and the map's own keys, which rest while the viewer is open, work again.
26
+
27
+ The detail sheet shows a node's picture and video too. Its picture opens the viewer, over the sheet, and its video has an Expand button that moves it there (a video file goes on from where it was); closing the viewer goes back to the sheet.
28
+
29
+ ## Keys
30
+
31
+ | Key | Does |
32
+ | --- | --- |
33
+ | `←` `→` | The previous or next picture or video; on a zoomed picture, pan it |
34
+ | `↑` `↓` | Pan a zoomed picture |
35
+ | `Home` `End` | The first or last picture or video |
36
+ | `+` `−` `0` | Zoom in, zoom out, fit the picture to the window |
37
+ | `Space` | Play or pause the video |
38
+ | `Escape` | Close the viewer |
39
+
40
+ On a video's own controls, the arrow keys and the space bar are the player's. Inside YouTube's and Vimeo's players every key is theirs, Escape included: Tab out of the player, or click around it, to close the viewer.
41
+
42
+ ## Choosing what the buttons do
43
+
44
+ The buttons ask your code first, as every button does: `onAction`, then the `action` event. What happens next:
45
+
46
+ | Your code | "View image" and "Play video" |
47
+ | --- | --- |
48
+ | Nothing | Open the media viewer |
49
+ | `mediaViewer: false` | Open the detail sheet at the picture or video, as before the viewer |
50
+ | `mediaViewer: false` and `showDetailSheet: false` | Nothing: Orbit opens neither |
51
+ | `showDetailSheet: false` | Open the media viewer: it turns off only the sheet, for notes, embeds and details |
52
+ | `onAction` returns `false`, or `preventDefault()` on the `action` event | Nothing: the press is yours |
53
+
54
+ The detail sheet's picture and Expand button are presses of "View image" and "Play video" too: your code sees them first, and when it takes one the sheet closes, so what you open is in front. With `mediaViewer: false`, the sheet's picture is only a picture.
55
+
56
+ ```js
57
+ createOrbitMap(element, markdown, {
58
+ onAction: ({ node, action }) => {
59
+ if (action === 'image') {
60
+ openMyLightbox(node.image.src);
61
+ return false;
62
+ }
63
+
64
+ return undefined;
65
+ },
66
+ });
67
+ ```
68
+
69
+ ## From code
70
+
71
+ A page can open the viewer itself, to show a picture big or play a video on a screen of its own:
72
+
73
+ ```js
74
+ map.openMedia('summit'); // a node's picture, or its video if it has no picture
75
+ map.openMedia('trail', { kind: 'video', autoplay: false }); // its video, ready to play
76
+ map.openMedia('trail', { kind: 'video', startAt: 42 }); // its video, playing from 0:42
77
+ map.playMedia();
78
+ map.seekMedia(42);
79
+ map.pauseMedia();
80
+ map.unmuteMedia(); // from a press: the sound of a video a browser started muted
81
+ map.closeMedia();
82
+ ```
83
+
84
+ | Method | Does |
85
+ | --- | --- |
86
+ | `openMedia(nodeId, { kind, autoplay, startAt })` | Shows the node's picture (`kind: 'image'`, the default when it has one) or video (`kind: 'video'`), opening the viewer if it's closed. A video starts playing unless `autoplay` is `false`, from its start or `startAt` seconds in (YouTube and Vimeo start at the whole second). Returns `false` when the map has no such node, or the node no such picture or video. |
87
+ | `closeMedia()` | Closes the viewer. |
88
+ | `playMedia()`, `pauseMedia()` | Plays or pauses the video it shows. Returns `false` when it shows no video it can drive. |
89
+ | `seekMedia(seconds)` | Moves the video to `seconds` from its start. Returns `false` when it shows no video it can drive. |
90
+ | `unmuteMedia()` | Turns on the sound of the video it shows. Browsers allow it only from a press (see [Sound and autoplay](#sound-and-autoplay)). Returns `false` when it shows no video it can drive. |
91
+
92
+ `openMedia` opens the viewer whatever `mediaViewer`, `showDetailSheet` and `interactive` say: those choose what the buttons do. Files, YouTube and Vimeo can be driven; a video on any other site plays in a frame, with its own controls only. `<orbit-map>` has the same six methods.
93
+
94
+ This is what an audience's screen does while someone presents from a phone: it opens what the presenter opens, where the presenter's video is, and follows the presenter's player, and tells the phone how the video is going.
95
+
96
+ ```js
97
+ const map = createOrbitMap(element, markdown, { interactive: false, showControls: false });
98
+
99
+ presenter.addEventListener('message', ({ data }) => {
100
+ if (data.type === 'media') {
101
+ map.openMedia(data.id, { kind: data.kind, autoplay: data.playing, startAt: data.seconds });
102
+ } else if (data.type === 'play') {
103
+ map.playMedia();
104
+ } else if (data.type === 'pause') {
105
+ map.pauseMedia();
106
+ } else if (data.type === 'seek') {
107
+ map.seekMedia(data.seconds);
108
+ } else if (data.type === 'close') {
109
+ map.closeMedia();
110
+ }
111
+ });
112
+
113
+ map.on('media-state', ({ node, playing, time }) => {
114
+ presenter.postMessage({ type: 'state', id: node.id, playing, time });
115
+ });
116
+ ```
117
+
118
+ ## Events
119
+
120
+ | Event | DOM event | Detail |
121
+ | --- | --- | --- |
122
+ | `media-open` | `orbit-media-open` | `{ node, kind, index, total }`: the viewer opened, or stepped, on a node's picture (`kind: 'image'`) or video (`'video'`), `index` of `total` in reading order, from 0 |
123
+ | `media-close` | `orbit-media-close` | `{ node, kind, index, total }`: the viewer closed, showing that |
124
+ | `media-state` | `orbit-media-state` | `{ node, playing, time, duration, ended, muted }`: the video started, paused, moved to another time, ended or was muted, at once; while it plays, its time a few times a second. `time` and `duration` are in seconds; `duration` is `null` until it's known |
125
+
126
+ `onMediaOpen`, `onMediaClose` and `onMediaState` receive the same, and so do the React props of the same names. The DOM events bubble from the map's container and out of `<orbit-map>`.
127
+
128
+ ## YouTube and Vimeo
129
+
130
+ YouTube videos play in YouTube's privacy-enhanced player (`youtube-nocookie.com`), and Vimeo videos in Vimeo's with `dnt=1`, which tracks no sessions. Links to watch pages, `youtu.be`, Shorts, Vimeo channels, groups, showcases and unlisted videos all work; Orbit frames their players, never their pages, which refuse to be framed.
131
+
132
+ - **The referrer:** the players' frames send the page's origin, never its address (`referrerpolicy="strict-origin-when-cross-origin"`). YouTube's player won't play without knowing the site it's on (its "Error 153"), and a page that sends no referrer (`Referrer-Policy: no-referrer`) still gives its origin to these frames. A page opened from a file has no origin to give, so YouTube's videos don't play there.
133
+ - **Driving them:** `playMedia`, `pauseMedia` and `seekMedia` reach the players through the messages their own JavaScript APIs send (YouTube's `enablejsapi=1`, with the page's `origin`; Vimeo's player.js messages), and `media-state` follows what the players say. Nothing is loaded from either site but the player itself.
134
+ - **Other sites:** a video on any other site plays in a frame, as the detail sheet shows an embed, with only its own controls: `playMedia` and its kin return `false` for it, and it reports no `media-state`.
135
+
136
+ ## Sound and autoplay
137
+
138
+ Browsers let a video start with sound only once the reader has interacted with the page. A press of "Play video", the arrows or a swipe counts, so those videos play with sound.
139
+
140
+ A video opened from code on a page nobody has touched yet, such as an audience's screen, may not be allowed to. A video file then starts muted, and its `media-state` says `muted: true`. YouTube's and Vimeo's players decide for themselves: they start muted, or wait for a press. Screens that should play sound from the start can allow it: a kiosk Chrome with `--autoplay-policy=no-user-gesture-required`, or an Android `WebView` with `setMediaPlaybackRequiresUserGesture(false)`.
141
+
142
+ Elsewhere, offer a button for the sound while `media-state` says the video is muted, or isn't playing when it should. Its press counts as interacting with the page, so `unmuteMedia()` and `playMedia()` from its click play the video with sound, YouTube's and Vimeo's included. The viewer is a modal `<dialog>` in the top layer, and a modal dialog makes the rest of the page inert, popovers included: show the button in a `<dialog>` of its own with `showModal()`, after the viewer opens, so it's on top and can be pressed.
143
+
144
+ ```js
145
+ soundButton.addEventListener('click', () => {
146
+ map.unmuteMedia();
147
+ map.playMedia();
148
+ soundDialog.close();
149
+ });
150
+ ```
151
+
152
+ ## Maps that aren't interactive
153
+
154
+ On a map with `interactive: false`, a still picture, the viewer is the page's to drive. It shows the picture or video with its title and caption, but no buttons, no counter and no controls on videos, and it takes no input of its own: Esc and clicks don't close it, and keys and presses reach the page, so a presentation's own keys keep working. Open, close and play it with the methods above.
155
+
156
+ ## Looks
157
+
158
+ The viewer takes its colors from the detail sheet's custom properties: `--orbit-sheet-background` behind the media (blurred by `--orbit-sheet-blur` where it's see-through, as in the glass themes), `--orbit-sheet-text` and `--orbit-sheet-muted-text` for the title, caption and counter, and `--orbit-sheet-shadow` under a video. Its buttons are the map's controls (`--orbit-control-*`, `--orbit-focus-ring`). See [Theming](theming.md#custom-properties).
159
+
160
+ With reduced motion (`prefers-reduced-motion: reduce`), zooms and steps happen at once instead of gliding and fading.