@peter.prib/renderengine 0.4.0 → 0.7.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 (78) hide show
  1. package/README.md +4 -0
  2. package/REFERENCE.md +267 -17
  3. package/dist/renderengine-workspace.umd.js +5289 -2895
  4. package/dist/renderengine.umd.js +4243 -2120
  5. package/examples/archimate.html +8 -28
  6. package/examples/data-model.html +45 -556
  7. package/package.json +70 -70
  8. package/src/archimateArchiFormat.js +11 -3
  9. package/src/archimateExchangeFormat.js +17 -9
  10. package/src/charting/ArchimateDiagramEditor.js +1 -1
  11. package/src/charting/Axis.js +20 -0
  12. package/src/charting/DiagramEditor.js +35 -3
  13. package/src/charting/Dialog.js +195 -0
  14. package/src/charting/DuplicatesDialog.js +108 -0
  15. package/src/charting/FormDialog.js +28 -97
  16. package/src/charting/InteractionManager.js +3 -0
  17. package/src/charting/Legend.js +45 -2
  18. package/src/charting/MergeCompareDialog.js +81 -0
  19. package/src/charting/PopupMenu.js +12 -3
  20. package/src/charting/SettingsDialog.js +6 -2
  21. package/src/charting/SettingsManager.js +14 -1
  22. package/src/charting/archimate/ArchimateCanvasEditor.js +190 -15
  23. package/src/charting/archimate/ArchimatePalette.js +1 -1
  24. package/src/charting/charts/AreaChart.js +5 -2
  25. package/src/charting/charts/BarChart.js +5 -3
  26. package/src/charting/charts/BarChart3DOrbit.js +5 -7
  27. package/src/charting/charts/BubbleChart.js +5 -7
  28. package/src/charting/charts/BubbleChart3D.js +5 -7
  29. package/src/charting/charts/CandlestickChart.js +3 -1
  30. package/src/charting/charts/DensityChart.js +4 -6
  31. package/src/charting/charts/GroupedBarChart.js +2 -2
  32. package/src/charting/charts/LineChart.js +5 -2
  33. package/src/charting/charts/ParallelCoordinatesChart.js +2 -2
  34. package/src/charting/charts/PipeChart.js +4 -6
  35. package/src/charting/charts/RadarChart.js +5 -6
  36. package/src/charting/charts/ScatterChart.js +5 -7
  37. package/src/charting/charts/StackedBarChart.js +4 -2
  38. package/src/charting/charts/StackedBarChart3DOrbit.js +5 -7
  39. package/src/charting/charts/StreamGraphChart.js +5 -2
  40. package/src/charting/charts/WaterfallChart.js +2 -1
  41. package/src/charting/content/ContentSource.js +129 -0
  42. package/src/charting/content/ContentSources.js +77 -0
  43. package/src/charting/datamodel/DataModel.js +116 -0
  44. package/src/charting/diagrams/ArchimateModel.js +38 -3
  45. package/src/charting/diagrams/ArchimateModelChecker.js +13 -4
  46. package/src/charting/diagrams/MergeElementsCommand.js +124 -101
  47. package/src/charting/diagrams/SvgZoom.js +20 -1
  48. package/src/charting/diagrams/duplicates.js +95 -13
  49. package/src/charting/duplicateSearchForm.js +58 -0
  50. package/src/charting/modelTree/ModelTree.js +239 -202
  51. package/src/charting/modelTree/ModelTreePanel.js +955 -919
  52. package/src/charting/modelTree/TreeBuilder.js +93 -45
  53. package/src/charting/modelTree/TreeViewMode.js +9 -6
  54. package/src/charting/modelTree/viewFolders.js +15 -6
  55. package/src/charting/requirements/AzureDevOpsApi.js +55 -0
  56. package/src/charting/requirements/GitLabApi.js +43 -0
  57. package/src/charting/requirements/JiraApi.js +69 -0
  58. package/src/charting/requirements/TrackerApi.js +142 -0
  59. package/src/charting/requirements/TrackerExport.js +3 -2
  60. package/src/charting/requirements/index.js +5 -0
  61. package/src/charting/requirements/trackerApis.js +21 -0
  62. package/src/icons/IconSet.js +86 -0
  63. package/src/icons/index.js +59 -0
  64. package/src/index.js +67 -10
  65. package/src/workspace/dom.js +56 -42
  66. package/src/workspace/index.js +1 -1
  67. package/src/workspace/nav/NavEntry.js +42 -0
  68. package/src/workspace/nav/NavGroup.js +43 -0
  69. package/src/workspace/nav/NavItem.js +53 -0
  70. package/src/workspace/nav/NavMenu.js +143 -0
  71. package/src/workspace/nav/NavSection.js +17 -0
  72. package/src/workspace/nav/index.js +5 -0
  73. package/src/workspace/parts/ArchimateCanvasPart.js +8 -3
  74. package/src/workspace/parts/DataModelPart.js +91 -0
  75. package/src/workspace/parts/EditorPart.js +8 -0
  76. package/src/workspace/parts/ModelHosted.js +1 -0
  77. package/src/workspace/parts/ModelsPart.js +765 -622
  78. package/src/workspace/styles.js +28 -1
package/README.md CHANGED
@@ -29,6 +29,10 @@ open the pages directly:
29
29
  - `examples/diagram-editor.html`: the diagram editor
30
30
  - `examples/data-model.html`: the data modeller
31
31
 
