@meltemi/diagrams-editor 1.9.0 → 1.10.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 (112) hide show
  1. package/dist/add-icons.d.ts +44 -0
  2. package/dist/add-icons.d.ts.map +1 -0
  3. package/dist/add-icons.js +178 -0
  4. package/dist/add-icons.js.map +1 -0
  5. package/dist/arrange.d.ts.map +1 -1
  6. package/dist/arrange.js +0 -1
  7. package/dist/arrange.js.map +1 -1
  8. package/dist/asset-files.d.ts +46 -0
  9. package/dist/asset-files.d.ts.map +1 -0
  10. package/dist/asset-files.js +79 -0
  11. package/dist/asset-files.js.map +1 -0
  12. package/dist/c4.d.ts +91 -1
  13. package/dist/c4.d.ts.map +1 -1
  14. package/dist/c4.js +358 -9
  15. package/dist/c4.js.map +1 -1
  16. package/dist/canvas/keyboard.d.ts +3 -1
  17. package/dist/canvas/keyboard.d.ts.map +1 -1
  18. package/dist/canvas/keyboard.js +4 -0
  19. package/dist/canvas/keyboard.js.map +1 -1
  20. package/dist/canvas/palette.d.ts +7 -5
  21. package/dist/canvas/palette.d.ts.map +1 -1
  22. package/dist/canvas/palette.js +13 -9
  23. package/dist/canvas/palette.js.map +1 -1
  24. package/dist/canvas/protocol.d.ts +223 -7
  25. package/dist/canvas/protocol.d.ts.map +1 -1
  26. package/dist/canvas/protocol.js +58 -1
  27. package/dist/canvas/protocol.js.map +1 -1
  28. package/dist/canvas/routes.d.ts +17 -0
  29. package/dist/canvas/routes.d.ts.map +1 -1
  30. package/dist/canvas/routes.js +37 -0
  31. package/dist/canvas/routes.js.map +1 -1
  32. package/dist/canvas/turning.d.ts +7 -0
  33. package/dist/canvas/turning.d.ts.map +1 -1
  34. package/dist/canvas/turning.js +17 -3
  35. package/dist/canvas/turning.js.map +1 -1
  36. package/dist/clipboard.d.ts.map +1 -1
  37. package/dist/clipboard.js +61 -9
  38. package/dist/clipboard.js.map +1 -1
  39. package/dist/effects.d.ts +5 -0
  40. package/dist/effects.d.ts.map +1 -1
  41. package/dist/effects.js +28 -22
  42. package/dist/effects.js.map +1 -1
  43. package/dist/exports.d.ts +19 -11
  44. package/dist/exports.d.ts.map +1 -1
  45. package/dist/exports.js +54 -19
  46. package/dist/exports.js.map +1 -1
  47. package/dist/icons.d.ts +2 -0
  48. package/dist/icons.d.ts.map +1 -1
  49. package/dist/icons.js +39 -6
  50. package/dist/icons.js.map +1 -1
  51. package/dist/index.d.ts +6 -3
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +4 -1
  54. package/dist/index.js.map +1 -1
  55. package/dist/inspector.d.ts.map +1 -1
  56. package/dist/inspector.js +229 -97
  57. package/dist/inspector.js.map +1 -1
  58. package/dist/messages/en.d.ts +55 -20
  59. package/dist/messages/en.d.ts.map +1 -1
  60. package/dist/messages/en.js +59 -26
  61. package/dist/messages/en.js.map +1 -1
  62. package/dist/messages/es.d.ts.map +1 -1
  63. package/dist/messages/es.js +47 -43
  64. package/dist/messages/es.js.map +1 -1
  65. package/dist/messages/index.d.ts +43 -17
  66. package/dist/messages/index.d.ts.map +1 -1
  67. package/dist/messages/keys.d.ts.map +1 -1
  68. package/dist/messages/keys.js +2 -0
  69. package/dist/messages/keys.js.map +1 -1
  70. package/dist/messages/services.d.ts +6 -0
  71. package/dist/messages/services.d.ts.map +1 -1
  72. package/dist/messages/services.js +8 -2
  73. package/dist/messages/services.js.map +1 -1
  74. package/dist/move-file.d.ts +32 -0
  75. package/dist/move-file.d.ts.map +1 -0
  76. package/dist/move-file.js +58 -0
  77. package/dist/move-file.js.map +1 -0
  78. package/dist/node.d.ts +1 -1
  79. package/dist/node.d.ts.map +1 -1
  80. package/dist/node.js.map +1 -1
  81. package/dist/projects.d.ts +65 -1
  82. package/dist/projects.d.ts.map +1 -1
  83. package/dist/projects.js +349 -1
  84. package/dist/projects.js.map +1 -1
  85. package/dist/services/actions.d.ts.map +1 -1
  86. package/dist/services/actions.js +11 -1
  87. package/dist/services/actions.js.map +1 -1
  88. package/dist/services/completion.d.ts.map +1 -1
  89. package/dist/services/completion.js +46 -6
  90. package/dist/services/completion.js.map +1 -1
  91. package/dist/services/hover.d.ts.map +1 -1
  92. package/dist/services/hover.js +3 -2
  93. package/dist/services/hover.js.map +1 -1
  94. package/dist/services/navigation.d.ts +1 -1
  95. package/dist/services/navigation.d.ts.map +1 -1
  96. package/dist/services/navigation.js +2 -1
  97. package/dist/services/navigation.js.map +1 -1
  98. package/dist/session.d.ts +56 -10
  99. package/dist/session.d.ts.map +1 -1
  100. package/dist/session.js +650 -38
  101. package/dist/session.js.map +1 -1
  102. package/dist/targets.d.ts +25 -4
  103. package/dist/targets.d.ts.map +1 -1
  104. package/dist/targets.js +103 -7
  105. package/dist/targets.js.map +1 -1
  106. package/dist/text-places.d.ts.map +1 -1
  107. package/dist/text-places.js +55 -1
  108. package/dist/text-places.js.map +1 -1
  109. package/dist/themes.d.ts.map +1 -1
  110. package/dist/themes.js +11 -1
  111. package/dist/themes.js.map +1 -1
  112. package/package.json +16 -16
