@peter.prib/renderengine 0.2.6 → 0.2.7

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 (49) hide show
  1. package/README.md +4 -0
  2. package/REFERENCE.md +180 -2
  3. package/dist/renderengine-workspace.umd.js +8704 -5310
  4. package/dist/renderengine.umd.js +5675 -3398
  5. package/examples/archimate.html +68 -66
  6. package/examples/data-model.html +223 -13
  7. package/package.json +1 -1
  8. package/src/bian.js +182 -0
  9. package/src/charting/DiagramEditor.js +5 -26
  10. package/src/charting/DiagramTypeRegistry.js +8 -2
  11. package/src/charting/ModelCheckDialog.js +63 -0
  12. package/src/charting/ModelChecker.js +26 -0
  13. package/src/charting/ViewTabStrip.js +94 -81
  14. package/src/charting/datamodel/BianLandscapeReader.js +185 -0
  15. package/src/charting/datamodel/BianSemanticApiReader.js +183 -0
  16. package/src/charting/datamodel/DataModel.js +356 -10
  17. package/src/charting/datamodel/DataModelChecker.js +92 -0
  18. package/src/charting/datamodel/DataModelCompared.js +127 -0
  19. package/src/charting/datamodel/DataModelFileFormat.js +33 -0
  20. package/src/charting/datamodel/DataModelPerspective.js +45 -0
  21. package/src/charting/datamodel/DataType.js +108 -0
  22. package/src/charting/datamodel/EnumerationType.js +51 -0
  23. package/src/charting/datamodel/ServiceDomain.js +177 -0
  24. package/src/charting/datamodel/ServiceLandscape.js +104 -0
  25. package/src/charting/datamodel/StructuredType.js +63 -0
  26. package/src/charting/datamodel/index.js +10 -0
  27. package/src/charting/diagramModel/MergeModelCommand.js +43 -37
  28. package/src/charting/diagramModel/OpenViewList.js +75 -0
  29. package/src/charting/diagramModel/ViewSetChecker.js +22 -34
  30. package/src/charting/diagrams/DataModelRelationshipStrategy.js +12 -4
  31. package/src/charting/diagrams/EdgeGeometry.js +578 -553
  32. package/src/charting/diagrams/GraphDiagram.js +11 -1
  33. package/src/charting/diagrams/NodeGeometry.js +169 -158
  34. package/src/charting/glyphs/PersonGlyph.js +24 -23
  35. package/src/index.js +2666 -2664
  36. package/src/workspace/EditorShell.js +166 -67
  37. package/src/workspace/PanelLayout.js +56 -0
  38. package/src/workspace/layout/TabsNode.js +205 -191
  39. package/src/workspace/parts/ArchimateCanvasPart.js +5 -7
  40. package/src/workspace/parts/DataModelPart.js +339 -29
  41. package/src/workspace/parts/DiagramEditorPart.js +2 -8
  42. package/src/workspace/parts/EditorPart.js +461 -407
  43. package/src/workspace/parts/ModelFile.js +216 -187
  44. package/src/workspace/parts/ModelsPart.js +483 -192
  45. package/src/workspace/storage/BackupFolder.js +145 -0
  46. package/src/workspace/storage/KeyValueStore.js +92 -0
  47. package/src/workspace/storage/ModelLibrary.js +65 -0
  48. package/src/workspace/workArea.js +417 -413
  49. package/src/yaml/YamlReader.js +372 -0
package/README.md CHANGED
@@ -39,6 +39,10 @@ other browsers download the file instead.
39
39
 
40
40
  - **Open and save Archi files** (`.archimate`, and archives that include pictures). Save writes back to the file you
41
41
  opened; Save As picks a new one. Unsaved changes are marked (•), and the page asks before you leave or close them.
42
+ - **Kept in the browser, backed up to the cloud.** Every model is kept in the browser as you edit it, and those open
43
+ reopen next time, with no file saved. File ▸ This browser backs them all up to a folder you choose once: pick one
44
+ that OneDrive, Google Drive or Dropbox syncs and the backups reach the cloud (each named with the date and time,
45
+ plus one under its plain name); browsers that cannot write to a folder download them instead.
42
46
  - **Import a spreadsheet** into the open model: choose the name, type, id, documentation, property and "related