32
+ The models they open come from [modelengine](https://gitlab.com/peter.prib/modelengine), kept apart from the tools: with
33
+ it checked out beside renderengine, the pages open on its models (File ▸ Open from ▸ modelengine lists them by domain);
34
+ without it they start empty.
35
+
32
36
  All work in current Chrome, Edge, Firefox and Safari. Save writes back to the file you opened in Chrome and Edge;
33
37
  other browsers download the file instead.
34
38
 
package/REFERENCE.md CHANGED
@@ -812,6 +812,22 @@ types — see `ARCHITECTURE.md`):
812
812
  is not there. Model ▸ **Check model…** lists the findings in the `ModelCheckDialog` the diagram editors' check uses;
813
813
  choosing one shows it (the entity's level, the service domain seen through, the mapping view).
814
814
 
815
+ **Likely duplicate entities.** **Find likely duplicates…** (Model ▸, Tools ▸ and the model's tab; the part's
816
+ `modelMenuItems()`) asks for a level (the one in front first) and how to compare, with the form the ArchiMate
817
+ modeller uses (`duplicateSearchFields`): names, definitions or attributes (their names together), exactly or by their
818
+ words, and the noise to leave out. The groups found are listed as the ArchiMate modeller lists them
819
+ (`DuplicatesDialog`, below), and merging shows the entities side by side first (`model.entityComparison(level, ids)`:
820
+ name, definition, each attribute, relationships, what each is mapped to; `MergeCompareDialog`): choose the entity
821
+ kept and, when their definitions differ, which (or all joined); entities alike merge without asking. The kept one takes the
822
+ attributes it lacks (by name, any case), their relationships (one left joining it to itself, or doubling one there,
823
+ goes) and their mapping pairs; the others go, perspectives and contexts forgetting them, one undo step
824
+ (`model.duplicateEntities(level, options)`, `model.mergeEntitiesCommand(level, keepId, otherIds, { description })`).
825
+
826
+ ```js
827
+ model.duplicateEntities('logical', { by: 'attributes' }); // [{ code: 'possible-duplicate', level, ids, message }]
828
+ part.runModelEdit(model.mergeEntitiesCommand('logical', 'customer', ['client']));
829
+ ```
830
+
815
831
  **Checked as DMBOK2 reviews a model.** The check also finds, at the logical and physical levels: an entity with
816
832
  attributes and no primary key (`no-primary-key`); a foreign key whose `references` (an entity, or `entity.attribute`,
817
833
  by id or name) is not there (`reference-missing`) or has no relationship with its entity either way
@@ -949,8 +965,9 @@ types — see `ARCHITECTURE.md`):
949
965
  service domains.
950
966
  `model.serviceDomainsCommand(specs, source)` brings them in as one undo step: a new one is added; one already
951
967
  there (by id, else by name) keeps its control record, qualifiers and the rest, taking its area and domain from
952
- the landscape, and its asset and pattern where it has none. `examples/bian-landscape-sample.xml` is a small
953
- excerpt in that shape to try it with (not BIAN's landscape, which is BIAN's to publish).
968
+ the landscape, and its asset and pattern where it has none. modelengine's banking domain has a small excerpt in
969
+ that shape to try it with (`test/fixtures/bian-landscape-sample.xml` here; not BIAN's landscape, which is BIAN's to
970
+ publish).
954
971
 
955
972
  ```javascript
956
973
  const specs = new BianLandscapeReader().read(BianLandscapeReader.parse(xml));
@@ -1045,7 +1062,8 @@ types — see `ARCHITECTURE.md`):
1045
1062
  and Redo; Tools ▸ Compare with and Merge from (the other open models and the bases; a merge is one undo step); Model ▸ New / Edit / Delete perspective and context (those dialogs), Perspective and Context selects (when
1046
1063
  it has some; a perspective brings its context, and the context's scope shows beside them), Level (Conceptual,
1047
1064
  Logical, Physical, and Mapping), Notation, and Export ▸ PNG / SVG. A box or note dragged stays on the level it
1048
- was dragged on (the mapping view's in its own layout). `examples/data-model.html` (`npm run datamodel`) uses every
1065
+ was dragged on (the mapping view's in its own layout). modelengine's enterprise ontology, which
1066
+ `examples/data-model.html` (`npm run datamodel`) opens when modelengine is checked out beside renderengine, uses every
1049
1067
  feature above. A data model can also be registered like a chart:
1050
1068
  `engine.register(id, 'dataModel', { entities, relationships, level })` then `engine.refreshChart(id)` (each
1051
1069
  diagram catalog entry names its data options, `args`).
@@ -1077,6 +1095,13 @@ but these recur across most 2D chart types:
1077
1095
  }
1078
1096
  ```
1079
1097
 
1098
+ - **Legend placement** (`Legend.beside`, every chart with a legend): to the right of the plot when there is
1099
+ room in the drawing; otherwise, since the plot usually fills it, inside the plot's top-right corner on a
1100
+ light backing. A chart of points (scatter, bubble) marks its entries with circles, the others with squares.
1101
+ - **Category labels** (line, area, bar, stacked bar, candlestick, waterfall, streamgraph): as many as fit.
1102
+ When they would overprint, every n-th is drawn, n from the longest label and the plot width
1103
+ (`Axis.categoryLabelStep(labels, width, { fontSize })`).
1104
+
1080
1105
  Data-resolution options (used with `register()`/`refreshChart()`, see below):
1081
1106
 
