umlimator 0.21.0 → 0.23.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.
@@ -44,7 +44,7 @@ export interface MapNode {
44
44
  /** A typed element in schema version 2's envelope (docs/diagrams/model.md §1): a node, a member, a typed edge, a
45
45
  * fragment. Nodes, edges, members and fragments share one id space; an id never contains "->". A typed edge is an
46
46
  * Element with `from` and `to`, which may name a member. A typed view is a MapView with `diagram` (uml.class), and may
47
- * have `subject`, `direction` ('right' | 'down'), `notation`, `present`, `fragments` and `timeline`. umlimator/all draws `uml.class` (0.18.0), `uml.object` and `uml.package` (0.19.0), `uml.sequence` and `uml.communication` (0.20.0), `uml.use-case`, `uml.activity`, `uml.state-machine` and `uml.interaction-overview` (0.21.0); other types draw as plain cards.
47
+ * have `subject`, `direction` ('right' | 'down'), `notation`, `present`, `fragments` and `timeline`. umlimator/all draws `uml.class` (0.18.0), `uml.object` and `uml.package` (0.19.0), `uml.sequence` and `uml.communication` (0.20.0), `uml.use-case`, `uml.activity`, `uml.state-machine` and `uml.interaction-overview` (0.21.0), `uml.component`, `uml.deployment` and `uml.composite-structure` (0.22.0), `uml.profile` and `uml.timing` (0.23.0); other types draw as plain cards.
48
48
  * From 0.19.0 the class, object and package types draw a shown package that owns something the view shows as a box holding it (`parent`), and
49
49
  * qualify names (`sales::Order`) from the nearest shown owner, the view's `subject`, or the top. */
50
50
  export interface Element {
@@ -157,6 +157,82 @@ export interface UmlTransitionProps { trigger?: string; guard?: string; effect?:
157
157
  export type UmlInteractionOverviewElementType = 'uml.InitialNode' | 'uml.ActivityFinalNode' | 'uml.FlowFinalNode' | 'uml.DecisionNode'
158
158
  | 'uml.MergeNode' | 'uml.ForkNode' | 'uml.JoinNode' | 'uml.InteractionUse' | 'uml.Interaction' | 'uml.Comment';
159
159
  export type UmlInteractionOverviewLineType = 'uml.ControlFlow' | 'uml.CommentLink';
160
+ /** The component diagram's vocabulary (a view with `diagram: 'uml.component'`, umlimator/all from 0.22.0;
161
+ * docs/conformance/uml-component.md). A component's `parent` is a component (drawn nested, a box) or a package; its
162
+ * ports (a class's too) are members, drawn on its border. A provided interface is always an `uml.Interface` node, joined
163
+ * to its provider (a component, a class or a port) by a realization and to its requirers by usages; `present.<id>.as:
164
+ * 'ball'` draws it as a ball, a usage to it ending in a socket. */
165
+ export type UmlComponentElementType = 'uml.Component' | 'uml.Interface' | 'uml.Artifact' | 'uml.Package' | 'uml.Comment';
166
+ export type UmlComponentMemberType = 'uml.Port';
167
+ export type UmlComponentLineType = 'uml.InterfaceRealization' | 'uml.ComponentRealization' | 'uml.Usage' | 'uml.Connector' | 'uml.Dependency'
168
+ | 'uml.Manifestation' | 'uml.CommentLink';
169
+ /** A port's props: its type, drawn `name : Type` beside it. */
170
+ export interface UmlPortProps { valueType?: string; stereotypes?: string[]; [key: string]: unknown }
171
+ /** A connector's props: in a component view its kind (an assembly when left out); in a composite structure view, an end
172
+ * through a part (UML's partWithPort): `ends.from.part` or `ends.to.part` names the part, `from` or `to` the port of its type. */
173
+ export interface UmlConnectorProps { kind?: 'assembly' | 'delegation'; ends?: { from?: { part?: string }; to?: { part?: string } }; stereotypes?: string[]; [key: string]: unknown }
174
+ /** What a component or composite structure view shows of one element, beside UmlPresent: an interface as a ball; a port's
175
+ * side; on a part, each of its type's ports' sides. */
176
+ export interface UmlStructurePresent { as?: 'ball'; side?: 'left' | 'right' | 'top' | 'bottom'; ports?: Record<string, { side?: 'left' | 'right' | 'top' | 'bottom' }> }
177
+ /** The deployment diagram's vocabulary (a view with `diagram: 'uml.deployment'`, umlimator/all from 0.22.0;
178
+ * docs/conformance/uml-deployment.md). A node, device or execution environment that holds something shown is a box drawn
179
+ * as a cube; an artifact or deployment specification whose `parent` is one is deployed on it, a `uml.Deployment` line
180
+ * from it to one is the other form. */
181
+ export type UmlDeploymentElementType = 'uml.Node' | 'uml.Device' | 'uml.ExecutionEnvironment' | 'uml.Artifact' | 'uml.DeploymentSpecification'
182
+ | 'uml.Component' | 'uml.Comment';
183
+ export type UmlDeploymentLineType = 'uml.Deployment' | 'uml.Manifestation' | 'uml.CommunicationPath' | 'uml.Dependency' | 'uml.CommentLink';
184
+ /** The composite structure diagram's vocabulary (a view with `diagram: 'uml.composite-structure'`, umlimator/all from
185
+ * 0.22.0; docs/conformance/uml-composite-structure.md). The view's `subject` (a class, component or collaboration, listed
186
+ * in `nodeIds`) is drawn as a box holding its parts (`uml.Property` members) and collaboration uses (`parent` the
187
+ * subject); a part shows the ports of the class or component its `valueType` names by label, each drawn under the id
188
+ * `<part>-><port>`. An open collaboration (listed in `nodeIds`) is a dashed ellipse round its roles. A role binding runs
189
+ * from a part to a collaboration use, its `label` the role. */
190
+ export type UmlCompositeStructureElementType = 'uml.Class' | 'uml.Component' | 'uml.Collaboration' | 'uml.CollaborationUse' | 'uml.Comment';
191
+ export type UmlCompositeStructureMemberType = 'uml.Property' | 'uml.Port';
192
+ export type UmlCompositeStructureLineType = 'uml.Connector' | 'uml.RoleBinding' | 'uml.CommentLink';
193
+ /** A part's (or role's) props: the class or component it is, by label; its multiplicity; shared parts are drawn dashed. */
194
+ export interface UmlPartProps { valueType?: string; multiplicity?: string; aggregation?: 'composite' | 'shared'; stereotypes?: string[]; [key: string]: unknown }
195
+ /** A collaboration use's props: the collaboration it uses, by label. */
196
+ export interface UmlCollaborationUseProps { collaboration?: string; stereotypes?: string[]; [key: string]: unknown }
197
+ /** Any UML element's tagged values (0.23.0): by stereotype (as written in `stereotypes`, `Profile::name` when qualified, or
198
+ * a required stereotype's name), then by tag name, each value as text. Checked against the profiles applied to the
199
+ * element's packages (docs/conformance/uml-profile.md); edited in the properties panel on every typed view. */
200
+ export type UmlTags = Record<string, Record<string, string>>;
201
+ /** The props any UML element may hold from 0.23.0: its stereotypes (a name, or `Profile::name`) and their tags. */
202
+ export interface UmlStereotypedProps { stereotypes?: string[]; tags?: UmlTags; [key: string]: unknown }
203
+ /** The profile diagram's vocabulary (a view with `diagram: 'uml.profile'`, umlimator/all from 0.23.0;
204
+ * docs/conformance/uml-profile.md). A stereotype's and a metaclass reference's `parent` is a profile; an enumeration's
205
+ * (a tag's type) may be. A metaclass reference's `label` names a UML metaclass (Class, Property, Classifier, …). A
206
+ * stereotype's tag definitions are `uml.Property` members (`name : Type [multiplicity] = default`). Any element that
207
+ * applies a stereotype may be shown, with a compartment of tags for each stereotype it has. */
208
+ export type UmlProfileElementType = 'uml.Profile' | 'uml.Stereotype' | 'uml.MetaclassReference' | 'uml.Enumeration' | 'uml.Package' | 'uml.Comment';
209
+ export type UmlProfileMemberType = 'uml.Property' | 'uml.EnumerationLiteral';
210
+ /** An extension from a stereotype to a metaclass reference; a profile application from a package to a profile; a
211
+ * generalization between stereotypes. */
212
+ export type UmlProfileLineType = 'uml.Extension' | 'uml.ProfileApplication' | 'uml.Generalization' | 'uml.CommentLink';
213
+ /** A stereotype's props: abstract (applied through a specialization). */
214
+ export interface UmlStereotypeProps { isAbstract?: boolean; [key: string]: unknown }
215
+ /** An extension's props: required (the stereotype is applied to every element of the metaclass in its profile's scope). */
216
+ export interface UmlExtensionProps { isRequired?: boolean; [key: string]: unknown }
217
+ /** The timing diagram's vocabulary (a view with `diagram: 'uml.timing'`, umlimator/all from 0.23.0;
218
+ * docs/conformance/uml-timing.md). Lifelines are model nodes the view lists in `nodeIds`, top to bottom; messages are inline
219
+ * in the view's `edges`; changes and constraints are in its `timeline`. `present.<lifeline>.as: 'values'` draws a value
220
+ * lifeline. The scale is derived, never stored. */
221
+ export type UmlTimingElementType = 'uml.Lifeline';
222
+ export type UmlTimingLineType = 'uml.Message';
223
+ /** A timing lifeline's props: what it represents, and its states, top to bottom (ignored by the sequence and
224
+ * communication types). */
225
+ export interface UmlTimingLifelineProps { represents?: string; states?: string[]; stereotypes?: string[]; tags?: UmlTags; [key: string]: unknown }
226
+ /** A timing message's props: a sort of call, async or reply, and when it is sent and received, in the view's unit. */
227
+ export interface UmlTimingMessageProps { sort?: 'call' | 'async' | 'reply'; replyTo?: string; sentAt?: number; receivedAt?: number; [key: string]: unknown }
228
+ /** A change of state on a lifeline at a time; an element of the model's id space, drawn by `at`. */
229
+ export interface UmlTimingChange { id: string; lifeline: string; at: number; state: string }
230
+ /** A duration constraint from one change to a later one, or a time constraint on one change. */
231
+ export interface UmlTimingConstraint { id: string; type: 'uml.DurationConstraint' | 'uml.TimeConstraint'; from: string; to?: string; label?: string }
232
+ /** A timing view's `timeline`: the ruler's unit (optional), its changes in any order, its constraints. */
233
+ export interface UmlTimeline { unit?: string; changes?: UmlTimingChange[]; constraints?: UmlTimingConstraint[] }
234
+ /** What a timing view shows of a lifeline, in its `present`: drawn as a value lifeline. */
235
+ export interface UmlTimingPresent { as?: 'values' }
160
236
  /** What a line means, drawn as its colour: calls (the default), feeds (hands work on), triggers (suggests, offers, fires),
161
237
  * state (reads or writes a file, board or config: a thinner line, a muted label, no flow) and guard (a hook steps in).
162
238
  * Colours are --um-w-<kind>. An edge's `style` (emphasis, dashed, security) wins over its kind's colour. */
package/lib/umlimator.js CHANGED
@@ -123,7 +123,7 @@ function registerDiagramType(name, def = {}) {
123
123
  reroute is no registry field: reroute: false goes in the dg the type's own layout passes to layoutTyped (src/typed/layout.js).
124
124
  Stage 4's (notes/stage-4.md §12.1, §15): edits.rename(model, view, id, text); a hook's { pick, then }; name: false and
125
125
  opens on a palette entry; a line's cut, its label's width (150); rows(model, view, id) and word(model, id), the PDF's.
126
- Stage 5's: a line's fromPort, toPort, inner, portRank, a box's pad, round, a view's labelGaps (src/typed/layout.js);
126
+ Stage 5's: a line's fromPort, toPort, inner, portRank, a box's pad, round, head, a view's labelGaps (src/typed/layout.js);
127
127
  edits.member, edits.key (src/typed/edit.js). */
128
128
 
129
129
  /* A view's type: the map for a view with no diagram, the registered type, or null for one this release does not draw */
@@ -131,7 +131,9 @@ const diagramOf = view => TYPES.get(view && view.diagram != null ? view.diagram
131
131
 
132
132
  /* What only a registered type's geometry uses, installed at load time by the files under src/typed/, which only
133
133
  lib/umlimator-all.js holds (tools/build.mjs): rows, rooms and routes in layoutPass, a typed line's ends in svgOf, the
134
- gestures of edit.js it takes over (edit). Empty in umlimator.js, where no view is laid out but as a map (risks.md C5) */
134
+ gestures of edit.js it takes over (edit), diff()'s words for typed models (diff, order: src/uml/diff.js). Empty in
135
+ umlimator.js, where no view is laid out but as a map (risks.md C5).
136
+ Stage 6's: across(model), validate()'s rules on the whole model; fields(model, x), more panel fields (notes/stage-6.md §5) */
135
137
  const TYPED = {};
136
138
 
137
139
  const propRefs = type => Object.assign({}, ...[...TYPES.values()].map(T => T.refs && Object.hasOwn(T.refs, type) && T.refs[type]));
@@ -248,6 +250,7 @@ function refs(m, ids, only) {
248
250
  arr(v.nodes).forEach((n, j) => element(n, [...at, 'nodes', j], false));
249
251
  arr(v.edges).forEach((e, j) => { if (has(e)) say([...at, 'edges', j], 'view'); else element(e, [...at, 'edges', j], true); });
250
252
  arr(v.fragments).forEach((f, j) => element(f, [...at, 'fragments', j], false));
253
+ ['changes', 'constraints'].forEach(k => arr(isObj(v.timeline) && v.timeline[k]).forEach((c, j) => isObj(c) && ['lifeline', 'from', 'to'].forEach(f => { if (has(c[f])) say([...at, 'timeline', k, j, f], 'end'); })));
251
254
  [['layout', 'pin'], ['present', 'present'], ['nodeOverrides', 'override']].forEach(([k, kind]) => Object.keys(isObj(v[k]) ? v[k] : {}).forEach(id => { if (has(id)) say([...at, k, id], kind); }));
252
255
  if (has(v.subject)) say([...at, 'subject'], 'subject');
253
256
  if (!only) ['parent', 'node'].forEach(k => { if (has(v[k])) say([...at, k], 'parent'); }); // a view opened from a card
@@ -374,9 +377,9 @@ function run(m, c, gone) {
374
377
  const take = id => { const r = at.get(id); if (!r || now.has(id)) return; now.add(id);
375
378
  arr(r.x.members).forEach(y => isObj(y) && take(y.id)); arr(kids.get(id)).forEach(take); }; // what it owns goes with it
376
379
  take(c.id);
377
- // lines to what is gone go too, lines to those lines, and what a type's refs cascade (a reply with its call)
380
+ // lines to what is gone go too, lines to those lines, a timeline's items, and what a type's refs cascade (a reply with its call)
378
381
  const cascade = x => Object.entries(propRefs(x.type)).some(([p, how]) => how === 'cascade' && [].concat(dig(x, p)).some(id => now.has(id)));
379
- for (let more = true; more;) { more = false; for (const [k, o] of at) if (!now.has(k) && (o.kind === 'edge' && (now.has(o.x.from) || now.has(o.x.to)) || cascade(o.x))) { take(k); more = true; } }
382
+ for (let more = true; more;) { more = false; for (const [k, o] of at) if (!now.has(k) && (o.kind === 'edge' && (now.has(o.x.from) || now.has(o.x.to)) || o.kind === 'time' && [o.x.lifeline, o.x.from, o.x.to].some(i => now.has(i)) || cascade(o.x))) { take(k); more = true; } }
380
383
  // what stays in something gone goes to its nearest owner left; an operand's messages to its fragment's parent
381
384
  const above = id => { const r = at.get(id); return r && (r.x.parent ?? (r.owner && above(r.owner.id))); };
382
385
  for (const [k, o] of at) if (!now.has(k) && now.has(o.x.parent)) { let p = o.x.parent;
@@ -1035,7 +1038,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
1035
1038
  // A typed view's boxes (notes/stage-2.md §3.2) take the same code as a map's: their title is the type's (`title`), and a box
1036
1039
  // grows to hold it; laid out turned (direction down), the 24 px header room goes left, the side that becomes the top
1037
1040
  const familyDefs = dg.containers || [], tagged = n => map && (n.author || n.k === 'm-ext'), TB = !map && dg.turned; // tagged: a name tag hangs under the card
1038
- const headW = f => textW(!map && f.title || f.label || f.id, 11) + 16; // a box's title, as the audit measures it
1041
+ const headW = f => textW(!map && f.title || f.label || f.id, !map && f.head ? 12 : 11) + 16; // a box's title, as the audit measures it (two lines: at 12 px)
1039
1042
  const framesNow = () => { const cache = Object.create(null); // a box may be named 'constructor'
1040
1043
  const frameOf = (f, trail = new Set()) => {
1041
1044
  if (cache[f.id]) return cache[f.id];
@@ -1048,7 +1051,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
1048
1051
  const p = 12, x = Math.min(...rects.map(r => r.x)) - p - (TB ? 24 : 0), y = Math.min(...rects.map(r => r.y)) - p - (TB ? 0 : 24);
1049
1052
  const b = {...f, x, y, w:Math.max(...rects.map(r => r.x+r.w)) + p - x, h:Math.max(...rects.map(r => r.y+r.h)) + p - y};
1050
1053
  if (!map) b[TB ? 'h' : 'w'] = Math.max(b[TB ? 'h' : 'w'], headW(f) + 16);
1051
- return cache[f.id] = dg.typed ? TYPED.frame(b) : b;
1054
+ return cache[f.id] = dg.typed ? TYPED.frame(b, TB) : b;
1052
1055
  };
1053
1056
  const out = familyDefs.map(f => frameOf(f)).filter(Boolean);
1054
1057
  if (map) out.forEach(f => { // the box fits its title and its badges, which are tabs on its top border, at the right
@@ -1450,7 +1453,7 @@ function layoutPass(dg, map, room, last) { // room: { x: { col: px }, y: { row:
1450
1453
  const runRouting = order => {
1451
1454
  routed.length = 0; reserved.length = 0; placed.length = 0; stats = {};
1452
1455
  (dg.blocks || []).forEach(b => { reserved.push(b); placed.push(b); });
1453
- families.forEach(f => { const h = TB ? { x: f.x + 3, y: f.y + 8, w: 22, h: headOf(f) } : { x: f.x + 8, y: f.y + 3, w: headOf(f), h: 22 }; reserved.push(h); placed.push(h);
1456
+ families.forEach(f => { const t = 22 + (!map && f.head || 0), h = TB ? { x: f.x + 3, y: f.y + 8, w: t, h: headOf(f) } : { x: f.x + 8, y: f.y + 3, w: headOf(f), h: t }; reserved.push(h); placed.push(h);
1454
1457
  placed.push(...[{ x: f.x, y: f.y - 3, w: f.w, h: 6 }, { x: f.x, y: f.y + f.h - 3, w: f.w, h: 6 }, { x: f.x - 3, y: f.y, w: 6, h: f.h }, { x: f.x + f.w - 3, y: f.y, w: 6, h: f.h }].map(r => ({ ...r, band: true }))); }); // lane headers and arrows stay clear
1455
1458
  edges.filter(e => e.fixed).forEach(e => { routed.push(e); if (e.under || e[OWN_ROUTE]) { placeLabel(e); stats[e.how] = (stats[e.how] || 0) + 1; return; } if (e.lbl) { placed.push(e.lbl); reserved.push({ x: e.lbl.x - 3, y: e.lbl.y - 3, w: e.lbl.w + 6, h: e.lbl.h + 6 }); } stats.comb = (stats.comb || 0) + 1; });
1456
1459
  order.forEach(e => {
@@ -1687,7 +1690,7 @@ function routeAudit(geo, where, only, refs) { // only: a set of edges; counts ju
1687
1690
  if (f.parent && (!parent || !inside(parent,f))) say(`${where}: family "${f.label}" sticks out of its parent`, f);
1688
1691
  (f.nodeIds || []).forEach(id => { const n = geo.byId[id]; if (n && !inside(f,n)) say(`${where}: family "${f.label}" does not contain "${n.label}"`, f, n); });
1689
1692
  families.filter(g => g !== f && g.parent === f.parent && g.id > f.id).forEach(g => { if (hit(f,g,-1)) say(`${where}: families "${f.label}" and "${g.label}" overlap`, f, g); });
1690
- geo.edges.forEach((e,i) => { if (segHit({x:f.x+8,y:f.y+3,w:Math.min(f.w-16,textW(f.label || f.id,11)+16),h:22},i)) say(`${where}: family "${f.label}": header crossed by edge ${name(e)}`, f, e); });
1693
+ geo.edges.forEach((e,i) => { if (segHit({x:f.x+8,y:f.y+3,w:Math.min(f.w-16,textW(f.label || f.id,11)+16),h:22+(!geo.map&&f.head||0)},i)) say(`${where}: family "${f.label}": header crossed by edge ${name(e)}`, f, e); });
1691
1694
  geo.edges.forEach((e,i) => { if (segs[i].some(([a,b]) => { const v = a[0] === b[0], c = v ? a[0] : a[1], p = v ? [a[1], b[1]] : [a[0], b[0]];
1692
1695
  const lo = Math.max(Math.min(...p), v ? f.y : f.x), hi = Math.min(Math.max(...p), v ? f.y + f.h : f.x + f.w);
1693
1696
  return hi - lo > 40 && (v ? [f.x, f.x + f.w] : [f.y, f.y + f.h]).some(u => Math.abs(c - u) < 8); })) say(`${where}: edge ${name(e)} runs along the border of family "${f.label}"`, e, f); });
@@ -1702,7 +1705,7 @@ const box = n => ({ x: n.x, y: n.y, w: n.w, h: n.h });
1702
1705
  function overlapIssues(geo, where, refs) {
1703
1706
  const issues = [], nodes = geo.nodes, say = teller(issues, refs);
1704
1707
  const inside = (a, b) => a.x >= b.x - 1 && a.y >= b.y - 1 && a.x + a.w <= b.x + b.w + 1 && a.y + a.h <= b.y + b.h + 1;
1705
- const holds = (a, b) => (a.holder && inside(b, a)) || (b.holder && inside(a, b)); // a holder (a sequence's frame) may hold what it meets, not cross it
1708
+ const holds = (a, b) => (a.holder && inside(b, a)) || (b.holder && inside(a, b)) || !geo.map && ((a.owner || 0) === b.id || (b.owner || 0) === a.id); // a holder (a sequence's frame) may hold what it meets, not cross it; a port its owner
1706
1709
  for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) if (hit(box(nodes[i]), box(nodes[j]), -1) && !holds(nodes[i], nodes[j])) say(`${where}: nodes "${nodes[i].label}" and "${nodes[j].label}" overlap`, nodes[i], nodes[j]);
1707
1710
  return issues;
1708
1711
  }
@@ -2614,9 +2617,10 @@ export function validate(model) {
2614
2617
  out.push(...typedProblems(m).filter(p => !(p.level === 'error' && said.has(p.path))));
2615
2618
  views.forEach((v, i) => { const at = `views[${i}].diagram`; // a well-formed name of a type this release does not draw (M13)
2616
2619
  if (typedView(v) && !diagramOf(v) && !out.some(p => p.path === at)) warn(at, `diagram "${v.diagram}" is not drawn by this release: its nodes are drawn as plain cards`); });
2617
- const once = new Set(); // each view's type's rules, a problem once; a validate(model) that takes no view, once a type
2618
- views.filter(typedView).forEach(v => { const T = diagramOf(v); if (!T || !T.validate || once.has(T)) return; if (T.validate.length < 2) once.add(T);
2619
- T.validate(m, v).forEach(p => { const k = JSON.stringify([p.level, p.path, p.message]); if (!once.has(k)) { once.add(k); out.push(p); } }); });
2620
+ const once = new Set(), say = p => { const k = JSON.stringify([p.level, p.path, p.message]); if (!once.has(k)) { once.add(k); out.push(p); } };
2621
+ // each view's type's rules, a problem once; a validate(model) that takes no view, once a type
2622
+ views.filter(typedView).forEach(v => { const T = diagramOf(v); if (!T || !T.validate || once.has(T)) return; if (T.validate.length < 2) once.add(T); T.validate(m, v).forEach(say); });
2623
+ if (TYPED.across) arrOf(TYPED.across(m)).forEach(say);
2620
2624
  }
2621
2625
  return out;
2622
2626
  }
@@ -2773,14 +2777,16 @@ export function diff(before, after) {
2773
2777
  if (h === 'removed') said.push(`removes ${name(n.id)}`);
2774
2778
  if (h === 'changed') { const [o, x] = was.get(n), keys = diffKeys([o, x]), rest = keys.filter(k => k !== 'label'), p0 = o.parent ?? null, p1 = x.parent ?? null;
2775
2779
  if (keys.join() === 'parent' && p0 !== p1 && A.schemaVersion === 2 && out.schemaVersion === 2) return said.push(`moves ${name(n.id)} ${p1 != null ? 'into ' + name(p1) : 'out of ' + name(p0)}`); // a re-own (schema version 2; absent or null is no owner)
2780
+ const t = TYPED.diff && TYPED.diff(o, x, name, [A, out]); if (t) return said.push(...t); // a typed element's own words (src/uml/diff.js)
2776
2781
  said.push((keys.includes('label') ? `renames ${o.label || o.id} to ${x.label || x.id}` : `changes ${name(n.id)}`) + (rest.length ? ` (${rest.join(', ')})` : '')); } });
2777
2782
  lines.forEach(e => { const h = how.get(e); if (!h || cardHow.get(e.from) === h && h !== 'changed' || cardHow.get(e.to) === h && h !== 'changed') return;
2778
2783
  const ends = `${name(e.from)} to ${name(e.to)}`, lab = e.label ? ` (${e.label})` : '';
2779
2784
  if (h === 'added') said.push(`adds a line from ${ends}${lab}`);
2780
2785
  if (h === 'removed') said.push(`removes the line from ${ends}${lab}`);
2781
- if (h === 'changed') { const [o, x] = was.get(e);
2786
+ if (h === 'changed') { const [o, x] = was.get(e), t = TYPED.diff && TYPED.diff(o, x, name, [A, out]); if (t) return said.push(...t);
2782
2787
  said.push(`changes the line from ${ends} (${diffKeys([o, x]).map(k => k === 'label' ? `label ${o.label ?? '-'} → ${x.label ?? '-'}` : k).join(', ')})`); } });
2783
2788
  moved.forEach(([verb, id, v]) => said.push(`${verb} ${name(id)} ${verb === 'puts' ? 'in' : 'out of'} the view ${v.title || v.id}`));
2789
+ if (TYPED.order) TYPED.order(A, out, views, V0, said, touched, name);
2784
2790
 
2785
2791
  // the views that show a change: a card or line the diff marks, or a card it put in or took out
2786
2792
  const changed = views.filter(v => v && (touched.has(v.id) || arr(v.nodeIds).some(id => cardHow.has(id)) || linesOf(out, v).some(e => how.has(e)))).map(v => v.id);
@@ -2834,9 +2840,11 @@ const walkFrom = (edges, start, back) => { const seen = new Set([start]); // bre
2834
2840
  for (let q = [start]; q.length;) { const u = q.shift(); edges.forEach(e => { const [a, b] = back ? [e.to, e.from] : [e.from, e.to]; if (a === u && !seen.has(b)) { seen.add(b); q.push(b); } }); }
2835
2841
  return seen; };
2836
2842
 
2843
+ const viewType = (model, viewId) => model && model.schemaVersion === 2 && diagramOf(arrOf(model.views).find(v => v && v.id === viewId));
2837
2844
  /** Every card and line on a path from one card to another, following lines forward: the cards both reachable from `from`
2838
2845
  * and leading to `to`, and the lines between them. [] when no path joins them. */
2839
2846
  export function paths(model, viewId, from, to) {
2847
+ const T = viewType(model, viewId); if (T && T.paths) return T.paths(model, viewId, from, to); // a type's own walk (a sequence's, in time)
2840
2848
  const { ids, edges } = lineGraph(model, viewId);
2841
2849
  if (!ids.includes(from) || !ids.includes(to)) return [];
2842
2850
  const F = walkFrom(edges, from), B = walkFrom(edges, to, true), on = id => F.has(id) && B.has(id);
@@ -2861,7 +2869,7 @@ export function reach(model, viewId, from, options = {}) {
2861
2869
  /** A view in plain sentences: its cards and their kinds, its boxes, what each card's lines do (in run order on a sequence
2862
2870
  * view), and the cards with no line. The same model gives the same words: a text alternative, docs, or an AI's context. */
2863
2871
  export function describe(model, viewId) {
2864
- const T = model && model.schemaVersion === 2 && diagramOf(arrOf(model.views).find(v => v && v.id === viewId)); if (T && T.describe) return T.describe(model, viewId); // a type's own words (1.9)
2872
+ const T = viewType(model, viewId); if (T && T.describe) return T.describe(model, viewId); // a type's own words (1.9)
2865
2873
  const { ctx, v, ids, edges } = lineGraph(model, viewId), boxes = (v.containers || []).filter(c => c && c.id);
2866
2874
  const name = id => ctx.N[id] ? ctx.nodeIn(id, viewId).label || id : (boxes.find(c => c.id === id) || {}).label || id;
2867
2875
  const list = xs => xs.length < 3 ? xs.join(' and ') : xs.slice(0, -1).join(', ') + ' and ' + xs[xs.length - 1];
@@ -3642,7 +3650,7 @@ function typedEditor(api) {
3642
3650
  function props() {
3643
3651
  const at = elementsOf(model()), id = selId(at), e = id && at.get(id);
3644
3652
  if (!e) return '<h3>Properties</h3><p class="um-note">Select a card or a line</p>';
3645
- const x = e.x, P = isRec(x.props) ? x.props : {}, schema = arrOf(isRec(def().props) ? def().props[x.type] : null).filter(f => isRec(f) && typeof f.key === 'string');
3653
+ const x = e.x, P = isRec(x.props) ? x.props : {}, F = a => arrOf(a).filter(f => isRec(f) && typeof f.key === 'string'), own = F(isRec(def().props) ? def().props[x.type] : null), schema = [...own, ...F(TYPED.fields && TYPED.fields(model(), x))];
3646
3654
  const off = can('rename') ? '' : ' disabled'; // the editable flags: a field the host does not offer is shown, to read
3647
3655
  const text = (f, v, name, more = '') => `<label>${esc(name)}<input data-f="${esc(f)}" data-was="${esc(v)}" value="${esc(v)}"${more}${off}></label>`;
3648
3656
  let h = `<h3>Properties${x.type != null ? ': ' + esc(x.type) : ''}</h3>` + text('label', x.label ?? '', 'Name');
@@ -3657,7 +3665,7 @@ function typedEditor(api) {
3657
3665
  const offer = Array.isArray(os) && [{ id: '', label: 'none' }, ...os.filter(o => isRec(o) && o.id != null && !under(o.id))];
3658
3666
  if (offer && x.parent != null && !offer.some(o => o.id === x.parent)) offer.push({ id: x.parent, label: (at.get(x.parent) || { x: {} }).x.label });
3659
3667
  if (offer) h += `<label>Owner<select data-f="owner" data-owner${can('move') ? '' : ' disabled'}>${offer.map(o => `<option value="${esc(o.id)}"${o.id === (x.parent ?? '') ? ' selected' : ''}>${esc(o.label ?? o.id)}</option>`).join('')}</select></label>`;
3660
- const rest = schema.length ? [] : Object.entries(P); // no schema: what it holds, to read
3668
+ const rest = own.length ? [] : Object.entries(P); // no schema: what it holds, to read
3661
3669
  if (rest.length) h += '<dl>' + rest.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(typeof v === 'string' ? v : JSON.stringify(v))}</dd>`).join('') + '</dl>';
3662
3670
  const M = def().member;
3663
3671
  if (e.kind === 'node' && !(M && M.holds && !M.holds(x))) { // a comment holds none
@@ -3843,7 +3851,7 @@ export function render(container, model, options = {}) {
3843
3851
  const emitSel = (again = false) => {
3844
3852
  const now = JSON.stringify(picked()); if (now === said && !again) return; said = now;
3845
3853
  say(!sel ? 'Nothing selected' : (sel.type === 'node' ? nameOf(sel.id) : sel.type === 'edge' ? (e => e ? `Line from ${nameOf(e.from)} to ${nameOf(e.to)}` : 'Line')(geo.edges[edgeIndex(sel)])
3846
- : `Box ${(svg.querySelector(`.family[data-family="${CSS.escape(sel.id)}"] .family-title`) || {}).textContent || sel.id}`) + (more.length ? `, ${more.length + 1} ${picked().every(r => r.type === 'node') ? 'cards ' : ''}selected` : ', selected'));
3854
+ : `Box ${[...svg.querySelectorAll(`.family[data-family="${CSS.escape(sel.id)}"] .family-title`)].map(t => t.textContent).join(' ') || sel.id}`) + (more.length ? `, ${more.length + 1} ${picked().every(r => r.type === 'node') ? 'cards ' : ''}selected` : ', selected'));
3847
3855
  emit('select', { target: copyRef(sel), targets: picked() });
3848
3856
  };
3849
3857
  const edgeRef = e => ({ type: 'edge', ids: edgeRefIds(e), view: cur });
@@ -4288,7 +4296,7 @@ export function render(container, model, options = {}) {
4288
4296
  if (ref.type === 'node') { const n = geo.byId[ref.id]; return n ? { x: n.x + 50, y: n.y + n.h / 2 - 13, w: n.w - 58, h: 26 } : null; }
4289
4297
  if (ref.type === 'edge') { const e = geo.edges[edgeIndex(ref)]; if (!e) return null; const L = e.lbl, m = e.pts[Math.floor(e.pts.length / 2)];
4290
4298
  return L ? { x: L.x - 8, y: L.y - 2, w: L.w + 16, h: L.h + 4 } : { x: m[0] - 60, y: m[1] - 12, w: 120, h: 24 }; }
4291
- const f = ref.type === 'container' && (geo.families || []).find(x => x.id === ref.id); return f ? { x: f.x + 8, y: f.y + 2, w: Math.min(f.w - 16, 220), h: 24 } : null; };
4299
+ const f = ref.type === 'container' && (geo.families || []).find(x => x.id === ref.id); return f ? { x: f.x + 8, y: f.y + 2 + (f.head || 0), w: Math.min(f.w - 16, 220), h: 24 } : null; };
4292
4300
  function editor(where, text, done, ref = null) { // where: a box in map units (a new card's), or ref names what the field sits over
4293
4301
  if (naming) naming.close(false);
4294
4302
  const inp = document.createElement('input');