@peter.prib/renderengine 0.5.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 (71) hide show
  1. package/README.md +4 -0
  2. package/REFERENCE.md +252 -17
  3. package/dist/renderengine-workspace.umd.js +4995 -2932
  4. package/dist/renderengine.umd.js +3985 -2193
  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/icons/IconSet.js +86 -0
  56. package/src/icons/index.js +59 -0
  57. package/src/index.js +65 -9
  58. package/src/workspace/dom.js +56 -42
  59. package/src/workspace/index.js +1 -1
  60. package/src/workspace/nav/NavEntry.js +42 -0
  61. package/src/workspace/nav/NavGroup.js +43 -0
  62. package/src/workspace/nav/NavItem.js +53 -0
  63. package/src/workspace/nav/NavMenu.js +143 -0
  64. package/src/workspace/nav/NavSection.js +17 -0
  65. package/src/workspace/nav/index.js +5 -0
  66. package/src/workspace/parts/ArchimateCanvasPart.js +8 -3
  67. package/src/workspace/parts/DataModelPart.js +91 -0
  68. package/src/workspace/parts/EditorPart.js +8 -0
  69. package/src/workspace/parts/ModelHosted.js +1 -0
  70. package/src/workspace/parts/ModelsPart.js +765 -622
  71. package/src/workspace/styles.js +28 -1
@@ -1,3 +1,20 @@
1
+ /** A row of the tree, branch or leaf: one line, compact; a label too long ends in an ellipsis. */
2
+ const ROW_STYLE = `
3
+ cursor: pointer;
4
+ padding: 1px 4px;
5
+ margin: 0;
6
+ border-radius: 3px;
7
+ user-select: none;
8
+ border-left: 3px solid transparent;
9
+ transition: background-color 0.1s;
10
+ display: flex;
11
+ align-items: center;
12
+ gap: 4px;
13
+ line-height: 1.35;
14
+ white-space: nowrap;
15
+ overflow: hidden;
16
+ `;
17
+
1
18
  /**
2
19
  * Generic tree component builder for hierarchical node rendering.
3
20
  * Handles expand/collapse, selection, context menus, and recursive rendering.
@@ -19,8 +36,14 @@ export class TreeBuilder {
19
36
  * @param {Map<string, boolean>} [options.openState] - Which folders are open, by node id: kept
20
37
  * by the caller so a rebuilt tree opens them as they were. A folder not in it is open unless
21
38
  * its node says `collapsed: true`; opening or closing one records it here.
39
+ * @param {(node) => boolean} [options.filter] - Which leaves are shown: a folder is shown when a
40
+ * leaf under it is, and open while filtering. None: every node.
41
+ * @param {number} [options.leafLimit] - The leaves a folder shows; the rest wait behind a
42
+ * "▸ N more" row, which shows them. The folders inside it always show.
43
+ * @param {Set<string>} [options.shownAll] - The folders (by node id) showing every leaf, kept by
44
+ * the caller as openState is; asking for "more" records the folder here.
22
45
  */