1082
1107
  ```javascript
@@ -1112,6 +1137,8 @@ engine.setGlobalData(data) // set a global object referenceable by
1112
1137
  engine.resolvePath(path, mapper?) // e.g. resolvePath('metrics.sales.0.value')
1113
1138
  engine.register(id, type, options) // remember a {type, options} pair for a container id
1114
1139
  engine.refreshChart(id) // re-invoke it — resolves options.data or dataPath+dataMapper fresh each time
1140
+ engine.refreshAll() // every registered chart
1141
+ engine.setData(data) // setGlobalData(data), then refreshAll(): a live page's poll
1115
1142
  engine.niceTicks(maxVal, count=4) // readable axis tick values
1116
1143
  engine.formatValue(v) // 1234567 -> "1.23M"
1117
1144
  engine.bindTooltip(element, text)
@@ -1120,7 +1147,24 @@ engine.adjustColor(hex, amount) // lighten/darken, used internally for 3
1120
1147
  ```
1121
1148
 
1122
1149
  `register`/`refreshChart` is the registration mechanism `addDataPoint`/`addGraphNode` (next section)
1123
- build on.
1150
+ build on. **One engine keeps a page of charts:** each registered id is drawn in the element with that id, by
1151
+ the engine itself when it is its own container, otherwise by an engine of its own for that element, made with
1152
+ this engine's options and kept while the element is the same, so a zoom survives the next refresh. A chart
1153
+ whose `dataPath` resolves to nothing yet is not drawn.
1154
+
1155
+ ```javascript
1156
+ const charts = new RenderEngine(null, { rendererType: 'svg', zoom: { wheelModifier: 'ctrl' } });
1157
+ charts.register('cpu-gauge', 'gauge-dial', { dataPath: 'cpuPercent' });
1158
+ charts.register('perf-chart', 'line', {
1159
+ dataPath: 'history',
1160
+ dataMapper: (h) => [{ name: 'CPU', values: h.map(x => ({ label: x.t, value: x.cpu })) }],
1161
+ });
1162
+ setInterval(async () => charts.setData(await (await fetch('/api/metrics')).json()), 2000);
1163
+ ```
1164
+
1165
+ **Zoom.** An SVG chart zooms with the mouse wheel. The engine option `zoom: { wheelModifier: 'ctrl' }`
1166
+ (or `'alt'`, `'shift'`; `ctrl` also takes ⌘) zooms only with that key held, so a plain wheel scrolls a page of
1167
+ charts; `zoom: false` turns it off.
1124
1168
 
1125
1169
  ## Interactivity
1126
1170
 