package/dist/inspector.js CHANGED
@@ -1,10 +1,10 @@
1
- import { ARROWHEADS, BOX_SHAPES, CORNERS, BUILT_IN_THEMES, DEFAULT_THEME, FONT_WEIGHTS, TEXT_ALIGNS, DIRECTIONS, ELEMENT_ONLY_PROPERTIES, ICON_POSITIONS, LABEL_POSITIONS, LAYOUT_MODES, LINE_STYLES, PATTERNS, PORTS, PRESETS, PROFILES, RELATION_ONLY_PROPERTIES, ROUTINGS, STYLE_PROPERTIES, THEME_COLOR_TOKENS, takesRotation, takesShape, } from "@meltemi/diagrams-model";
1
+ import { ARROWHEADS, BOX_SHAPES, CORNERS, BUILT_IN_THEMES, DEFAULT_THEME, FONT_WEIGHTS, TEXT_ALIGNS, VERTICAL_ALIGNS, DIRECTIONS, ELEMENT_ONLY_PROPERTIES, ICON_POSITIONS, LABEL_POSITIONS, LAYOUT_MODES, LINE_STYLES, PATTERNS, PORTS, PRESETS, PROFILES, RELATION_ONLY_PROPERTIES, ROUTINGS, STYLE_PROPERTIES, THEME_COLOR_TOKENS, takesRotation, takesShape, } from "@meltemi/diagrams-model";
2
2
  import { CORNERED_SHAPES, StyleResolver } from "@meltemi/diagrams-layout";
3
3
  import { DEFAULT_DURATIONS, themeDurations } from "@meltemi/diagrams-motion";
4
4
  import { diagramView } from "./analysis.js";
5
5
  import { interiorOf, steadyGroups } from "./arrange.js";
6
6
  import { c4DiagramFields, c4ElementFields, c4FieldOperations, c4RelationFields } from "./c4.js";
7
- import { effectFields, effectLabel, effectOperations, effectsFor } from "./effects.js";
7
+ import { effectFields, effectLabel, effectOperations, effectsFor, selectionAfter, } from "./effects.js";
8
8
  import { drawnIconSize, iconChoice, iconKind } from "./icons.js";
9
9
  import { messages } from "./messages/index.js";
10
10
  import { freshRelationId, namedNotice, namedRelation, styleTargets } from "./targets.js";
@@ -97,12 +97,27 @@ const STYLE_FIELDS = [
97
97
  glyphs: "shape",
98
98
  elements: (kind) => takesShape(kind),
99
99
  },
100
+ // Free boxes and texts: where the text sits across; boxes also where it sits down the shape.
101
+ // The canvas draws the two as one grid of cells.
102
+ {
103
+ name: "textAlign",
104
+ type: "enum",
105
+ options: TEXT_ALIGNS,
106
+ glyphs: "textAlign",
107
+ elements: (kind, diagram) => diagram.type === "free" && (kind === "box" || kind === "text"),
108
+ },
109
+ {
110
+ name: "verticalAlign",
111
+ type: "enum",
112
+ options: VERTICAL_ALIGNS,
113
+ glyphs: "verticalAlign",
114
+ elements: (kind, diagram) => diagram.type === "free" && kind === "box",
115
+ },
100
116
  ];