23
- constructor(doc, { container, rootNodes = [], onNodeClick = null, onNodeContext = null, getNodeChildren = null, openState = new Map(), iconFor = null } = {}) {
46
+ constructor(doc, { container, rootNodes = [], onNodeClick = null, onNodeContext = null, getNodeChildren = null, openState = new Map(), iconFor = null, filter = null, leafLimit = Infinity, shownAll = new Set() } = {}) {
24
47
  this.doc = doc;
25
48
  this.container = container;
26
49
  this.rootNodes = rootNodes;
@@ -29,7 +52,12 @@ export class TreeBuilder {
29
52
  this.getNodeChildren = getNodeChildren;
30
53
  this.openState = openState;
31
54
  this.iconFor = iconFor;
55
+ this.filter = filter;
56
+ this.leafLimit = leafLimit;
57
+ this.shownAll = shownAll;
32
58
  this.selectedNode = null;
59
+ /** Each node's shown children, asked for once per render (getNodeChildren makes new nodes). */
60
+ this._children = new WeakMap();
33
61
  }
34
62
 
35
63
  /**
@@ -40,10 +68,25 @@ export class TreeBuilder {
40
68
  const container = this.container;
41
69
  if (!container) return;
42
70
 
43
- container.innerHTML = '';
44
- for (const node of nodes) {
45
- container.appendChild(this._renderNode(node));
71
+ this._children = new WeakMap();
72
+ container.replaceChildren(...nodes.filter(node => this._shown(node)).map(node => this._renderNode(node)));
73
+ }
74
+
75
+ /** A node's children that are shown: with a filter, the leaves it keeps and the folders holding one. @private */
76
+ _childrenOf(node) {
77
+ if (!this._children.has(node)) {
78
+ const all = this.getNodeChildren?.(node) || [];
79
+ this._children.set(node, this.filter ? all.filter(child => this._shown(child)) : all);
46
80
  }
81
+ return this._children.get(node);
82
+ }
83
+
84
+ /** Whether a node is shown: always without a filter; else a leaf the filter keeps, or a folder with a shown child. @private */
85
+ _shown(node) {
86
+ if (!this.filter) return true;
87
+ if (this._childrenOf(node).length) return true;
88
+ // A folder whose children are all filtered out is not a leaf to test.
89
+ return !(this.getNodeChildren?.(node) || []).length && !!this.filter(node);
47
90
  }
48
91
 
49
92
  /**
@@ -51,14 +94,8 @@ export class TreeBuilder {
51
94
  * @private
52
95
  */
53
96
  _renderNode(node) {
54
- const children = this.getNodeChildren?.(node) || [];
55
- const hasChildren = children.length > 0;
56
-
57
- if (hasChildren) {
58
- return this._renderExpandable(node, children);
59
- } else {
60
- return this._renderLeaf(node);
61
- }
97
+ const children = this._childrenOf(node);
98
+ return children.length ? this._renderExpandable(node, children) : this._renderLeaf(node);
62
99
  }
63
100
 
64
101
  /**
@@ -67,23 +104,13 @@ export class TreeBuilder {
67
104
  */
68
105
  _renderExpandable(node, children) {
69
106
  const details = this.doc.createElement('details');
70
- details.open = this.openState.get(node.id) ?? !node.collapsed;
107
+ // Open while filtering, to show what was found (not recorded); else as it was.
108
+ details.open = this.filter ? true : this.openState.get(node.id) ?? !node.collapsed;
71
109
  details.setAttribute('data-node-id', node.id);
72
- details.addEventListener('toggle', () => this.openState.set(node.id, details.open));
110
+ details.addEventListener('toggle', () => { if (!this.filter) this.openState.set(node.id, details.open); });
73
111
 
74
112
  const summary = this.doc.createElement('summary');
75
- summary.style.cssText = `
76
- cursor: pointer;
77
- padding: 4px 6px;
78
- margin: 2px 0;
79
- border-radius: 3px;
80
- user-select: none;
81
- border-left: 3px solid transparent;
82
- transition: background-color 0.1s;
83
- display: flex;
84
- align-items: center;
85
- gap: 6px;
86
- `;
113
+ summary.style.cssText = ROW_STYLE;
87
114
  summary.setAttribute('data-node-id', node.id);
88
115
 
89
116
  this._applyNodeStyling(summary, node);
@@ -95,15 +122,45 @@ export class TreeBuilder {
95
122
  details.appendChild(summary);
96
123
 
97
124
  const childContainer = this.doc.createElement('div');
98
- childContainer.style.cssText = 'padding-left: 16px;';
99
- for (const child of children) {
100
- childContainer.appendChild(this._renderNode(child));
101
- }
125
+ childContainer.style.cssText = 'padding-left: 12px;';
126
+ this._fillChildren(childContainer, node, children);
102
127
  details.appendChild(childContainer);
103
128
 
104
129
  return details;
105
130
  }
106
131
 
132
+ /**
133
+ * A folder's children: its folders, and its leaves up to leafLimit, then a
134
+ * "▸ N more" row that shows the rest (unless the folder shows them all already).
135
+ * @private
136
+ */
137
+ _fillChildren(container, node, children) {
138
+ const limit = this.shownAll.has(node.id) ? Infinity : this.leafLimit;
139
+ let leaves = 0;
140
+ let waiting = 0;
141
+ const rows = [];
142
+ for (const child of children) {
143
+ if (!this._childrenOf(child).length && leaves++ >= limit) { waiting++; continue; }
144
+ rows.push(this._renderNode(child));
145
+ }
146
+ if (waiting) rows.push(this._moreRow(waiting, () => {
147
+ this.shownAll.add(node.id);
148
+ this._fillChildren(container, node, children);
149
+ }));
150
+ container.replaceChildren(...rows);
151
+ }
152
+
153
+ /** The row standing for `count` leaves not shown; choosing it calls `showAll`. @private */
154
+ _moreRow(count, showAll) {
155
+ const row = this.doc.createElement('div');
156
+ row.setAttribute('data-tree-more', String(count));
157
+ row.style.cssText = `${ROW_STYLE} color: #666; font-style: italic;`;
158
+ row.textContent = `▸ ${count} more`;
159
+ row.title = 'Show them all';
160
+ row.addEventListener('click', (e) => { e.stopPropagation(); showAll(); });
161
+ return row;
162
+ }
163
+
107
164
  /**
108
165
  * Render a leaf node without children.
109
166
  * @private
@@ -111,23 +168,12 @@ export class TreeBuilder {
111
168
  _renderLeaf(node) {
112
169
  const leaf = this.doc.createElement('div');
113
170
  leaf.setAttribute('data-node-id', node.id);
114
- // Element leaves are filter targets and drag sources; mark them here so a rebuilt tree keeps that.
171
+ // Element leaves are drag sources; mark them here so a rebuilt tree keeps that.
115
172
  if (node.type === 'element' && node._element) {
116
173
  leaf.setAttribute('data-element-id', node._element.id);
117
174
  leaf.draggable = true;
118
175
  }
119
- leaf.style.cssText = `
120
- padding: 4px 6px;
121
- margin: 2px 0;
122
- border-radius: 3px;
123
- cursor: pointer;
124
- user-select: none;
125
- border-left: 3px solid transparent;
126
- transition: background-color 0.1s;
127
- display: flex;
128
- align-items: center;
129
- gap: 6px;
130
- `;
176
+ leaf.style.cssText = ROW_STYLE;
131
177
 
132
178
  this._applyNodeStyling(leaf, node);
133
179
  leaf.addEventListener('click', (e) => this._handleNodeClick(e, leaf, node));
@@ -153,13 +199,15 @@ export class TreeBuilder {
153
199
  } else if (node.icon) {
154
200
  const icon = this.doc.createElement('span');
155
201
  icon.textContent = node.icon;
156
- icon.style.cssText = 'flex-shrink: 0; font-size: 14px;';
202
+ icon.style.cssText = 'flex-shrink: 0; font-size: 11px;';
157
203
  element.appendChild(icon);
158
204
  }
159
205
 
160
- // Label
206
+ // Label: one line, cut with an ellipsis; the whole of it on hover.
161
207
  const label = this.doc.createElement('span');
162
208
  label.textContent = node.label || 'Untitled';
209
+ label.style.cssText = 'overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;';
210
+ element.title = label.textContent;
163
211
  element.appendChild(label);
164
212
  }
165
213
 
@@ -7,6 +7,13 @@ export class TreeViewMode {
7
7
  static key = null; // registry key; subclasses override
8
8
  static label = ''; // human-readable label; subclasses override
9
9
 
10
+ /** Whether `text` is in any of `fields` (case ignored); no text matches everything. */
11
+ static textMatches(text, fields) {
12
+ if (!text) return true;
13
+ const lower = text.toLowerCase();
14
+ return fields.some(f => String(f ?? '').toLowerCase().includes(lower));
15
+ }
16
+
10
17
  constructor() {
11
18
  if (new.target === TreeViewMode) {
12
19
  throw new TypeError('TreeViewMode is abstract — subclass it');
@@ -59,12 +66,8 @@ export class TreeViewMode {
59
66
  */
60
67
  _matchesFilter(item, { text = '', typeIds = null, specializationIds = null, propertyIds = null, inDescriptions = false } = {}) {
61
68
  // Text filter (label or type; the description too when asked)
62
- if (text) {
63
- const textLower = text.toLowerCase();
64
- const fields = [item.label, item.type, ...(inDescriptions ? [item.description] : [])];
65
- if (!fields.some(f => String(f ?? '').toLowerCase().includes(textLower))) {
66
- return false;
67
- }
69
+ if (!TreeViewMode.textMatches(text, [item.label, item.type, ...(inDescriptions ? [item.description] : [])])) {
70
+ return false;
68
71
  }
69
72
 
70
73
  // Type filter (elements only; 'type:business-actor' format)
@@ -6,23 +6,32 @@ const VIEW_ICONS = Object.freeze({ canvas: '🎨' });
6
6
  * keeps them: each view remembers its sub-folder path (`archiFolder`, folder
7
7
  * steps {id, name}). A folder node per sub-folder at this depth (holding the
8
8
  * views under it, opened a level deeper), then the views that sit here, each
9
- * in the model's order.
9
+ * in the model's order. A sub-folder that holds no view is there too, from
10
+ * its path (the model's `emptyViewFolders`).
10
11
  * @param {Array<Object>} views
11
12
  * @param {number} [depth]
13
+ * @param {Array<Array<{id, name}>>} [emptyFolders] - The paths of the folders with no view in them
12
14
  * @returns {Array<Object>} TreeBuilder nodes: {id, label, icon, type: 'view-folder'|'view', ...}
13
15
  */
14
- export function viewFolderNodes(views, depth = 0) {
16
+ export function viewFolderNodes(views, depth = 0, emptyFolders = []) {
15
17
  const folders = new Map();
18
+ const folderOf = (step) => {
19
+ if (!folders.has(step.id)) folders.set(step.id, { step, views: [], empty: [] });
20
+ return folders.get(step.id);
21
+ };
16
22
  const here = [];
17
23
  for (const view of views) {
18
24
  const step = view.archiFolder?.[depth];
19
25
  if (!step) { here.push(view); continue; }
20
- if (!folders.has(step.id)) folders.set(step.id, { step, views: [] });
21
- folders.get(step.id).views.push(view);
26
+ folderOf(step).views.push(view);
27
+ }
28
+ for (const path of emptyFolders) {
29
+ if (path.length > depth) folderOf(path[depth]).empty.push(path);
22
30
  }
23
31
  return [
24
- ...[...folders.values()].map(({ step, views: inside }) => ({
25
- id: `view-folder-${step.id}`, label: step.name || 'Untitled Folder', icon: '📁', type: 'view-folder', _views: inside, _depth: depth + 1,
32
+ ...[...folders.values()].map(({ step, views: inside, empty }) => ({
33
+ id: `view-folder-${step.id}`, label: step.name || 'Untitled Folder', icon: '📁', type: 'view-folder',
34
+ _views: inside, _emptyFolders: empty, _depth: depth + 1,
26
35
  })),
27
36
  ...here.map(view => ({
28
37
  id: view.id, label: view.name || view.title || 'Untitled View', icon: VIEW_ICONS[view.constructor.kind] ?? '📄', type: 'view', _view: view,
@@ -0,0 +1,86 @@
1
+ /**
2
+ * IconSet — named icons as data, built as `<svg>` specs for el().
3
+ *
4
+ * An icon is JSON: a viewBox, how it is painted, and its shapes as
5
+ * `[tag, attrs]` pairs:
6
+ *
7
+ * { viewBox: '0 0 24 24', paint: 'stroke', shapes: [['circle', { cx: 12, cy: 12, r: 10 }]] }
8
+ *
9
+ * `paint` is 'stroke' (outlined in currentColor — the default, so an icon
10
+ * takes its parent's colour), 'fill' (filled in currentColor) or 'none' (the
11
+ * shapes carry their own colours: a brand mark). No markup is parsed: el()
12
+ * builds the spec in the SVG namespace.
13
+ */
14
+ const PAINT = {
15
+ stroke: { fill: 'none', stroke: 'currentColor', 'stroke-width': 2, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' },
16
+ fill: { fill: 'currentColor' },
17
+ none: {},
18
+ };
19
+
20
+ export class IconSet {
21
+ /** @param {Object<string, Object>} [defs] - Name → icon */
22
+ constructor(defs = {}) {
23
+ this.defs = new Map();
24
+ this.add(defs);
25
+ }
26
+
27
+ /** Add (or replace) icons, name → icon. */
28
+ add(defs) {
29
+ for (const [name, def] of Object.entries(defs)) this.define(name, def);
30
+ return this;
31
+ }
32
+
33
+ /** Add (or replace) one icon; throws when it is not one. */
34
+ define(name, def) {
35
+ if (typeof def?.viewBox !== 'string' || !Array.isArray(def.shapes) || !def.shapes.length) {
36
+ throw new Error(`IconSet: icon "${name}" needs a viewBox and shapes`);
37
+ }
38
+ if (def.paint != null && !(def.paint in PAINT)) {
39
+ throw new Error(`IconSet: icon "${name}" paint is ${Object.keys(PAINT).join(', ')}, not "${def.paint}"`);
40
+ }
41
+ for (const shape of def.shapes) {
42
+ if (!Array.isArray(shape) || typeof shape[0] !== 'string') throw new Error(`IconSet: icon "${name}" shapes are [tag, attrs] pairs`);
43
+ }
44
+ this.defs.set(name, def);
45
+ return this;
46
+ }
47
+
48
+ has(name) { return this.defs.has(name); }
49
+
50
+ get(name) { return this.defs.get(name) ?? null; }
51
+
52
+ /** Every icon's name, in the order added. */
53
+ names() { return [...this.defs.keys()]; }
54
+
55
+ /** A copy of this set with `defs` added: a host's own icons over these. */
56
+ with(defs) {
57
+ return new IconSet(Object.fromEntries(this.defs)).add(defs);
58
+ }
59
+
60
+ /**
61
+ * The icon as an el() spec.
62
+ * @param {string} name
63
+ * @param {Object} [options]
64
+ * @param {number|string} [options.size] - Width and height (default 24; CSS can override)
65
+ * @param {string} [options.title] - Its accessible name; without one it is hidden from screen readers
66
+ * @param {string|string[]} [options.cls] - Extra class names
67
+ */
68
+ spec(name, { size = 24, title = null, cls = null } = {}) {
69
+ const def = this.defs.get(name);
70
+ if (!def) throw new Error(`IconSet: no icon "${name}"`);
71
+ return {
72
+ tag: 'svg',
73
+ cls: ['re-icon', ...(Array.isArray(cls) ? cls : [cls])],
74
+ attrs: {
75
+ viewBox: def.viewBox, width: size, height: size, focusable: 'false',
76
+ role: title ? 'img' : null, 'aria-hidden': title ? null : 'true',
77
+ ...PAINT[def.paint ?? 'stroke'],
78
+ },
79
+ dataset: { icon: name },
80
+ children: [
81
+ ...(title ? [{ tag: 'title', text: title }] : []),
82
+ ...def.shapes.map(([tag, attrs]) => ({ tag, attrs })),
83
+ ],
84
+ };
85
+ }
86
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The built-in icons — a shared pool any host draws from (`ICONS.spec('cpu')`,
3
+ * or `icon('cpu')`), extended with its own (`ICONS.with({ ... })`). Outlined
4
+ * icons on a 24-unit grid, stroked in currentColor; the brand marks keep their
5
+ * colours. Names say what the picture is, not where it is used.
6
+ */
7
+ import { IconSet } from './IconSet.js';
8
+
9
+ const box = (x, y, width, height, rx = 1) => ['rect', { x, y, width, height, rx }];
10
+ const path = (d) => ['path', { d }];
11
+
12
+ export const ICON_DEFS = {
13
+ dashboard: { viewBox: '0 0 24 24', shapes: [box(3, 3, 7, 9), box(14, 3, 7, 5), box(14, 12, 7, 9), box(3, 16, 7, 5)] },
14
+ zap: { viewBox: '0 0 24 24', shapes: [path('m13 2-2 10h8L11 22l2-10H5L13 2z')] },
15
+ clock: { viewBox: '0 0 24 24', shapes: [['circle', { cx: 12, cy: 12, r: 10 }], ['polyline', { points: '12 6 12 12 16 14' }]] },
16
+ 'clipboard-list': { viewBox: '0 0 24 24', shapes: [
17
+ box(8, 2, 8, 4),
18
+ path('M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'),
19
+ path('M12 11h4'), path('M12 16h4'), path('M8 11h.01'), path('M8 16h.01'),
20
+ ] },
21
+ book: { viewBox: '0 0 24 24', shapes: [
22
+ path('M4 19.5A2.5 2.5 0 0 1 6.5 17H20'),
23
+ path('M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z'),
24
+ ] },
25
+ cpu: { viewBox: '0 0 24 24', shapes: [
26
+ box(4, 4, 16, 16, 2), box(9, 9, 6, 6),
27
+ ...['M15 2v2', 'M15 20v2', 'M2 15h2', 'M2 9h2', 'M20 15h2', 'M20 9h2', 'M9 2v2', 'M9 20v2'].map(path),
28
+ ] },
29
+ memory: { viewBox: '0 0 24 24', shapes: [
30
+ path('M22 12s-3 1-5 1-3-1-5-1-3 1-5 1-5-1-5-1'), path('M7 21h10'), path('M9 21V10'), path('M15 21V10'),
31
+ path('M8 3h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2Z'),
32
+ ] },
33
+ activity: { viewBox: '0 0 24 24', shapes: [['polyline', { points: '22 12 18 12 15 21 9 3 6 12 2 12' }]] },
34
+ database: { viewBox: '0 0 24 24', shapes: [
35
+ ['ellipse', { cx: 12, cy: 5, rx: 9, ry: 3 }],
36
+ path('M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5'),
37
+ path('M3 12c0 1.66 4.03 3 9 3s9-1.34 9-3'),
38
+ ] },
39
+ google: { viewBox: '0 0 24 24', paint: 'none', shapes: [
40
+ ['path', { fill: '#4285F4', d: 'M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z' }],
41
+ ['path', { fill: '#34A853', d: 'M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z' }],
42
+ ['path', { fill: '#FBBC05', d: 'M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z' }],
43
+ ['path', { fill: '#EA4335', d: 'M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z' }],
44
+ ] },
45
+ microsoft: { viewBox: '0 0 21 21', paint: 'none', shapes: [
46
+ ['rect', { x: 1, y: 1, width: 9, height: 9, fill: '#F25022' }],
47
+ ['rect', { x: 11, y: 1, width: 9, height: 9, fill: '#7FBA00' }],
48
+ ['rect', { x: 1, y: 11, width: 9, height: 9, fill: '#00A4EF' }],
49
+ ['rect', { x: 11, y: 11, width: 9, height: 9, fill: '#FFB900' }],
50
+ ] },
51
+ };
52
+
53
+ /** The built-in set. */
54
+ export const ICONS = new IconSet(ICON_DEFS);
55
+
56
+ /** A built-in icon as an el() spec — `ICONS.spec(name, options)`. */
57
+ export const icon = (name, options) => ICONS.spec(name, options);
58
+
59
+ export { IconSet };
package/src/index.js CHANGED
@@ -96,7 +96,7 @@ import { ArchimateSpecializationRegistry } from './archimateSpecializations.js';
96
96
  import { ArchimateSpecializationSelector } from './charting/ArchimateSpecializationSelector.js';
97
97
  import { ImageSelector } from './charting/ImageSelector.js';
98
98
  import { CheckboxTreeSelector } from './charting/CheckboxTreeSelector.js';
99
- import { SettingsManager, settingsManager } from './charting/SettingsManager.js';
99
+ import { SettingsManager, settingsManager, TREE_SETTINGS_PROPERTIES } from './charting/SettingsManager.js';
100
100
  import { showGlobalSettingsDialog } from './charting/SettingsDialog.js';
101
101
  import { ArchimateModel, ArchimateView } from './charting/diagrams/ArchimateModel.js';
102
102
  import { CanvasView } from './charting/diagrams/CanvasView.js';
@@ -144,6 +144,7 @@ import { PopupMenu } from './charting/PopupMenu.js';
144
144
  import { ViewTabStrip } from './charting/ViewTabStrip.js';
145
145
  import { EditorShell } from './workspace/EditorShell.js';
146
146
  import { FormDialog } from './charting/FormDialog.js';
147
+ import { Dialog } from './charting/Dialog.js';
147
148
  import { ColorField } from './charting/ColorField.js';
148
149
  import { TextStyle } from './charting/TextStyle.js';
149
150
  import { ArchimateModelChecker } from './charting/diagrams/ArchimateModelChecker.js';
@@ -158,6 +159,10 @@ import { PropertiesPanel } from './charting/PropertiesPanel.js';
158
159
  import { MenuBar } from './charting/MenuBar.js';
159
160
  import { ArchimateDiagramEditor } from './charting/ArchimateDiagramEditor.js';
160
161
  import { ModelTree } from './charting/modelTree/ModelTree.js';
162
+ import { TreeBuilder } from './charting/modelTree/TreeBuilder.js';
163
+ import { ContentSource } from './charting/content/ContentSource.js';
164
+ import { ContentSources } from './charting/content/ContentSources.js';
165
+ import { DUPLICATE_OPTIONS, NOISE_WORDS, FILLER_WORDS, duplicateGroups, duplicateKey, wordKey, nameKey } from './charting/diagrams/duplicates.js';
161
166
  import { TreeModeRegistry } from './charting/modelTree/TreeModeRegistry.js';
162
167
  import { TREE_MODE_CATALOG, treeModeRegistry, getTreeModeCatalogEntry } from './treeModeCatalog.js';
163
168
  import { buildModelTreePanel } from './charting/modelTree/ModelTreePanel.js';
@@ -215,15 +220,16 @@ export {
215
220
  TREE_MODE_CATALOG, treeModeRegistry, getTreeModeCatalogEntry,
216
221
  AXIS_KIND, ENCODING_CHANNEL, COORD_SYSTEM, COLOR_MODE, CAMERA_MODE,
217
222
  Command, CompositeCommand, CommandHistory,
218
- DiagramEditor, DiagramEditorState, Template, PolygonTemplate, RegularPolygonTemplate, RectangleTemplate, RoundedRectangleTemplate, StadiumTemplate, PredefinedProcessTemplate, ClassTemplate, ComponentTemplate, SwimlaneTemplate, CircleTemplate, FinalStateTemplate, EllipseTemplate, DiamondTemplate, TriangleTemplate, PentagonTemplate, HexagonTemplate, OctagonTemplate, StarTemplate, ParallelogramTemplate, TrapezoidTemplate, ManualInputTemplate, CylinderTemplate, DocumentTemplate, NoteTemplate, CloudTemplate, ActorTemplate, PackageTemplate, BoxTemplate, DataStoreTemplate, LifelineTemplate, TemplateCatalog, Palette, PropertiesPanel, MenuBar, MoveElementCommand, BringToFrontCommand, SendToBackCommand, RaiseCommand, LowerCommand, CreateTemplateCommand, EditPropertyCommand, ResizeCommand, RotateCommand, SetPropertiesCommand, ReorderCommand, DIAGRAM_TYPES, getTemplatesForDiagramType, getAllDiagramTypes, getDiagramTypesByGroup, EdgeMarker, ArrowMarker, DiamondMarker, CircleMarker, CrowsFootMarker, HollowDiamondMarker, HollowTriangleMarker, OpenArrowMarker, HalfArrowMarker, EDGE_MARKERS, getEdgeMarker, Edge, AddEdgeCommand, DeleteEdgeCommand, ReconnectEdgeCommand, EDGE_STYLE_PROPERTIES, LINE_ONLY_PROPERTIES, LINE_STYLE_DASH, EdgeStyleCascade, SetStyleCommand, EdgeStyleDialog, DiagramModel, DiagramView, ViewSetModel, ViewMenu, ViewPropertiesDialog, SettingsViewPropertiesDialog, DiagramViewPropertiesDialog, inheritableFields, readInheritableFields, LocalJsonStore, PopupMenu, ViewTabStrip, EditorShell, FormDialog, ToolbarItem, ToolbarButton, ToolbarMenu, ToolbarSelect, ToolbarCheckbox, ToolbarText, ToolbarSeparator, pickFile, reorderedZ, stackingOrder, MoveWaypointCommand, AddWaypointCommand, DeleteWaypointCommand,
223
+ DiagramEditor, DiagramEditorState, Template, PolygonTemplate, RegularPolygonTemplate, RectangleTemplate, RoundedRectangleTemplate, StadiumTemplate, PredefinedProcessTemplate, ClassTemplate, ComponentTemplate, SwimlaneTemplate, CircleTemplate, FinalStateTemplate, EllipseTemplate, DiamondTemplate, TriangleTemplate, PentagonTemplate, HexagonTemplate, OctagonTemplate, StarTemplate, ParallelogramTemplate, TrapezoidTemplate, ManualInputTemplate, CylinderTemplate, DocumentTemplate, NoteTemplate, CloudTemplate, ActorTemplate, PackageTemplate, BoxTemplate, DataStoreTemplate, LifelineTemplate, TemplateCatalog, Palette, PropertiesPanel, MenuBar, MoveElementCommand, BringToFrontCommand, SendToBackCommand, RaiseCommand, LowerCommand, CreateTemplateCommand, EditPropertyCommand, ResizeCommand, RotateCommand, SetPropertiesCommand, ReorderCommand, DIAGRAM_TYPES, getTemplatesForDiagramType, getAllDiagramTypes, getDiagramTypesByGroup, EdgeMarker, ArrowMarker, DiamondMarker, CircleMarker, CrowsFootMarker, HollowDiamondMarker, HollowTriangleMarker, OpenArrowMarker, HalfArrowMarker, EDGE_MARKERS, getEdgeMarker, Edge, AddEdgeCommand, DeleteEdgeCommand, ReconnectEdgeCommand, EDGE_STYLE_PROPERTIES, LINE_ONLY_PROPERTIES, LINE_STYLE_DASH, EdgeStyleCascade, SetStyleCommand, EdgeStyleDialog, DiagramModel, DiagramView, ViewSetModel, ViewMenu, ViewPropertiesDialog, SettingsViewPropertiesDialog, DiagramViewPropertiesDialog, inheritableFields, readInheritableFields, LocalJsonStore, PopupMenu, ViewTabStrip, EditorShell, Dialog, FormDialog, ToolbarItem, ToolbarButton, ToolbarMenu, ToolbarSelect, ToolbarCheckbox, ToolbarText, ToolbarSeparator, pickFile, reorderedZ, stackingOrder, MoveWaypointCommand, AddWaypointCommand, DeleteWaypointCommand,
219
224
  ebnfAstToDiagram, parseEbnf,
220
225
  createDataWindow, GraphDataWindow,
221
226
  ArchimateNotation, ArchimateLayer, ArchimateElementType, ArchimateRelationshipType,
222
227
  Glyph, GLYPHS, glyphFor, drawGlyph, glyphElement, glyphToSvg, GlyphTemplate, ArchimateElementTemplate, ArchimateEdge, ArchimateCanvasEditor, ArchimatePalette, KIND_PREFERENCE, ARCHIMATE_TYPE_MIME,
223
228
  ArchimateModel, ArchimateView, CanvasView, PlaceItemsCommand, RemoveLineCommand, CanvasBoxKind, CANVAS_BOX_KINDS, canvasBoxKind, canvasBoxMenuItems, ArchiHint, HintPanel, EditHintCommand, hintMenuItems, CanvasPalette, CANVAS_BOX_MIME, ViewLineEdge, Arrangement, ItemGroup, BackgroundGroup, GroupCommand, UngroupCommand, LockCommand, BackgroundCommand, arrangeMenuItems, TemplateLibrary, CanvasTemplateLibrary, CanvasTemplateMenu, DEFAULT_CANVAS_TEMPLATES, canvasTemplate, EditViewItemCommand, viewItemChanges, ColorField, TextStyle, ArchimateModelChecker, ViewSetChecker, ViewFit, archiElementType, NEAREST_ARCHI_TYPE, ElementInstance, ViewObjectInstance, RelationshipInstance, ArchimateDiagramEditor,
224
229
  ITIconNotation, ITElementType,
225
- ArchimateValidator, ArchimateSpecializationSelector, ImageSelector, CheckboxTreeSelector, SettingsManager, settingsManager,
226
- ModelTree, TreeModeRegistry,
230
+ ArchimateValidator, ArchimateSpecializationSelector, ImageSelector, CheckboxTreeSelector, SettingsManager, settingsManager, TREE_SETTINGS_PROPERTIES,
231
+ ModelTree, TreeBuilder, TreeModeRegistry, ContentSource, ContentSources,
232
+ DUPLICATE_OPTIONS, NOISE_WORDS, FILLER_WORDS, duplicateGroups, duplicateKey, wordKey, nameKey,
227
233
  RadialLayout, DerivedRelationships, EdgeGeometry,
228
234
  buildModelTreePanel, buildAnalysisPanel, buildAnalysisData, buildTypeCategoryTree, buildPropertyTree, DiagramTabs,
229
235
  DataModelNotation, DataModelLevel, AttributedLevel, ConceptualLevel, LogicalLevel, PhysicalLevel, NotationStyle, RelationshipKind, AssociationKind, GeneralizationKind, EntityRole, FactRole, DimensionRole,
@@ -251,6 +257,11 @@ export {
251
257
  * @class RenderEngine
252
258
  * This class decides which renderer to use and provides a unified drawing API.
253
259
  */
260
+ // Navigation menus (a sidebar's): DOM built from JSON, safe to import under node.
261
+ export { NavEntry, NavItem, NavGroup, NavSection, NavMenu } from './workspace/nav/index.js';
262
+ // Named icons as data (an <svg> spec each); the built-in pool and its class.
263
+ export { IconSet, ICONS, ICON_DEFS, icon } from './icons/index.js';
264
+
254
265
  export class RenderEngine {
255
266
  /** Layout constants for graphDiagram's `shape: 'table'` node type. */
256
267
  static TABLE_NODE = NodeGeometry.TABLE_NODE;
@@ -439,8 +450,12 @@ export class RenderEngine {
439
450
  * @param {HTMLElement} container The container element for the chart.
440
451
  * @param {object} [options]
441
452
  * @param {'canvas' | 'svg'} [options.rendererType='canvas']
453
+ * @param {false|{wheelModifier?: 'ctrl'|'alt'|'shift'}} [options.zoom] - SVG wheel zoom: off, or only
454
+ * with a modifier held (so a plain wheel scrolls the page); on, with a plain wheel, by default
442
455
  */
443
456
  constructor(container, options = {}) {
457
+ // Kept: the engines refreshChart() makes for other containers are made with the same options.
458
+ this.options = options;
444
459
  if (container) {
445
460
  const { rendererType = 'canvas' } = options;
446
461
  const width = container.clientWidth;
@@ -448,11 +463,9 @@ export class RenderEngine {
448
463
 
449
464
  this.container = container; // Store container for event listeners
450
465
  this.renderer = RendererFactory.create(rendererType, container, width, height);
451
- } else {
452
- // Allow initialization without a container for testing
453
- this.options = options;
454
- console.log('RenderEngine initialized in test mode.');
455
466
  }
467
+ // No container: a test, or an engine keeping a page of registered
468
+ // charts, each drawn in its own element (refreshChart).
456
469
 
457
470
  if (this.container) {
458
471
  this._interactions = new InteractionManager(this);
@@ -2588,7 +2601,10 @@ export class RenderEngine {
2588
2601
 
2589
2602
  /**
2590
2603
  * Refreshes a registered chart, re-resolving its data and re-invoking
2591
- * its render method.
2604
+ * its render method, in the element with that id: this engine's own
2605
+ * container, or — so one engine can keep a page of charts — an engine of
2606
+ * its own for that element, made with this engine's options and kept
2607
+ * (so a zoom survives the next refresh) while the element is the same.
2592
2608
  * @param {string} id Chart container ID
2593
2609
  */
2594
2610
  refreshChart(id) {
@@ -2596,7 +2612,46 @@ export class RenderEngine {
2596
2612
  const config = this.registry[id];
2597
2613
  const container = document.getElementById(id);
2598
2614
  if (!container) return;
2615
+ const target = this._engineFor(id, container);
2616
+ if (target !== this) target.fitToContainer();
2617
+ target._drawRegistered(config);
2618
+ }
2619
+
2620
+ /** Every registered chart, refreshed. */
2621
+ refreshAll() {
2622
+ for (const id of Object.keys(this.registry ?? {})) this.refreshChart(id);
2623
+ }
2624
+
2625
+ /**
2626
+ * Set the data registered charts read by `dataPath`, and refresh them all —
2627
+ * a live page's poll: `engine.setData(await fetchMetrics())`.
2628
+ * @param {object} data
2629
+ */
2630
+ setData(data) {
2631
+ this.setGlobalData(data);
2632
+ this.refreshAll();
2633
+ }
2634
+
2635
+ /** The engine that draws into `container` for registered chart `id`. @private */
2636
+ _engineFor(id, container) {
2637
+ if (container === this.container) return this;
2638
+ this._childEngines ??= new Map();
2639
+ let engine = this._childEngines.get(id);
2640
+ if (engine?.container !== container) {
2641
+ engine = this._newEngine(container);
2642
+ this._childEngines.set(id, engine);
2643
+ }
2644
+ engine.globalData = this.globalData;
2645
+ return engine;
2646
+ }
2647
+
2648
+ /** An engine for another container, with this one's options. @private */
2649
+ _newEngine(container) {
2650
+ return new RenderEngine(container, this.options);
2651
+ }
2599
2652
 
2653
+ /** Draw a registration (`{type, options}`) with this engine. @private */
2654
+ _drawRegistered(config) {
2600
2655
  // Catalog keys ('bar', 'heatmap', 'graph', 'archimate', ...) resolve
2601
2656
  // through the registries to their real method name; railroadDiagram
2602
2657
  // isn't a registry entry, so `type` already IS the method name there.
@@ -2613,6 +2668,7 @@ export class RenderEngine {
2613
2668
  this[methodName](config.options.nodes, config.options.edges || [], config.options);
2614
2669
  } else {
2615
2670
  const data = config.options.data || this.resolvePath(config.options.dataPath, config.options.dataMapper);
2671
+ if (data === undefined) return; // registered before its data has arrived
2616
2672
  this[methodName](data, config.options);
2617
2673
  }
2618
2674
  }