@@ -1359,6 +1403,24 @@ do, and a folder of one type in the Layers tree too (`typeGlyph(typeName, { size
1359
1403
  `archimate/typeGlyph.js`, the one drawing both use; a 3.2 type under ArchiMate 4 is drawn as 3.2 draws it).
1360
1404
  `TreeBuilder`'s `iconFor(node)` gives a node a picture in place of its `icon` text.
1361
1405
 
1406
+ **Tree rows and long folders.** A tree row is one compact line: a label too long for the panel ends in an ellipsis,
1407
+ with the whole of it as the row's tooltip. A folder shows its first `leafLimit` leaves (8 in the model tree,
1408
+ `ModelTree.LEAF_LIMIT`, set per tree by `new ModelTree(model, { leafLimit })` or `setOptions({ leafLimit })`), then
1409
+ a "▸ N more" row that shows the rest; the folders inside it always show. A folder asked for more keeps showing all
1410
+ when the tree is redrawn (`TreeBuilder`'s `shownAll`, a Set of node ids the caller keeps, as it keeps `openState`).
1411
+ `TreeBuilder`'s `filter(node)` says which leaves to show: a folder shows when a leaf under it does, and opens while
1412
+ filtering (without changing what `openState` remembers); one with nothing kept inside is left out, whatever its own
1413
+ name.
1414
+
1415
+ ```js
1416
+ const tree = new TreeBuilder(document, {
1417
+ container, rootNodes, getNodeChildren: (node) => node.kids ?? [],
1418
+ leafLimit: 8, shownAll: new Set(),
1419
+ filter: (node) => node.label.toLowerCase().includes('order'), // null: every node
1420
+ });
1421
+ tree.render();
1422
+ ```
1423
+
1362
1424
  **Nesting.** In the ArchiMate canvas editor, dropping a box wholly inside an element's box nests it there (Archi's
1363
1425
  nested elements); so does dropping an element with its centre on another element's box when a relationship a
1364
1426
  nesting stands for is allowed between them (`nestsOnDrop`), and the parent then grows to show it whole, with its
@@ -1441,9 +1503,15 @@ there (`editor.addImage(dataURL, point)`, one undo step, selected; the base edit
1441
1503
  relationships) under the palette, for an element chosen on the canvas or in the tree. The tree's own filters (its
1442
1504
  "Filters…" dialog: text, types, specializations, properties) dim what they leave out on the canvas too:
1443
1505
  `modelTree.matchingElementIds()` (null with no filter on) and `modelTree.onFilterChange(fn)` feed
1444
- `editor.setElementFilter`. Under the search box, **In:** chooses what the search covers, the whole model or only
1506
+ `editor.setElementFilter`. The search text filters from 3 characters on (`ModelTree.MIN_FILTER_CHARS`; per tree,
1507
+ `minFilterChars` or `setOptions({ minFilterChars })`); shorter, nothing is filtered (`modelTree.filterText` is the
1508
+ text in effect, `getFilterText()` the text typed). While it filters, the box is marked (`data-filtering`, a blue
1509
+ border) and a line under it says what was found ("Filtering: 3 elements, 1 view found"); with fewer characters it
1510
+ says how many more to type. It finds views, templates and folders by name as well as
1511
+ elements, and leaves out the folders with nothing found in them (`modelTree.textMatches(name, documentation)`; type,
1512
+ specialization and property filters are the elements' only). Under the search box, **In:** chooses what the search covers, the whole model or only
1445
1513
  the diagram in front (`modelTree.setSearchScope('model' | 'view')`, `ModelTree.SEARCH_SCOPES`), and
1446
- **Descriptions** looks for the text in elements' and relationships' documentation too
1514
+ **Descriptions** looks for the text in elements', relationships' and views' documentation too
1447
1515
  (`modelTree.setSearchInDescriptions(true)`). The Filters… dialog opens showing the filters on now
1448
1516
  (`getTypeFilter()`, `getSpecializationFilter()`, `getPropertyFilter()`). Its properties start unticked (no property
1449
1517
  filter; `OptInCheckboxTreeSelector`: a value is ticked to keep what has it), and a property with more than 10
@@ -1470,7 +1538,11 @@ leaves it alone). Any `Edge` can say so (`editable`, default true) and add to it
1470
1538
 
1471
1539
  **Selecting several shapes.** In the base editor (so the template editor and the ArchiMate canvas alike): drag a
1472
1540
  box on empty canvas to select the shapes wholly inside it (Ctrl or Cmd adds them to the selection;
1473
- `editor.templatesInside(rect)`, `editor.selectTemplates(ids, { extend })`); Shift- or Ctrl-click adds one;
1541
+ `editor.templatesInside(rect)`, `editor.selectTemplates(ids, { extend })`); Shift- or Ctrl-click adds one, and so
1542
+ does a right-click with Shift or Ctrl, before the menu opens for the whole selection; a plain right-click on a shape
1543
+ outside the selection selects it alone (`editor.selectForMenu(id, { extend })`). The last shape chosen has the
1544
+ handles; every other selected shape is marked by a dashed, tinted outline set off its border
1545
+ (`DiagramEditor.SELECTION_OUTLINE`: `pad`, `stroke`, `strokeWidth`, `dash`, `fill`);
1474
1546
  **Ctrl+A** selects every shape that is drawn (`selectableIds()`). Whatever is selected acts together, each as one
1475
1547
  undo step: dragging any of them moves them all (with their groups and what they hold); the arrow keys nudge
1476
1548
  them a grid step, one pixel without a grid, ten times as far with Shift (`nudgeSelection(dx, dy)`); **Delete**
@@ -1634,6 +1706,28 @@ whose schema changed) gives them afresh with `models.setBases(specs)`; what is o
1634
1706
  (`models.viewBase(i)`) opens a base to look at: a tab of its own copy, named "… (view)", that can be laid out and
1635
1707
  moved about but is never kept (`part.keepOff`).
1636
1708
 
1709
+ **Content: models kept apart from the tools.** Models, data models and user stories by domain live apart from the
1710
+ modellers (modelengine, or any collection laid out like it): a `manifest.json` listing items by domain, each a file in
1711
+ a format a modeller already reads. A `ContentSource` reads one, fetched from its address (`url`: a folder holding
1712
+ `manifest.json`, or the manifest itself) or given whole (`bundle`: `{ manifest, files }`, what modelengine's
1713
+ `dist/modelengine.js` puts on `globalThis.ModelEngineContent` for a page opened off disk; `ContentSource.fromPage()`).
1714
+ Nothing in the tools lists what a collection has: `source.items({ formats })` offers the items a modeller's file
1715
+ formats read, each matched by its file's whole ending (`ContentSource.formatFor(item, formats)`: the one format
1716
+ accepting all after the name's first dot, so `.requirements.json` is not any `.json`), and `source.open(item,
1717
+ formats)` opens one through that format. A `ModelsPart` offers its sources (`ContentSources`: the page's bundle, its
1718
+ spec's `content: [{ label?, url }]`, the host's `contentSources` service, and those added with **Add a source…**,
1719
+ kept in this browser) under File ▸ **Open from** ▸ *source* ▸ *domain* ▸ *item* (`models.contentItems()`); a source
1720
+ not read says why there. An item opens in a tab of its own (`models.openContent(source, item)`) remembering where it
1721
+ came from (`part.contentOrigin`), so its tab menu and Tools offer **Update from *source*…**: the item read again and
1722
+ its changes reviewed as a merge, one undo step, keeping what was changed here (`models.updateFromContent(part)`).
1723
+ `openContent: 'first'` (or an item's id) in the spec opens that item on a first visit, in the empty model's place.
1724
+
1725
+ ```js
1726
+ const source = new ContentSource({ url: 'https://example.org/modelengine/' });
1727
+ const [item] = await source.items({ formats: [new DataModelFileFormat()] }); // what the data modeller reads
1728
+ const { model } = await source.open(item, [new DataModelFileFormat()]);
1729
+ ```
1730
+
1637
1731
  **Kept in this browser, and backed up.** With `keep: true` in its spec, a `ModelsPart` keeps every model it holds
1638
1732
  in this browser, so work outlives the page without saving a file: a moment after each edit (`ModelsPart.KEEP_DELAY`)
1639
1733
  and at once when imported, each in its editor's own file format (`part.keepEntry()`: the file's format, else the
@@ -1840,20 +1934,56 @@ template editor (the testbed) and both ArchiMate editors have them. The **View**
1840
1934
  (`editor.viewMenuItems()`) has **Zoom to fit** (`editor.zoomToFit()`: everything drawn in sight, in the
1841
1935
  drawing's own units; `ViewFit` computes it, as a viewBox for the graph engine or a scale and pan for the
1842
1936
  template editor), **Reset zoom**, **Highlight…** (below), and, when the editor has a `model`, **Check model…**
1843
- (`editor.showModelCheck()`). The checks are the model's own (`model.checker(options)`): a `ViewSetChecker` for
1937
+ (`editor.showModelCheck()`; `showFindings(findings, { title, empty })` shows any findings in its dialog). The checks are the model's own (`model.checker(options)`): a `ViewSetChecker` for
1844
1938
  any view set (empty views; an empty template is not one), and for an ArchiMate model an `ArchimateModelChecker`
1845
1939
  that adds relationships the specification does not allow, likely duplicates (elements of one type whose names
1846
- have the same words, ignoring case, punctuation, word order and filler words such as "of" and "the":
1940
+ have the same words, ignoring case, punctuation, word order, plurals ("Customers" is "Customer", `singular(word)`)
1941
+ and filler words such as "of" and "the":
1847
1942
  `nameKey(name)` and `duplicateGroups(elements)` in `diagrams/duplicates.js`), elements and relationships on no
1848
1943
  view, nested elements with no relationship, and names or documentation with unprintable characters (control
1849
1944
  characters, zero-width spaces; tabs and line breaks are fine in documentation). Each check is a method listed by
1850
1945
  `checks()`, so a subclass changes the set; choosing a finding shows it (`editor.revealFinding(finding)`: its view,
1851
1946
  its shape, or in the ArchiMate tree), and a finding can offer actions beside it (`editor.findingActions(finding)`).
1852
- On the ArchiMate canvas, likely duplicates have **Merge…**: choose the element to keep (each choice says how many
1853
- views and relationships use it) and the others are merged into it, one undo step (`model.mergeCommand(keepId,
1854
- otherIds)`, a `MergeElementsCommand`). The kept element takes their relationships, their boxes on every view, the
1855
- documentation and properties it lacks and a specialization if it has none; a relationship left joining it to
1856
- itself, or the same as another, goes, its lines drawn as the other. The **File** menu (`EditorPart`'s, and the ArchiMate editor's) exports the current view
1947
+ On the ArchiMate canvas, likely duplicates have **Merge…**, and **Find likely duplicates…**, a tool for the whole
1948
+ model, is on the model's tab menu and in **Tools** (a part's `modelMenuItems()`, the editor's for an `EditorPart`;
1949
+ in its **Edit** menu when no `ModelsPart` hosts it). It asks where to look (the whole model, this view, the elements
1950
+ selected: `editor.duplicateScopes()`) and how to compare, then lists the groups found (a `DuplicatesDialog`, one for
1951
+ every modeller): each with a box to tick, its message (choosing it shows it) and **Merge…**; **Merge selected** merges
1952
+ the ticked groups one after another. The list stays open while they are merged, found again after each merge, so what
1953
+ was merged leaves it (`editor.askDuplicates()`, `editor.showDuplicates(options, { within })`, `checker.possibleDuplicates(options, { within })`).
1954
+ Its form is one for every modeller (`duplicateSearchFields({ scope, compare, properties, specializations })`,
1955
+ `duplicateSearchOptions(values)`, `validateDuplicateSearch`). The options are the jArchi dedupe scripts' (`DUPLICATE_OPTIONS`,
1956
+ `duplicateGroups(elements, options)`, `duplicateKey(element, options)`, `wordKey(text, options)`): compare the
1957
+ names, the documentation (an element with none, or only noise, is left out) or a property's value (an application
1958
+ id); leave out noise words (`NOISE_WORDS`: filler, "tbc", "tba" …), numbers and version numbers ("2", "v2",
1959
+ "1.2.3"), one-letter words and words matching a pattern (`^[Aa][0-9A-Fa-f]{5}$` for id codes); take a plural as its
1960
+ word; look across types; leave out a specialization (Decommissioned). **Match** `exact` compares the text as it is,
1961
+ case and spacing aside; `words` (the default) by its words, as above. Any other field can be compared by naming it
1962
+ (`by: 'attributes'`; a list is its items' names together). The model check compares names by their words with the
1963
+ filler words only.
1964
+
1965
+ ```js
1966
+ editor.showDuplicates({ by: 'documentation', noiseWords: NOISE_WORDS, ignoreNumbers: true });
1967
+ editor.showDuplicates({ by: 'property', property: 'AppId', acrossTypes: true });
1968
+ ```
1969
+
1970
+ With two or more elements selected, a shape's menu has **Merge N elements…** for them
1971
+ (`editor.elementIdsOf(boxIds)`: each box's element, once). Merging (`editor.askMerge(ids)`, resolving to the command
1972
+ or null) shows the elements side by side first (a `MergeCompareDialog`, one for every modeller: a column each, a row
1973
+ each for name, type, specialization, documentation, each property, relationships and views, the rows where they
1974
+ differ marked ≠; `editor.mergeComparison(elements)`), and under it asks which element to keep (each choice says how
1975
+ many views and relationships use it) and, where they differ, what it ends with: the documentation (all of it joined,
1976
+ the default, or one element's), the value of each property they have with different values, the specialization
1977
+ (`editor.mergeChoices(elements)`). **OK, merge** merges; Cancel leaves them. Elements that differ only in the views
1978
+ they are on merge into the first without asking. The others are merged into it, one undo step (`model.mergeCommand(keepId,
1979
+ otherIds, { documentation, properties, specialization })`, a `MergeElementsCommand`). The kept element always takes
1980
+ their relationships and their boxes on every view, and the properties it lacks; a relationship left joining it to
1981
+ itself, or the same as another, goes, its lines drawn as the other.
1982
+
1983
+ **View ▸ Remove repeated boxes (N)** takes off the view each box that only repeats another of its element: the same
1984
+ label shown, in the same box (or none), with the same lines drawn (each relationship, or connection, to the same
1985
+ thing at the other end), as one undo step with its lines (`view.repeatedBoxes()`, `editor.removeRepeatedBoxes()`).
1986
+ The first box stays; a box holding others is never taken off. The **File** menu (`EditorPart`'s, and the ArchiMate editor's) exports the current view
1857
1987
  as an image: PNG, SVG or a copy to the clipboard (`editor.exportImage('png' | 'svg' | 'copy')`, without the grid
1858
1988
  or selection; the ArchiMate editor's is off with `features.exportImage: false`). **Generate View For This**, on
1859
1989
  an element's menu in the diagram or the tree, makes a view of the element with everything related to it
@@ -1894,7 +2024,8 @@ changed: `editor.model.duplicateView(id)` opens and shows the copy, deleting a v
1894
2024
 
1895
2025
  **Canvases and templates.** As in Archi, the model tree's **Views** holds every view, canvases (`CanvasView`)
1896
2026
  and sketches included, in the sub-folders the Archi file kept it in (`view.archiFolder`; `viewFolderNodes(views,
1897
- depth)` makes a level of them); templates are apart (`model.diagramViews()`, `model.canvases()`,
2027
+ depth, emptyFolders)` makes a level of them). A sub-folder with no view in it is kept too, as its path
2028
+ (`model.emptyViewFolders`, read from an Archi or Exchange Format file and written back to either); templates are apart (`model.diagramViews()`, `model.canvases()`,
1898
2029
  `model.templates()`). The **Templates** folder holds the model's own templates (canvases