43
47
  to" columns; existing elements are updated, not duplicated (Archi's elements.csv imports as it is).
44
48
  - **draw.io export**: every view a page, in draw.io's ArchiMate shapes, with the elements' properties.
package/REFERENCE.md CHANGED
@@ -723,7 +723,7 @@ types — see `ARCHITECTURE.md`):
723
723
  `'cylinder'`, ...) and `labelPosition: 'below'` keeps the shape's size. In Chen a relationship's diamond holds
724
724
  its name and sits midway between its two entities when both have `x`/`y`, or when something is drawn there (an entity, a
725
725
  note, another diamond) the nearest clear spot across the line (`RelationshipAsNodeStrategy.spotFor`,
726
- `DataModelDiagram.boxOf`); its lines have no arrowheads.
726
+ `DataModelDiagram.boxOf`); its lines have no arrowheads; when no spot near the line is clear, the least covered one.
727
727
  Each level and notation has a `label` for menus (`'Conceptual'`, `"Crow's Foot"`).
728
728
  **Levels of their own, and how they map.** A `DataModel` (`src/charting/datamodel/`, exported from the package
729
729
  root) keeps each level as its own schema, a `LevelSchema` (`model.level('conceptual' | 'logical' | 'physical')`):
@@ -782,8 +782,137 @@ types — see `ARCHITECTURE.md`):
782
782
  `DataModelContext.fields(model, context?)` and `DataModelContext.specFrom(values, model, context?)` (meanings, dates,
783
783
  and rules a line each, `the rule | the concepts, by name`).
784
784
 