101
117
  /** Style fields of free diagrams: text. */
102
118
  const FREE_STYLE_FIELDS = [
103
119
  { name: "fontSize", type: "number" },
104
120
  { name: "fontWeight", type: "enum", options: FONT_WEIGHTS.map(String) },
105
- { name: "textAlign", type: "enum", options: TEXT_ALIGNS },
106
121
  ];
107
122
  /** The word for a kind in titles: the interface's, or the kind itself for declared types. */
108
123
  function kindName(kind) {
@@ -127,7 +142,10 @@ const shown = (value) => value === undefined
127
142
  ? value.slice(1)
128
143
  : value;
129
144
  function styleFields(diagram, targets) {
130
- const kinds = new Set(targets.map((t) => t.kind));
145
+ // Free lines take a relation's properties and none of a box's.
146
+ const isLine = (t) => t.kind === "element" && diagram.elementsById.get(t.id)?.kind === "line";
147
+ const lines = targets.length > 0 && targets.every(isLine);
148
+ const kinds = new Set(targets.map((t) => (isLine(t) ? "relation" : t.kind)));
131
149
  const styles = targets.map((t) => writtenStyle(diagram, t.id, t.kind));
132
150
  const fields = diagram.type === "free" ? [...STYLE_FIELDS, ...FREE_STYLE_FIELDS] : STYLE_FIELDS;
133
151
  const labels = messages().style;
@@ -137,10 +155,11 @@ function styleFields(diagram, targets) {
137
155
  !(kinds.has("relation") && ELEMENT_ONLY_PROPERTIES.includes(f.name)) &&
138
156
  !(diagram.type === "sequence" && NOT_IN_SEQUENCES.has(f.name)) &&
139
157
  (f.elements === undefined ||
140
- targets.every((t) => {
141
- const element = t.kind === "element" ? diagram.elementsById.get(t.id) : undefined;
142
- return element !== undefined && f.elements(element.kind, diagram);
143
- })))
158
+ (!lines &&
159
+ targets.every((t) => {
160
+ const element = t.kind === "element" ? diagram.elementsById.get(t.id) : undefined;
161
+ return element !== undefined && f.elements(element.kind, diagram);
162
+ }))))
144
163
  .map((f) => {
145
164
  const values = styles.map((s) => shown(s[f.name]));
146
165
  const same = values.every((v) => v === values[0]);
@@ -170,6 +189,11 @@ function withElementDefaults(fields, view, theme, ids) {
170
189
  const elements = ids.flatMap((id) => view.diagram.elementsById.get(id) ?? []);
171
190
  const shared = (values) => values.length > 0 && values.every((v) => v === values[0]) ? values[0] : undefined;
172
191
  const styles = new StyleResolver(theme, view.diagram);
192
+ // The styles the targets would take without their own `style` statements.
193
+ const inherited = new StyleResolver(theme, {
194
+ ...view.diagram,
195
+ styles: view.diagram.styles.filter((entry) => !ids.includes(entry.target)),
196
+ });
173
197
  const shapeOf = (e) => view.layout?.shapes?.get(e.id) ?? styles.element(e).shape ?? e.shape ?? "rounded";
174
198
  // The radius drawn without one written: the theme's on rounded boxes, notes and frames, the
175
199
  // group radius on groups, a person's or an agent's body radius, and none on the other corners.
@@ -200,8 +224,23 @@ function withElementDefaults(fields, view, theme, ids) {
200
224
  const centrePiece = (e) => iconKind(e.kind, view.diagram) && (iconShaped(e) || position(e) === "center");
201
225
  const some = elements.length > 0;
202
226
  const icon = shared(elements.map((e) => styles.element(e).icon ?? ""));
227
+ // Free lines: no head unless written, straight segments, the theme's corners.
228
+ const lines = some && elements.every((e) => e.kind === "line");
203
229
  const defaults = {
230
+ ...(lines
231
+ ? {
232
+ "style.arrowhead": "none",
233
+ "style.arrowtail": "none",
234
+ "style.routing": "polyline",
235
+ "style.corners": theme.corners,
236
+ }
237
+ : {}),
204
238
  "style.shape": shared(elements.map((e) => view.layout?.shapes?.get(e.id) ?? "rounded")),
239
+ // What applies without the targets' own statements (classes, rules), else a box's text
240
+ // centred in the middle of its shape and a free text's starting on the left: the alignment
241
+ // picker writes a cell's value only where it differs from this.
242
+ "style.textAlign": shared(elements.map((e) => inherited.element(e).textAlign ?? (e.kind === "text" ? "left" : "center"))),
243
+ "style.verticalAlign": shared(elements.map((e) => inherited.element(e).verticalAlign ?? "middle")),
205
244
  "style.radius": shared(elements.map((e) => String(radius(e)))),
206
245
  // A rule's or a class's angle, else upright.
207
246
  "style.rotation": shared(elements.map((e) => String(styles.element(e).rotation ?? 0))),
@@ -255,20 +294,23 @@ function withIcons(model, view, icons) {
255
294
  }
256
295
  /**
257
296
  * The relation's style fields with what applies without a written value: the theme's head at the
258
- * end (calls and replies in sequence diagrams), the end's head at the start of a two-way arrow,
259
- * the diagram's route kind and the theme's corners.
297
+ * end (calls and replies in sequence diagrams; none for a line), the end's head at the start of a
298
+ * two-way arrow, the diagram's route kind and the theme's corners.
260
299
  */
261
300
  function withRelationDefaults(fields, diagram, relation, theme) {
262
301
  const written = writtenStyle(diagram, relation.id ?? relation.key, "relation");
263
- const endDefault = diagram.type === "sequence"
264
- ? relation.line === "dashed" || relation.async
265
- ? "open"
266
- : "triangle"
267
- : theme.arrowhead;
302
+ // A line (`--`, `---`) has no head at either end unless one is written.
303
+ const endDefault = relation.heads === "none"
304
+ ? "none"
305
+ : diagram.type === "sequence"
306
+ ? relation.line === "dashed" || relation.async
307
+ ? "open"
308
+ : "triangle"
309
+ : theme.arrowhead;
268
310
  const end = typeof written.arrowhead === "string" ? written.arrowhead : endDefault;
269
311
  const defaults = {
270
312
  "style.arrowhead": endDefault,
271
- "style.arrowtail": relation.twoWay ? end : "none",
313
+ "style.arrowtail": relation.heads === "both" ? end : "none",
272
314
  "style.routing": diagram.properties.routing,
273
315
  "style.corners": theme.corners,
274
316
  };
@@ -276,11 +318,34 @@ function withRelationDefaults(fields, diagram, relation, theme) {
276
318
  }
277
319
  /** Kinds an element of a profile can become; icons, images and groups keep theirs. */
278
320
  function kindOptions(diagram, element) {
279
- if (element.kind === "group" || element.kind === "icon" || element.kind === "image") {
321
+ if (element.kind === "group" ||
322
+ element.kind === "icon" ||
323
+ element.kind === "image" ||
324
+ element.kind === "line") {
280
325
  return [element.kind];
281
326
  }
327
+ if (element.kind === "note" && diagram.type !== "flow")
328
+ return [element.kind];
282
329
  return diagram.type === "free" ? ["box", "text"] : ["node", "decision", "data", "note", "tag"];
283
330
  }
331
+ /**
332
+ * The element a note is attached to, as a picker of the diagram's other elements (the drawn
333
+ * entity paths in a C4 view), empty for none.
334
+ */
335
+ function attachmentField(diagram, note) {
336
+ const targets = diagram.elements.filter((e) => e.id !== note.id &&
337
+ e.kind !== "line" &&
338
+ // A view attaches to the entities it draws.
339
+ (diagram.type !== "c4" || e.entity !== null));
340
+ return {
341
+ name: "attachedTo",
342
+ label: messages().fields.attachedTo,
343
+ type: "enum",
344
+ value: note.attachedTo,
345
+ placeholder: messages().placeholders.none,
346
+ options: targets.map((e) => e.id),
347
+ };
348
+ }
284
349
  /**
285
350
  * The `layer` and `link` fields of free objects and of every element, and the group an element of
286
351
  * a flow or free diagram is declared in.
@@ -298,7 +363,7 @@ function identityFields(diagram, element) {
298
363
  placeholder: "main",
299
364
  options: diagram.properties.layers,
300
365
  });
301
- if (element) {
366
+ if (element && element.kind !== "line") {
302
367
  // A group cannot go into itself or into a group it holds.
303
368
  const groups = diagram.elements.filter((e) => e.kind === "group" && e.id !== element.id && !e.groupPath.includes(element.id));
304
369
  // A flow node or a free box can become the container of what is drawn on it.
@@ -338,7 +403,8 @@ function identityFields(diagram, element) {
338
403
  }
339
404
  return fields;
340
405
  }
341
- function elementFields(view, element) {
406
+ /** The place and the size of an element, written as `place` and `size` statements. */
407
+ function geometryFields(view, element) {
342
408
  const diagram = view.diagram;
343
409
  const place = diagram.placement.find((p) => p.statement === "place" && p.target === element.id);
344
410
  const size = diagram.placement.find((p) => p.statement === "size" && p.target === element.id);
@@ -351,6 +417,71 @@ function elementFields(view, element) {
351
417
  y: round(box.y + box.height / 2 - (origin?.y ?? 0)),
352
418
  }
353
419
  : null;
420
+ const words = messages().fields;
421
+ return [
422
+ {
423
+ name: "x",
424
+ label: words.x,
425
+ type: "number",
426
+ value: place ? place.x : null,
427
+ ...(center ? { placeholder: String(center.x) } : {}),
428
+ },
429
+ {
430
+ name: "y",
431
+ label: words.y,
432
+ type: "number",
433
+ value: place ? place.y : null,
434
+ ...(center ? { placeholder: String(center.y) } : {}),
435
+ },
436
+ {
437
+ name: "w",
438
+ label: words.width,
439
+ type: "number",
440
+ value: size ? size.width : null,
441
+ ...(box ? { placeholder: String(round(box.width)) } : {}),
442
+ },
443
+ {
444
+ name: "h",
445
+ label: words.height,
446
+ type: "number",
447
+ value: size ? size.height : null,
448
+ ...(box ? { placeholder: String(round(box.height)) } : {}),
449
+ },
450
+ ];
451
+ }
452
+ /** The fields of a note of a C4 view: its own, and none of an entity's. */
453
+ function viewNoteFields(view, element) {
454
+ const words = messages().fields;
455
+ return [
456
+ { name: "id", label: words.id, type: "id", value: element.id },
457
+ {
458
+ name: "kind",
459
+ label: words.kind,
460
+ type: "enum",
461
+ value: element.kind,
462
+ options: [element.kind],
463
+ readonly: true,
464
+ },
465
+ { name: "label", label: words.label, type: "text", value: element.label },
466
+ attachmentField(view.diagram, element),
467
+ {
468
+ name: "classes",
469
+ label: words.classes,
470
+ type: "classes",
471
+ value: element.classes.map((c) => c.name).join(", ") || null,
472
+ },
473
+ {
474
+ name: "url",
475
+ label: words.url,
476
+ type: "text",
477
+ value: element.url,
478
+ placeholder: "https://…",
479
+ },
480
+ ...geometryFields(view, element),
481
+ ];
482
+ }
483
+ function elementFields(view, element) {
484
+ const diagram = view.diagram;
354
485
  const ports = element.ports
355
486
  .map((p) => `${p.name} ${p.side}${Math.round(p.position * 100) === 50 ? "" : ` ${Math.round(p.position * 100)}%`}`)
356
487
  .join(", ");
@@ -370,6 +501,7 @@ function elementFields(view, element) {
370
501
  : element.kind === "image"
371
502
  ? { name: "source", label: words.imageFile, type: "text", value: element.source }
372
503
  : { name: "label", label: words.label, type: "text", value: element.label },
504
+ ...(element.kind === "note" ? [attachmentField(diagram, element)] : []),
373
505
  {
374
506
  name: "description",
375
507
  label: words.description,
@@ -384,6 +516,10 @@ function elementFields(view, element) {
384
516
  },
385
517
  ];
386
518
  fields.push(...identityFields(diagram, element));
519
+ // A free line is its points: no ports, no place, no size.
520
+ if (element.kind === "line") {
521
+ return [...fields, ...styleFields(diagram, [{ id: element.id, kind: "element" }])];
522
+ }
387
523
  if (element.kind !== "group")
388
524
  fields.push({
389
525
  name: "ports",
@@ -392,31 +528,7 @@ function elementFields(view, element) {
392
528
  value: ports || null,
393
529
  placeholder: "in west 30%, out east",
394
530
  });
395
- fields.push({
396
- name: "x",
397
- label: words.x,
398
- type: "number",
399
- value: place ? place.x : null,
400
- ...(center ? { placeholder: String(center.x) } : {}),
401
- }, {
402
- name: "y",
403
- label: words.y,
404
- type: "number",
405
- value: place ? place.y : null,
406
- ...(center ? { placeholder: String(center.y) } : {}),
407
- }, {
408
- name: "w",
409
- label: words.width,
410
- type: "number",
411
- value: size ? size.width : null,
412
- ...(box ? { placeholder: String(round(box.width)) } : {}),
413
- }, {
414
- name: "h",
415
- label: words.height,
416
- type: "number",
417
- value: size ? size.height : null,
418
- ...(box ? { placeholder: String(round(box.height)) } : {}),
419
- });
531
+ fields.push(...geometryFields(view, element));
420
532
  return [...fields, ...styleFields(diagram, [{ id: element.id, kind: "element" }])];
421
533
  }
422
534
  function relationFields(view, relation) {
@@ -683,12 +795,26 @@ export function themeSwatches(theme) {
683
795
  }
684
796
  /** The built-in theme of the same family in the other scheme: `technical.dark` for `technical.light`. */
685
797
  export function counterpartTheme(name) {
686
- const match = /^(technical|editorial|stage)\.(light|dark)$/.exec(name);
798
+ const match = /^(technical|editorial|stage|sketch)\.(light|dark)$/.exec(name);
687
799
  if (!match)
688
800
  return undefined;
689
801
  return `${match[1]}.${match[2] === "light" ? "dark" : "light"}`;
690
802
  }
691
- /** The strip's fields, in the order it shows them; the first `line` is the relation's arrow. */
803
+ /** The effects in the order of the strip's toggles. */
804
+ const EFFECT_ORDER = ["flow", "dash", "pulse", "trace"];
805
+ /** The fields of an effect that is on, in the order of its card. */
806
+ const EFFECT_PROPERTIES = [
807
+ "duration",
808
+ "repeat",
809
+ "delay",
810
+ "direction",
811
+ "particles",
812
+ "color",
813
+ ];
814
+ /**
815
+ * The strip's fields, in the order it shows them; the first `line` is the relation's arrow. The
816
+ * style fields come first, then each effect's toggle with the fields of its card.
817
+ */
692
818
  const STRIP = [
693
819
  "theme",
694
820
  "style.fill",
@@ -704,22 +830,22 @@ const STRIP = [
704
830
  "style.radius",
705
831
  "style.rotation",
706
832
  "style.shape",
833
+ // Free boxes and texts: one grid of cells for the two.
834
+ "style.textAlign",
835
+ "style.verticalAlign",
707
836
  // For elements with an icon only.
708
837
  "style.iconPosition",
709
- "effect.flow",
710
- "effect.flow.duration",
711
- "effect.dash",
712
- "effect.dash.duration",
713
- "effect.pulse",
714
- "effect.pulse.duration",
715
- "effect.trace",
716
- "effect.trace.duration",
838
+ ...EFFECT_ORDER.flatMap((effect) => [
839
+ `effect.${effect}`,
840
+ ...EFFECT_PROPERTIES.map((property) => `effect.${effect}.${property}`),
841
+ ]),
717
842
  ];
718
843
  /**
719
844
  * Marks the fields of the style strip and puts them first: colours with the theme's swatches, the
720
845
  * line, width and arrowhead, the shape and, for elements with an icon (`iconed`), the icon's
721
- * position, the effects with their speed (the theme's default as placeholder), and, for the
722
- * diagram, the theme with its light or dark counterpart.
846
+ * position, the effects as toggles with every field of an effect that is on grouped under its
847
+ * toggle (`group`; the speed with the theme's default as placeholder, the colour with the
848
+ * swatches), and, for the diagram, the theme with its light or dark counterpart.
723
849
  */
724
850
  function withStrip(model, theme, themes, iconed) {
725
851
  const swatches = themeSwatches(theme);
@@ -733,14 +859,15 @@ function withStrip(model, theme, themes, iconed) {
733
859
  const fields = model.fields.map((field) => {
734
860
  if (!inStrip(field.name))
735
861
  return field;
736
- const duration = /^effect\.(flow|dash|pulse|trace)\.duration$/.exec(field.name);
737
- if (duration) {
738
- const effect = duration[1];
739
- return {
740
- ...field,
741
- section: "strip",
742
- placeholder: String(durations[effect] ?? DEFAULT_DURATIONS[effect]),
743
- };
862
+ // A field of an effect that is on belongs to the card of its toggle.
863
+ const grouped = /^(effect\.(flow|dash|pulse|trace))\.(\w+)$/.exec(field.name);
864
+ if (grouped) {
865
+ const effect = grouped[2];
866
+ const placed = { ...field, section: "strip", group: grouped[1] };
867
+ if (grouped[3] === "duration") {
868
+ return { ...placed, placeholder: String(durations[effect] ?? DEFAULT_DURATIONS[effect]) };
869
+ }
870
+ return field.type === "color" ? { ...placed, swatches } : placed;
744
871
  }
745
872
  if (field.type === "color")
746
873
  return { ...field, section: "strip", swatches };
@@ -823,7 +950,9 @@ function inspectFields(analysis, view, selection) {
823
950
  title: titles.target(kindName(element.kind === "group" ? "boundary" : element.kind), id),
824
951
  target: { kind: "element", ids: [id] },
825
952
  fields: [
826
- ...c4ElementFields(diagram, element),
953
+ ...(element.kind === "note" && element.entity === null
954
+ ? viewNoteFields(view, element)
955
+ : c4ElementFields(diagram, element)),
827
956
  ...withElementDefaults(styleFields(diagram, [{ id, kind: "element" }]), view, effectiveTheme(analysis, view), [id]),
828
957
  ...effectFields(diagram, id, "element"),
829
958
  ],
@@ -833,12 +962,11 @@ function inspectFields(analysis, view, selection) {
833
962
  return {
834
963
  title: titles.relation(relation.id ?? `${relation.from} → ${relation.to}`),
835
964
  target: { kind: "relation", ids: [id] },
965
+ // Anonymous relations take styles and effects too: setting one names them in the model.
836
966
  fields: [
837
967
  ...c4RelationFields(diagram, relation),
838
- ...(relation.id
839
- ? withRelationDefaults(styleFields(diagram, [{ id: relation.id, kind: "relation" }]), diagram, relation, effectiveTheme(analysis, view))
840
- : []),
841
- ...(relation.id ? effectFields(diagram, relation.id, "relation") : []),
968
+ ...withRelationDefaults(styleFields(diagram, [{ id: relation.id ?? relation.key, kind: "relation" }]), diagram, relation, effectiveTheme(analysis, view)),
969
+ ...effectFields(diagram, relation.id ?? relation.key, "relation"),
842
970
  ],
843
971
  };
844
972
  }
@@ -862,16 +990,16 @@ function inspectFields(analysis, view, selection) {
862
990
  }
863
991
  if (kind === "event") {
864
992
  const event = diagram.eventsByKey.get(id);
865
- // Notes and fragments with an id take styles.
866
- const styled = event.id !== null && (event.kind === "note" || event.kind === "fragment");
993
+ // Notes and fragments take styles and effects; setting one names an anonymous one.
994
+ const styled = event.kind === "note" || event.kind === "fragment";
867
995
  return {
868
996
  // Fragment operators are notation and stay as written.
869
997
  title: titles.target(event.kind === "fragment" ? event.operator : kindName(event.kind), event.id ?? ""),
870
998
  target: { kind: "event", ids: [id] },
871
999
  fields: [
872
1000
  ...eventFields(diagram, event),
873
- ...(styled ? styleFields(diagram, [{ id: event.id, kind: "event" }]) : []),
874
- ...(styled ? effectFields(diagram, event.id, "event") : []),
1001
+ ...(styled ? styleFields(diagram, [{ id: event.id ?? id, kind: "event" }]) : []),
1002
+ ...(styled ? effectFields(diagram, event.id ?? id, "event") : []),
875
1003
  ],
876
1004
  };
877
1005
  }
@@ -887,10 +1015,8 @@ function inspectFields(analysis, view, selection) {
887
1015
  ...styleFields(diagram, [{ id: relation.id ?? relation.key, kind: "relation" }]),
888
1016
  ]
889
1017
  : relationFields(view, relation), diagram, relation, effectiveTheme(analysis, view)),
890
- // Anonymous relations take effects too: setting one names the relation.
891
- ...(relation.id || !sequence
892
- ? effectFields(diagram, relation.id ?? relation.key, "relation")
893
- : []),
1018
+ // Anonymous relations and messages take effects too: setting one names them.
1019
+ ...effectFields(diagram, relation.id ?? relation.key, "relation"),
894
1020
  ],
895
1021
  };
896
1022
  }
@@ -901,8 +1027,8 @@ function inspectFields(analysis, view, selection) {
901
1027
  const event = diagram.eventsByKey.get(t.id);
902
1028
  if (event.kind === "message")
903
1029
  return [{ id: diagram.relationsByKey.get(t.id)?.id ?? t.id, kind: "relation" }];
904
- return event.id !== null && (event.kind === "note" || event.kind === "fragment")
905
- ? [{ id: event.id, kind: "event" }]
1030
+ return event.kind === "note" || event.kind === "fragment"
1031
+ ? [{ id: event.id ?? t.id, kind: "event" }]
906
1032
  : [];
907
1033
  });
908
1034
  return {
@@ -957,12 +1083,6 @@ export function fieldOperations(analysis, diagramId, ids, field, value) {
957
1083
  return effectOperations(diagram, ids, field, value);
958
1084
  if (field.startsWith("style.")) {
959
1085
  const property = field.slice("style.".length);
960
- for (const id of ids) {
961
- const event = diagram.relationsByKey.has(id) ? undefined : diagram.eventsByKey.get(id);
962
- if (event && event.id === null) {
963
- return { ok: false, reason: reasons.nameToStyle(event.kind) };
964
- }
965
- }
966
1086
  let written = text === "" ? null : text;
967
1087
  // Numbers within the property's bounds, such as a corner radius from 0 to 64.
968
1088
  const spec = STYLE_PROPERTIES[property];
@@ -1007,7 +1127,11 @@ export function fieldOperations(analysis, diagramId, ids, field, value) {
1007
1127
  });
1008
1128
  }
1009
1129
  const notice = namedNotice(resolved.named);
1010
- return { ok: true, operations: ops, ...(notice ? { notice } : {}) };
1130
+ return {
1131
+ ok: true,
1132
+ operations: ops,
1133
+ ...(notice ? { notice, select: selectionAfter(ids, resolved.targets) } : {}),
1134
+ };
1011
1135
  }
1012
1136
  const id = ids[0];
1013
1137
  if (diagram.type === "sequence") {
@@ -1062,6 +1186,9 @@ export function fieldOperations(analysis, diagramId, ids, field, value) {
1062
1186
  ops.push(...steadyGroups(view, [id], parent));
1063
1187
  break;
1064
1188
  }
1189
+ case "attachedTo":
1190
+ ops.push({ kind: "attachNote", diagram: d, note: id, element: text || null });
1191
+ break;
1065
1192
  case "kind":
1066
1193
  if (text && text !== "group")
1067
1194
  ops.push({
@@ -1171,6 +1298,11 @@ export function fieldOperations(analysis, diagramId, ids, field, value) {
1171
1298
  }
1172
1299
  if (relation) {
1173
1300
  const named = relation.id;
1301
+ // What an edit that named the relation first says, and the selection that follows the id.
1302
+ const naming = (target, operations) => {
1303
+ const notice = namedNotice(operations.length > 0 ? [target] : []);
1304
+ return notice ? { notice, select: [target] } : {};
1305
+ };
1174
1306
  switch (field) {
1175
1307
  case "id":
1176
1308
  if (!text)
@@ -1187,7 +1319,7 @@ export function fieldOperations(analysis, diagramId, ids, field, value) {
1187
1319
  relation: target,
1188
1320
  label: text || null,
1189
1321
  });
1190
- break;
1322
+ return { ok: true, operations: ops, ...naming(target, operations) };
1191
1323
  }
1192
1324
  case "description":
1193
1325
  ops.push({
@@ -1208,8 +1340,7 @@ export function fieldOperations(analysis, diagramId, ids, field, value) {
1208
1340
  property: "layer",
1209
1341
  value: text || null,
1210
1342
  });
1211
- const notice = namedNotice(operations.length > 0 ? [target] : []);
1212
- return { ok: true, operations: ops, ...(notice ? { notice } : {}) };
1343
+ return { ok: true, operations: ops, ...naming(target, operations) };
1213
1344
  }
1214
1345
  case "classes":
1215
1346
  ops.push({
@@ -1235,8 +1366,7 @@ export function fieldOperations(analysis, diagramId, ids, field, value) {
1235
1366
  property: "line",
1236
1367
  value: "dotted",
1237
1368
  });
1238
- const notice = namedNotice(operations.length > 0 ? [target] : []);
1239
- return { ok: true, operations: ops, ...(notice ? { notice } : {}) };
1369
+ return { ok: true, operations: ops, ...naming(target, operations) };
1240
1370
  }
1241
1371
  ops.push({
1242
1372
  kind: "setRelationLine",
@@ -1283,17 +1413,19 @@ export function fieldOperations(analysis, diagramId, ids, field, value) {
1283
1413
  case "labelAt": {
1284
1414
  if (number === null || !Number.isFinite(number))
1285
1415
  return { ok: false, reason: reasons.percentage };
1286
- if (!named)
1287
- return { ok: false, reason: reasons.nameToMoveLabel };
1288
- const entry = diagram.placement.find((p) => p.statement === "label" && p.target === named);
1289
- ops.push({
1416
+ // An anonymous relation is named first: its label is placed by the relation's id.
1417
+ const { id: target, operations } = namedRelation(diagram, relation);
1418
+ const entry = named
1419
+ ? diagram.placement.find((p) => p.statement === "label" && p.target === named)
1420
+ : undefined;
1421
+ ops.push(...operations, {
1290
1422
  kind: "setLabelAnchor",
1291
1423
  diagram: d,
1292
- relation: named,
1424
+ relation: target,
1293
1425
  at: number,
1294
1426
  offset: entry?.offset ?? { x: 0, y: 0 },
1295
1427
  });
1296
- break;
1428
+ return { ok: true, operations: ops, ...naming(target, operations) };
1297
1429
  }
1298
1430
  default:
1299
1431
  return { ok: false, reason: `Unknown field \`${field}\`.` };