1899
2030
  marked `template`, saved in the `.archimate` file) and a **Library** of templates kept across models: a
1900
2031
  `CanvasTemplateLibrary` (a `TemplateLibrary`, the base any kind of view's library extends: entries, built-ins
@@ -2307,7 +2438,7 @@ for something else in ArchiMate, each as one undo step:
2307
2438
 
2308
2439
  `ArchimatePalette({version, onPick, document})` renders the element types grouped by layer
2309
2440
  (`ArchimatePalette.groups(version)`: the notation's layers, then ArchiMate 4's common behaviour types),
2310
- each tile only its notation glyph in its layer colour, four to a row (`ArchimatePalette.TILE`; the name shows on hover); drag a tile onto the canvas or click it (`onPick`).
2441
+ each tile only its notation glyph in its layer colour, six to a row at 12px (`ArchimatePalette.TILE`; the name shows on hover); drag a tile onto the canvas or click it (`onPick`).
2311
2442
 
2312
2443
  `ArchimateCanvasPart` (workspace bundle, type `archimate-canvas`; `examples/archimate.html`, and the testbed
2313
2444
  `test/archimate-canvas-testbed.html`) is the
@@ -2421,7 +2552,11 @@ model's, then the tool's, then the default (`cascade(view, modelId)`; `getModelO
2421
2552
  what the model level itself sets, `getModelSettings(modelId)` the model's resolved values). Global
2422
2553
  Settings sets the tool's value and the model's (which may inherit the tool's); a view's Properties sets
2423
2554
  the diagram's (which may inherit the model's). "Invalid relationships" (`invalidRelationships`:
2424
- `highlight`, `tooltip`, `off`) is set this way.
2555
+ `highlight`, `tooltip`, `off`) is set this way. Global Settings also sets how the model tree searches and shows, for
2556
+ every model (tool level, `TREE_SETTINGS_PROPERTIES`): the leaves a folder shows before "more" (`treeLeafLimit`, 8)
2557
+ and the characters a search needs (`treeSearchMinChars`, 3). `settingsManager.treeOptions()` gives them as
2558
+ ModelTree's options (`{ leafLimit, minFilterChars }`); the canvas part makes its tree with them and redraws it when
2559
+ they change.
2425
2560
 
2426
2561
  ```js
2427
2562
  settingsManager.saveToolSettings({ invalidRelationships: 'tooltip' }); // every model, unless it says otherwise
@@ -2614,6 +2749,16 @@ open model:
2614
2749
  or title. Ids are the tracker's keys, so a newer export brought in replaces what it had. Where each tracker keeps
2615
2750
  each field is data (`TRACKERS`); a team's own column is given with `new TrackerExport({ columns: { criteria:
2616
2751
  ['Done when'] } })` (in Jira's JSON, a custom field's key).
2752
+ - **A tracker, live** (`TrackerApi`; `JiraApi`, `AzureDevOpsApi`, `GitLabApi`): the issues a connection names, read
2753
+ through the tracker's API page by page — Jira by JQL (a project's by default; Cloud with an email and API token,
2754
+ Server / Data Center with a personal access token), Azure DevOps by WIQL (a project's work items but its tasks,
2755
+ 200 at a time, each linked to its page in Boards), GitLab a project's issues (open and closed, filtered as its
2756
+ issues API filters). `read()` is the JSON `TrackerExport` reads, `model()` the model's spec, named as the
2757
+ connection is; at most `maxIssues` (5000) are read, `truncated` saying when there were more. `fetch` is given,
2758
+ since a tracker's token belongs where the request is made (a server); an error says what the tracker answered,
2759
+ never the token. What a connection asks for is each class's `fields` (`trackerConnectionFields()`: per tracker,
2760
+ `{key, label, type?, choices?, required?, secret?, placeholder?, hint?}`), for a host to build its form from;
2761
+ `problems()` what is missing before the tracker is asked.
2617
2762
  - **A storyboard** (`StoryboardFileFormat`, `Storyboard`): Markdown — a `#` title names it, `##` are epics (or any