785
+ **BIAN service domains.** `BianNotation` (`src/bian.js`, exported from the package root) holds BIAN's vocabulary:
786
+ the nineteen functional patterns, each with its generic artifact, description, creating action term and behaviour
787
+ qualifier type (`BianNotation.functionalPattern('Fulfill')`, a `BianFunctionalPattern`), the seventeen action terms
788
+ in their four groups, and the v14.0 behaviour qualifier types. Each pattern's artifact, creating term and qualifier
789
+ type are as BIAN's v14 model has them (its service landscape portal: the artifact a pattern accesses, the qualifier
790
+ type the artifact aggregates, the creating term the pattern aggregates), the names control records have as its
791
+ release 14 semantic APIs do (Fulfill's is a *Facility*, the guide's *Arrangement* kept in `formerArtifacts`); none
792
+ is `inferred`. BIAN spells *Analyze* (the guide's *Analyse* still finds it); `BianNotation.findPattern(text)` finds
793
+ one by its name in any case, and `BianNotation.patternOfControlRecord(name)` by the artifact a control record's name
794
+ ends with (`'CurrentAccountFacility'`: Fulfill). A `ServiceDomain` is a kind of perspective (`kind: 'service-domain'`; a perspective spec's
795
+ `kind` picks its class, `DataModelPerspective.from`, `register`): its business area and domain, the `asset` it manages,
796
+ its functional `pattern`, its `controlRecord` (a logical entity; `controlRecordName` is the asset and the pattern's
797
+ artifact, *Current Account Facility*) and `qualifiers` (`[{ name, type?, entity }]`). `serviceOperations()` gives
798
+ its semantic API as BIAN names it (*initiate Current Account Facility | Interest*, `BianNotation.serviceOperations`).
799
+ Seen through at the logical level it shows its control record and qualifiers whatever they realise, coloured and
800
+ saying what they are on hover, with a note of its service operations; a perspective kind adds to a view through
801
+ `alsoShows(level)`, `decorate(entity, level)` and `notes(level)`. Model ▸ **New service domain…** makes one
802
+ (`ServiceDomain.fields`, `specFrom`: the pattern from BIAN's, qualifiers a line each, `name | type | logical entity`).
803
+ The data-model example's second base, *Retail banking (BIAN)*, has six. See `BIAN_ALIGNMENT.md` for the design.
804
+
805
+ **Ownership and the model check.** As in BIAN, each business object (a logical entity) is owned by one service
806
+ domain: a perspective owns what `ownedEntities()` gives (none for a plain one; a service domain owns its control
807
+ record, its qualifiers' entities and those it names in `owns`, its dialog's *Also owns*), `model.ownersOf(id)`
808
+ says who owns one, and the logical level shows it on hover (*owned by*). `model.checker()` is a `DataModelChecker`
809
+ (a `ModelChecker`, the base `ViewSetChecker` extends too): a business object owned by more than one service domain
810
+ (`owned-twice`), owned by none once any are owned (`unowned`), a service domain without a control record or with
811
+ one, or a qualifier, that is no logical entity, or with a pattern BIAN lacks, and a mapping joining an entity that
812
+ is not there. Model ▸ **Check model…** lists the findings in the `ModelCheckDialog` the diagram editors' check uses;
813
+ choosing one shows it (the entity's level, the service domain seen through, the mapping view).
814
+ The ontology base keeps BIAN's **Agreement** (the contract) apart from its **Arrangement** (the terms a product is
815
+ provided on), and has **Product Feature** and **Condition**; its concepts say which BIAN business object they are
816
+ (`details.bian`, on hover).
817
+
818
+ **The service landscape.** Level ▸ **Service landscape** (offered once a model has a service domain;
819
+ `DataModel.LANDSCAPE`) draws the service domains as BIAN draws its landscape: a column per business area, a frame
820
+ per business domain in it, a box per service domain coloured by its functional pattern (grey for one BIAN lacks),
821
+ its pattern, control record and asset on hover. Those with no area or domain go under *No business area* /
822
+ *No business domain*. It is laid out from that structure each time (`ServiceLandscape.view(serviceDomains)`), so
823
+ a box dragged there is not kept; double-click one to see the model through it at the logical level. Mapping and
824
+ the landscape are `DataModel.OTHER_VIEWS`, the views that are no level, which the Level choice and `view()` read.
825
+
826
+ **Reading BIAN's landscape.** Model ▸ **Read service domains from a BIAN landscape…** reads an ArchiMate model
827
+ (an Archi file or the Exchange Format; `BianLandscapeReader.parse`). In BIAN's v14 model a business area is a
828
+ Grouping marked *BusinessArea*, composed of business domains (Capabilities marked *BusinessDomain*), composed of
829
+ service domains (Capabilities marked *ServiceDomain*), each realized by its functional pattern; other Groupings
830
+ aggregate service domains as overlays (a value chain, a release wave). Where the file keeps the stereotypes (Archi
831
+ specializations, or a *Stereotype* property) they say which is which: a service domain's business domain is the
832
+ container marked so and its area the one holding that; a Capability in a business domain is a service domain though
833
+ unmarked (BIAN leaves a few so); of copies of one, the placed one is it. Without stereotypes it goes by structure: a
834
+ Capability nothing sits in is a service domain, its nearest container its business domain, the outermost its area.
835
+ What holds what: composition before aggregation, then a box drawn inside another in a view. A service domain's
836
+ pattern is the one realizing it (an element named as a BIAN pattern), else its *Functional Pattern* property; its
837
+ asset its *Asset Type* property. The types, relationships, stereotypes and property keys are options of
838
+ `new BianLandscapeReader({...})`. Run over BIAN's v14 model it places all 344 service domains the model places in
839
+ their area and domain, and gives 349 of 350 their pattern; by structure alone it would take 820 Capabilities for
840
+ service domains.
841
+ `model.serviceDomainsCommand(specs, source)` brings them in as one undo step: a new one is added; one already
842
+ there (by id, else by name) keeps its control record, qualifiers and the rest, taking its area and domain from
843
+ the landscape, and its asset and pattern where it has none. `examples/bian-landscape-sample.xml` is a small
844
+ excerpt in that shape to try it with (not BIAN's landscape, which is BIAN's to publish).
845
+
846
+ ```javascript
847
+ const specs = new BianLandscapeReader().read(BianLandscapeReader.parse(xml));
848
+ model.history.execute(model.serviceDomainsCommand(specs, '"bian.xml"'));
849
+ model.view({ level: DataModel.LANDSCAPE }); // { entities, folders, elementFolders, ... }
850
+ ```
851
+
852
+ **Data types.** A model's `dataTypes` are the types its attributes have, each defined once: a `DataType` (a plain
853
+ named type, `basedOn` another), an `EnumerationType` (`values: [{ name, description? }]`) or a `StructuredType`
854
+ (`fields: [{ name, type?, description?, iso20022? }]`), its `kind` in a spec picking the class (`DataType.from`,
855
+ `register`), as BIAN publishes its data types and DAMA-DMBOK2 calls domains. An attribute names one by its `type`
856
+ (the type's id, else its name in any case: `model.typeOf(attribute)`); a plain type (`VARCHAR(20)`) names none.
857
+ On hover an entity lists its attributes' data types; `model.usesOf(id)` says where one is used. Level ▸ **Data types**
858
+ draws each as a box with a row per value or field, joined to the types a structure's fields are. Model ▸ **Data
859
+ types** ▸ makes one of each kind, edits or deletes each (a dialog per kind: values a line each, `value | meaning`;
860
+ fields `name | type`). They are saved, compared and merged as the rest of the model is (`DataTypesCompared`), and
861
+ the model check reports an enumeration with no values or a value twice, two types of one name, and a type nothing uses.
862
+
863
+ **ISO 20022.** An entity, an attribute or a data type may say which ISO 20022 type it is (`iso20022: 'CurrencyCode'`;
864
+ `DataType.iso20022Url(name)` its page in the ISO 20022 standards repository). On hover an entity lists its own and
865
+ its attributes' (an attribute's own, else its data type's).
866
+
867
+ **Reading a BIAN semantic API.** BIAN publishes a semantic API per service domain (OpenAPI files, YAML; the plain ones
868
+ and those extended with ISO 20022, at github.com/bian-official/public, Apache-2.0). Model ▸ **Read a BIAN service
869
+ domain (semantic API)…** reads one (`.yaml`, `.yml` or `.json`; `BianSemanticApiReader`): its control record and
870
+ behaviour qualifiers (the operations' `CR - …` and `BQ - …` tags) become logical entities with the attributes their
871
+ schemas have, the qualifiers joined to the control record; the schemas those attributes are become data types (an
872
+ `enum` an enumeration, each value's meaning from BIAN's description; an object a structure; else a plain type), as
873
+ deep as `depth` (1); a class the model already has as a logical entity stays a reference to it; the ISO 20022 version
874
+ gives each attribute its ISO 20022 type. The service domain is named by the API's title, its pattern and asset by its
875
+ control record's name. `model.bringCommand({ entities, relationships, dataTypes, serviceDomains }, label)` brings
876
+ it in as one undo step: what is new is added; an entity or data type there by name stays and is used; only the data
877
+ types the new entities need come (and the types those need); a service domain there keeps what it has (as for a
878
+ landscape) and gains the behaviour qualifiers it lacks; one that would come out the same is left alone. Every one
879
+ of release 14's 258 APIs reads, in both versions.
880
+
881
+ ```javascript
882
+ const read = new BianSemanticApiReader().read(BianSemanticApiReader.parse(yamlText));
883
+ model.history.execute(model.bringCommand(read, 'Read Current Account'));
884
+ model.typeOf(model.level('logical').entity('current-account-facility').attributes[1]); // EnumerationType
885
+ ```
886
+
887
+ `YamlReader.parse(text)` (`src/yaml/YamlReader.js`) is the dependency-free reader it uses: block mappings and
888
+ sequences, quoted and plain scalars (typed as YAML 1.2's core schema has them), block scalars with chomping, flow
889
+ collections, comments; it refuses anchors, aliases and tags with the line they are on.
890
+
891
+ ```js
892
+ const sd = model.addPerspective({ kind: 'service-domain', name: 'Current Account', asset: 'Current Account', pattern: 'Fulfill',
893
+ controlRecord: 'current-account', qualifiers: [{ name: 'Interest', entity: 'interest' }] });
894
+ sd.controlRecordName; // 'Current Account Facility'
895
+ sd.serviceOperations()[3].name; // 'initiate Current Account Facility'
896
+ ```
897
+
898
+ **Compared and merged.** `model.compareWith(other)` is a `ModelComparison`, as for ArchiMate models, kind by kind
899
+ (`model.comparedKinds()`): each level's entities (`LevelEntitiesCompared`; where a box is drawn does not count) and
900
+ relationships (`LevelRelationshipsCompared`; one with no id known by its ends and name, and needing them), the
901
+ mappings (`MappingsCompared`, a pair each, needing its two entities), contexts and perspectives (a perspective
902
+ needing the context it is read in). `comparison.mergeCommand(keys)` brings the chosen differences and what they
903
+ need, as one step on `model.history`.
904
+
905
+ **Saved, kept and backed up.** A data model is a file of its own, `.datamodel.json` (`DataModelFileFormat`: its
906
+ `name`, levels, mappings, layouts, perspectives and contexts, `model.toJSON()`; the older one-entity form reads
907
+ too). It knows when it changed since it was saved (`model.edited()`, `markSaved()`, `isDirty()`, `onEdit(fn)`):
908
+ a perspective or context made, changed or removed, a box or note moved.
909
+
785
910
  **In the workbench:** `DataModelPart` (type `data-model`, `RenderEngineWorkspace.DataModelPart`) shows one
786
- model: Model ▸ New / Edit / Delete perspective and context (those dialogs), Perspective and Context selects (when
911
+ model: File ▸ Open…, Save, Save As… (Ctrl+O, Ctrl+S; `part.file`, the `ModelFile` editors use, • while changed,
912
+ asking before the page is left with changes unsaved), and hosted by a `ModelsPart` (`editorType: 'data-model'`)
913
+ New ▸ (New model, From base: …), Duplicate model, Close model and This browser ▸ (with `keep`: kept in this browser
914
+ as it changes, reopened next time, backed up to a folder chosen once, as the modeller's models are); Edit ▸ Undo
915
+ 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
787
916
  it has some; a perspective brings its context, and the context's scope shows beside them), Level (Conceptual,
788
917
  Logical, Physical, and Mapping), Notation, and Export ▸ PNG / SVG. A box or note dragged stays on the level it
789
918
  was dragged on (the mapping view's in its own layout). `examples/data-model.html` (`npm run datamodel`) uses every
@@ -1359,6 +1488,34 @@ mine.history.execute(comparison.mergeCommand(['view:id-extra'])); // the view,
1359
1488
  mine.history.undo();
1360
1489
  ```
1361
1490
 
1491
+ **A model tab's menu.** Right-click a model's tab for what can be done with that model (`models.tabMenuItems(part)`):
1492
+ Save and Save As… (an editor that saves files), Duplicate model, Compare with ▸ and Merge from ▸, This browser ▸
1493
+ (with `keep`), Close model and Close other models. The work area hands a tab's right-click to its `tabMenu` option
1494
+ (`(partId, event) => shown`), through `TabsNode`'s `ctx.onTabMenu`; with none, the browser's own menu shows.
1495
+
1496
+ **Bases, and copies.** A `ModelsPart` spec's `bases` are reference models kept as given: each made from its own copy
1497
+ of its spec when the workbench is first drawn (`models.bases()`, parts never opened, edited or kept, so an open model
1498
+ sharing the spec's data cannot reach them). File ▸ New ▸ **From base: …** (`models.newFromBase(i)`,
1499
+ `models.baseItems()`) opens a model as a copy of one; Tools ▸ **Compare with** and **Merge from** offer them, after
1500
+ the other open models (`models.toolItems(part)`); File ▸ **Duplicate model** (`models.duplicate(part)`) opens a copy
1501
+ of the model in front, written and read back through its own file format. A copy, from a base or duplicated, is named
1502
+ "… (copy)" (`ModelsPart.copyName`, the suffix once).
1503
+
1504
+ **Kept in this browser, and backed up.** With `keep: true` in its spec, a `ModelsPart` keeps every model it holds
1505
+ in this browser, so work outlives the page without saving a file: a moment after each edit (`ModelsPart.KEEP_DELAY`)
1506
+ and at once when imported, each in its editor's own file format (`part.keepEntry()`: the file's format, else the
1507
+ part's save format), in a `ModelLibrary` over an IndexedDB store (`KeyValueStore`: `IndexedDbStore`, or a
1508
+ `MemoryStore` where the browser has none). Those open when the page closed open again next time
1509
+ (`models.ready` resolves once they have), in place of the starting model if it was left untouched; a tab closed
1510
+ is kept but not reopened. File ▸ **This browser** has **Open a kept model…**, **Stop keeping this model**, **Back
1511
+ up kept models now** and **Backup folder…**. Backups (`BackupFolder`, after au-accounts-app's) go to a folder chosen
1512
+ once and remembered (the File System Access API, in Chrome and Edge, from `file://` too): choose one that OneDrive,
1513
+ Google Drive or Dropbox syncs and they reach the cloud, with no sign-in in the page. Each is written stamped with
1514
+ the date and time (`BackupFolder.stamped('shop.archimate')` gives `shop-20261015-093012.archimate`), so they gather
1515
+ as a history, and under its plain name too (`keepUnstamped`); where a folder cannot be written (Firefox, Safari,
1516
+ permission refused) they download instead. `backups.log()` records each (newest first, `LOG_MAX`), and
1517
+ `backups.daysSinceLast()` how long ago the last was. `examples/archimate.html` keeps its models.
1518
+
1362
1519
  ```js
1363
1520
  ws.addParts([{ id: 'models', type: 'models', editorType: 'archimate-canvas', newModel: { analysis: true },
1364
1521
  models: [{ id: 'modeller', title: 'Sample model', elements: [...], views: [...] }] }]);
@@ -1945,6 +2102,11 @@ parts.children.length; // 3: front, top and side faces
1945
2102
  context, and the template draws its parts as SVG (`glyphToSvg`; `glyphElement(part)` gives the tag and
1946
2103
  attributes, by the SVG renderer's rules) and attaches lines to the glyph's `outline(ctx)`. Every glyph
1947
2104
  has an outline: its box by default, the real shape for round, skewed and pointed ones.
2105
+ `graphDiagram` (and the data-model diagram drawn by it) does the same: a line ends where it meets the node's
2106
+ glyph outline (`NodeGeometry.outline(shape, size)`, crossed by `EdgeGeometry.rayPolygonT`), so it reaches a wide
2107
+ ellipse or product box rather than stopping short on a circle as wide as the node; a shape no glyph draws keeps
2108
+ that circle. Centred initials stand in for a name drawn below the shape: a node whose name is inside it gets none
2109
+ (`GraphDiagram.showsInitials(node)`).
1948
2110
 
1949
2111
  **ArchiMate in the template editor.** `ArchimateElementTemplate(instance, model, {nodeOptions})` and
1950
2112
  `ArchimateEdge(instance, view)` put an `ArchimateView` into a `DiagramEditor`. Neither keeps a copy:
@@ -2196,6 +2358,22 @@ its `fileFormats()`, none by default, then the image export), and its editor dis
2196
2358
  import goes through `replaceModel(model)`, which a subclass follows in `modelReplaced(model)`.
2197
2359
  `DiagramEditorPart`, `ArchimateEditorPart` and `ArchimateCanvasPart` extend it.
2198
2360
 
2361
+ **Tabs, as a browser has them.** A part's tabs are the views open (`part.openViews`, an `OpenViewList` over its
2362
+ model), not every view: a model opened or imported starts with its first view only, so an Archi file of many
2363
+ views does not fill the strip. Going to a view (choosing it in the model tree, a search match, a new view)
2364
+ opens its tab; a tab's **×** closes the tab and leaves the view in the model (one tab always stays, and the
2365
+ tab in front closed brings its neighbour); **+** after the tabs makes something new (`newItems()`: a view, or
2366
+ a menu of a view, a canvas, from a template). `part.viewTabs({ orientation })` builds the strip
2367
+ (`ViewTabStrip` with `canClose` and `onAdd`). Drawing every view for the HTML report opens none
2368
+ (`openViews.while(fn)`). The spec's `openViews` (view ids) chooses which open at the start.
2369
+
2370
+ **Panels that fold and resize.** The side panels (the model tree, the palette and analysis) have a splitter
2371
+ on their inner edge: drag it to resize the panel, drag it nearly shut or double-click it to fold the panel to
2372
+ a stub that names it, and click the stub to open it at its width again. `part.layout` (a `PanelLayout`:
2373
+ `resize(name, width)`, `toggle(name)`, `widthOf(name)`) keeps the widths and folds across renders; the spec's
2374
+ `layout` (`{ side: { width, collapsed }, aside: {...} }`) sets them at the start. `EditorShell` draws them
2375
+ when given a `layout` and stub `labels`.
2376
+
2199
2377
  `DiagramEditorPart` (workspace bundle) puts this together: `MenuBar` (File creates and deletes
2200
2378
  diagrams; Settings has the grid and line-style levels), the palette, a `ViewTabStrip` (right-click
2201
2379
  a tab for its **Properties…**: name, documentation and the diagram's line style; **Duplicate**;