2618
2763
  heading saying *Epic*), `###` stories (or *Story*); "As a … I want …" list items under an epic are stories; a
2619
2764
  picture is kept by its caption.
@@ -2623,6 +2768,11 @@ const { RequirementsModel, TrackerExport } = window.RenderEngine;
2623
2768
  const model = new RequirementsModel(new TrackerExport().read(jiraCsvText));
2624
2769
  model.storyMap().columns.map(c => c.name); // the epics, across
2625
2770
  model.coverage().criteriaShare; // how many stories say when they are done
2771
+
2772
+ // The same, live — on a server, where the token is kept:
2773
+ const api = trackerApi({ tracker: 'jira', name: 'Shop backlog', baseUrl: 'https://shop.atlassian.net',
2774
+ user: 'me@example.com', token: process.env.JIRA_TOKEN, project: 'SHOP' }, { fetch });
2775
+ const live = new RequirementsModel(await api.model());
2626
2776
  ```
2627
2777
 
2628
2778
  `RequirementsPart` (type `requirements`, in a `ModelsPart` with `editorType: 'requirements'`) shows the model as
@@ -2655,6 +2805,11 @@ diagram). After an item acts, the bar re-renders. Items with live labels (the zo
2655
2805
  | `ToolbarText(text, style)` | Live text |
2656
2806
  | `ToolbarSeparator()` | Divider |
2657
2807
 
2808
+ `PopupMenu.show(x, y, items)` is the one popup or context menu (a toolbar menu's, an editor's, a host's
2809
+ right-click): items `{ label, action, danger?, checked?, disabled? }`, `{ label, items }` for a submenu, or
2810
+ `{ separator: true }`. One is open at a time; it moves back inside the window near an edge, and closes on a
2811
+ choice, a click elsewhere, Escape, or a scroll.
2812
+
2658
2813
  ```javascript
2659
2814
  new MenuBar({ container, editor, items: [
2660
2815
  new ToolbarMenu('File', [{ label: 'Export', action: exportIt }]),
@@ -2662,6 +2817,19 @@ new MenuBar({ container, editor, items: [
2662
2817
  ] }).render();
2663
2818
  ```
2664
2819
 
2820
+ `Dialog` is the one dialog frame, which `FormDialog` (a `<form>`) and any host's own dialog share: a title
2821
+ (drag it to move the dialog), an optional ✕ (`closeButton`), a body that scrolls between the title and the
2822
+ buttons, which stay in view, and Escape or a click outside to close. `buttons` are
2823
+ `[{ label, action(dialog), primary? }]`, `size` is `'tall'` or `'wide'`, and `onClose(applied)` is told once.
2824
+ `open()` shows it over the page; `open(host)` places it in `host` with no backdrop and no ✕, to show a dialog's
2825
+ content as a pane or a tab. A subclass gives its content as `content()` specs; a host fills `dialog.body` after
2826
+ `open()`, and adds to `dialog.titleBar` (a link) or `dialog.footer` (a pager).
2827
+
2828
+ ```javascript
2829
+ const rows = new Dialog({ title: 'orders', closeButton: true, size: 'tall' }).open();
2830
+ rows.body.append(table);
2831
+ ```
2832
+
2665
2833
  `FormDialog` is the one modal form: `fields` of type `text`, `textarea`, `number`, `color`, `select`
2666
2834
  or `checkbox`. A field with `inherit: { text, short? }` may be left unset; it then reads as `undefined` (a select
2667
2835
  offers `text` as its first choice; any other field, a checkbox too, gets a checkbox named `short`, default
@@ -2732,6 +2900,88 @@ to it) / `deleteEdgeCommand(id)`. A subclass whose shapes and lines stand for so
2732
2900
  overrides those factories (ArchimateCanvasEditor does). A template class says whether the editor
2733
2901
  offers it a rotate handle (`static rotatable`, true by default; ArchiMate's templates are not). `pickFile(accept)` resolves with the chosen `File` (or `null`).
2734
2902
 
2903
+ ## Navigation menus
2904
+
2905
+ `NavMenu` is a vertical navigation menu (a sidebar's), written as data: no markup. Each entry is
2906
+ one of three kinds, built from its JSON by `NavMenu.entry`:
2907
+
2908
+ | JSON | Class | Renders |
2909
+ |---|---|---|
2910
+ | `{ label, id?, icon?, href?, action?, selectable?, title?, cls?, visible? }` | `NavItem` | A link (`<a href>`) with `href`, otherwise a button |
2911
+ | `{ group, items, id?, open?, title?, cls?, visible? }` | `NavGroup` | A heading that opens and closes (`<details>`) over its items |
2912
+ | `{ section, title?, cls?, visible? }` | `NavSection` | A heading between entries |
2913
+
2914
+ ```javascript
2915
+ import { NavMenu } from '@peter.prib/renderengine';
2916
+
2917
+ const menu = new NavMenu({
2918
+ label: 'Workbench',
2919
+ context: { role: 'analyst' },
2920
+ open: { devtools: true },
2921
+ items: [
2922
+ { section: 'Notebooks' },
2923
+ { id: 'new-nb', label: '+ New Notebook', action: newNotebook, selectable: false },
2924
+ { group: 'Dev Tools', id: 'devtools', items: [
2925
+ { id: 'datamodel', icon: '◫', label: 'Data Model', href: '#', action: (event) => {
2926
+ event.preventDefault();
2927
+ openTool('datamodel', { beside: event.altKey });
2928
+ } },
2929
+ ] },
2930
+ { id: 'admin', icon: '⚙', label: 'Admin', href: '/admin', visible: (ctx) => ctx.role === 'admin' },
2931
+ ],
2932
+ onToggle: (groupId, open) => save(menu.serialize()),
2933
+ }).render(document.getElementById('nav'));
2934
+ ```
2935
+
2936
+ - **A click** runs the item's `action(event, item, menu)`, then makes the item the active one (unless it
2937
+ has no `id`, or `selectable: false`), then calls `onSelect(item, event)`. A link still navigates unless
2938
+ the action calls `event.preventDefault()`.
2939
+ `href` may be a function of the context, for an address known only later (`setContext({ adminUrl })`). The active item has the class `ws-nav-item-active` and
2940
+ `aria-current="page"`; `setActive(id)` moves it in place (`null`: none).
2941
+ - **`visible`** is a boolean or a function of the menu's `context`. A group none of whose items shows is
2942
+ hidden too. `setContext({ role })` merges into the context and re-renders.
2943
+ - **State:** `serialize()` returns `{ activeId, open: { groupId: boolean } }` (groups with an `id`), and
2944
+ the constructor takes `activeId` and `open` back; `onToggle(groupId, open)` reports a group opened or
2945
+ closed.
2946
+ - `find(id)` returns an entry anywhere in the menu; `all()` lists them depth first.
2947
+ - **Styles** are the workspace's (`ensureStyles`): `ws-nav`, `ws-nav-item`, `ws-nav-icon`, `ws-nav-label`,
2948
+ `ws-nav-group`, `ws-nav-group-summary`, `ws-nav-section`. Colours are inherited, so the menu suits a
2949
+ dark sidebar and a light one; `cls` adds a host's own classes to an entry.
2950
+
2951
+ `NavMenu` imports nothing from the browser at module scope, so it is exported from the node entry
2952
+ (`src/index.js`) as well as the workspace bundle.
2953
+
2954
+ ## Icons
2955
+
2956
+ `IconSet` holds named icons as data, and builds each as an `<svg>` spec for `el()`, so nothing is parsed. An
2957
+ icon is JSON: a `viewBox`, its `paint`, and its shapes as `[tag, attrs]` pairs.
2958
+
2959
+ ```javascript
2960
+ import { ICONS, icon, IconSet } from '@peter.prib/renderengine';
2961
+
2962
+ el(icon('cpu')); // the built-in pool
2963
+ el(ICONS.spec('database', { size: 16, title: 'Catalog' }));
2964
+ const mine = ICONS.with({ // a host's own icons over the built-in ones
2965
+ ring: { viewBox: '0 0 24 24', shapes: [['circle', { cx: 12, cy: 12, r: 10 }]] },
2966
+ });
2967
+ ```
2968
+
2969
+ - **`paint`:** `'stroke'` (the default) outlines the shapes in `currentColor`, so an icon takes its parent's
2970
+ colour. `'fill'` fills them in `currentColor`. `'none'` leaves the colours the shapes carry, as a brand mark does.
2971
+ - **`spec(name, { size = 24, title, cls })`:** returns an `<svg class="re-icon" data-icon="name">` spec. Without
2972
+ a `title` it is `aria-hidden`; with one it is `role="img"` and carries a `<title>`. An unknown name throws.
2973
+ - **Methods:** `define(name, icon)` checks the icon as it is added; `add(defs)`, `has(name)`, `get(name)` and
2974
+ `names()` work on the set; `with(defs)` returns a copy with `defs` added, leaving the original unchanged.
2975
+ - **The built-in icons** (`ICONS`, defined by `ICON_DEFS`): `dashboard`, `zap`, `clock`, `clipboard-list`,
2976
+ `book`, `cpu`, `memory`, `activity`, `database`, and the brand marks `google` and `microsoft`. They are named
2977
+ for the picture, not for where they are used.
2978
+ - **In a `NavMenu`:** an item's `icon` that names an icon in the menu's `icons` (default `ICONS`) is drawn as
2979
+ that icon; any other text, such as an emoji, is shown as it is.
2980
+
2981
+ `el()` builds an `svg` spec, and everything inside it, in the SVG namespace (`SVG_NS`, exported with `el` from
2982
+ the workspace bundle), until a `foreignObject`, whose children are HTML again. A spec's `ns` sets the namespace
2983
+ explicitly.
2984
+
2735
2985
  ## Where to go next
2736
2986
 
2737
2987
  - **`ARCHITECTURE.md`** — module layout, the chart-type catalog mechanics, how to add a new chart type.