@echelon-foundry/design-system 0.3.0-main.219.1 → 0.4.0-main.220.2

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 (41) hide show
  1. package/README.md +20 -0
  2. package/contracts/diagram-presentation.json +10 -4
  3. package/contracts/workflow-capabilities.json +25 -0
  4. package/dist/all.css +59 -0
  5. package/dist/build-metrics.json +3 -3
  6. package/dist/components.css +57 -0
  7. package/dist/foundations.css +2 -0
  8. package/dist/marketing/forma-all.css +60 -1
  9. package/dist/marketing/forma-echelon-marketing.css +3 -1
  10. package/dist/marketing/forma-marketing-theme-echelon.css +1 -1
  11. package/dist/marketing/forma-marketing.css +3 -1
  12. package/dist/marketing/forma-marketing.manifest.json +9 -9
  13. package/dist/marketing/forma-marketing.sha256 +4 -4
  14. package/docs/workflow/EMBEDDING.md +148 -0
  15. package/docs/workflow/EXTERNAL-PRODUCERS.md +34 -0
  16. package/docs/workflow/FORMAT.md +196 -0
  17. package/docs/workflow/RENDERING.md +90 -0
  18. package/docs/workflow/REQUIREMENTS-AUDIT-2026-10-01.md +55 -0
  19. package/docs/workflow/VALIDATION.md +91 -0
  20. package/examples/workflows/fixtures.json +101 -0
  21. package/examples/workflows/forma/workflows/accessible.forma-workflow.json +90 -0
  22. package/examples/workflows/forma/workflows/branching.forma-workflow.json +91 -0
  23. package/examples/workflows/forma/workflows/branded.forma-workflow.json +85 -0
  24. package/examples/workflows/forma/workflows/color-mapping.forma-workflow.json +111 -0
  25. package/examples/workflows/forma/workflows/colored.forma-workflow.json +131 -0
  26. package/examples/workflows/forma/workflows/edge-metadata.forma-workflow.json +66 -0
  27. package/examples/workflows/forma/workflows/embedded-edit.forma-workflow.json +117 -0
  28. package/examples/workflows/forma/workflows/embedded-view.forma-workflow.json +93 -0
  29. package/examples/workflows/forma/workflows/extensions.forma-workflow.json +90 -0
  30. package/examples/workflows/forma/workflows/groups.forma-workflow.json +89 -0
  31. package/examples/workflows/forma/workflows/interactive-export.forma-workflow.json +62 -0
  32. package/examples/workflows/forma/workflows/minimal.forma-workflow.json +17 -0
  33. package/examples/workflows/forma/workflows/mobile.forma-workflow.json +80 -0
  34. package/examples/workflows/forma/workflows/multi-step.forma-workflow.json +82 -0
  35. package/examples/workflows/forma/workflows/no-layout.forma-workflow.json +90 -0
  36. package/examples/workflows/forma/workflows/node-metadata.forma-workflow.json +80 -0
  37. package/examples/workflows/forma/workflows/runtime-ports.forma-workflow.json +118 -0
  38. package/examples/workflows/forma/workflows/static-export.forma-workflow.json +77 -0
  39. package/examples/workflows/forma/workflows/swimlanes.forma-workflow.json +130 -0
  40. package/package.json +21 -6
  41. package/schemas/workflow/1.0/forma-workflow.schema.json +1045 -0
package/README.md CHANGED
@@ -123,6 +123,26 @@ Workflow/diagram items may also carry authored fill, border/stroke, accent, conn
123
123
 
124
124
  See `requirements/OBJECT-METADATA-AND-DIAGRAM-PRESENTATION.md`.
125
125
 
126
+ ## Portable workflows
127
+
128
+ Forma owns the portable workflow standard (`.forma-workflow.json`): a versioned
129
+ JSON Schema, independent validation, deterministic layout, static HTML
130
+ rendering, and the embeddable `<forma-workflow>` renderer and editor. None of it
131
+ needs Forma Studio.
132
+
133
+ | Need | Use |
134
+ |---|---|
135
+ | The format | [`docs/workflow/FORMAT.md`](docs/workflow/FORMAT.md), schema [`schemas/workflow/1.0/forma-workflow.schema.json`](schemas/workflow/1.0/forma-workflow.schema.json) |
136
+ | Validate in CI or from an agent | `forma-workflow validate --json` ([`docs/workflow/VALIDATION.md`](docs/workflow/VALIDATION.md)) |
137
+ | Static or complete-document HTML | `forma-workflow render` ([`docs/workflow/RENDERING.md`](docs/workflow/RENDERING.md)) |
138
+ | Embed a viewer or editor | `@echelon-foundry/forma-workflow` ([`docs/workflow/EMBEDDING.md`](docs/workflow/EMBEDDING.md), [`examples/workflow-embedding`](examples/workflow-embedding)) |
139
+ | Produce workflows elsewhere | [`docs/workflow/EXTERNAL-PRODUCERS.md`](docs/workflow/EXTERNAL-PRODUCERS.md) |
140
+ | Reference fixtures | [`examples/workflows/`](examples/workflows/) |
141
+
142
+ The design-system package stays zero-runtime. The editor ships separately; see
143
+ [ADR-0006](docs/decisions/ADR-0006-portable-workflow-capability.md). Run
144
+ `npm run workflow:check` for the full workflow test suite.
145
+
126
146
  ## Figma
127
147
 
128
148
  Forma treats Figma as a design and developer-consumption surface, not as a
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
3
  "contract": "forma.diagram-presentation",
4
- "contractVersion": "2.0.0",
4
+ "contractVersion": "2.1.0",
5
5
  "stylesheet": "components.css",
6
6
  "decision": "docs/decisions/ADR-0004-diagram-presentation-boundary.md",
7
7
  "requirements": "requirements/OBJECT-METADATA-AND-DIAGRAM-PRESENTATION.md",
8
8
  "ownership": {
9
- "forma": ["surface", "node", "group", "connector", "connectorLabel", "marker", "relations", "legend", "valueState"],
10
- "consumer": ["geometry", "routing", "semanticKind", "metadataValues", "metadataVisibility", "namedStyles", "paletteSlots", "metadataMappings", "appearancePrecedence", "membership"],
9
+ "forma": ["surface", "node", "group", "connector", "connectorLabel", "marker", "relations", "legend", "valueState", "port", "status", "description", "membership"],
10
+ "consumer": ["geometry", "routing", "semanticKind", "metadataValues", "metadataVisibility", "namedStyles", "paletteSlots", "metadataMappings", "appearancePrecedence", "membership", "portPlacement", "statusValues"],
11
11
  "editorOnly": ["selection", "handles", "guides", "routePreview", "marquee", "dragTargets", "connectionTargets", "minimap", "validationOverlay"],
12
12
  "editorOnlyRule": "Editor-only adorners never use ef-diagram* classes and never appear in exported output."
13
13
  },
@@ -16,7 +16,10 @@
16
16
  "node": {
17
17
  "element": "article",
18
18
  "class": "ef-diagram-node",
19
- "regions": { "kind": "ef-diagram-node__kind", "label": "ef-diagram-node__label", "metadata": "ef-diagram-node__meta" },
19
+ "regions": { "kind": "ef-diagram-node__kind", "label": "ef-diagram-node__label", "description": "ef-diagram-node__description", "metadata": "ef-diagram-node__meta" },
20
+ "statusAttribute": "data-ef-status",
21
+ "statusCues": { "active": "outer ring", "blocked": "double border", "failed": "double border", "skipped": "dashed border", "cancelled": "dashed border", "pending": "dotted border", "waiting": "dotted border" },
22
+ "statusRule": "Status is text in the node metadata (dt Status, dd label). data-ef-status selects only a secondary non-colour cue; other values render no cue.",
20
23
  "shapeAttribute": "data-ef-shape",
21
24
  "shapes": ["rectangle", "rounded", "pill", "ellipse", "diamond"],
22
25
  "defaultShape": "rectangle",
@@ -47,6 +50,8 @@
47
50
  "markers": { "class": "ef-diagram-marker", "kinds": ["arrow"], "attachment": ["marker-start", "marker-end"] },
48
51
  "appearance": ["connectorStroke", "connectorWidth"]
49
52
  },
53
+ "port": { "element": "span", "class": "ef-diagram-port", "parent": "ef-diagram-node", "label": "ef-diagram-port__label", "sideAttribute": "data-ef-side", "sides": ["top", "right", "bottom", "left"], "directionAttribute": "data-ef-direction", "directions": ["in", "out", "inout"], "directionCue": "in: square, out: circle, inout: diamond", "offset": "--ef-diagram-port-offset", "rule": "A port is a marker. The relationship list names the port in text when a connector uses it." },
54
+ "membership": { "element": "dl", "class": "ef-diagram__membership", "rule": "One dt per group, lane or phase; its dd lists members in reading order. Canvas overlap never implies membership." },
50
55
  "connectorLabel": { "element": "span", "class": "ef-diagram-connector__label", "geometry": ["x", "y"] },
51
56
  "relations": { "element": "ol", "class": "ef-diagram__relations", "rule": "One text item per connector, in reading order." },
52
57
  "legend": { "element": "dl", "class": "ef-diagram-legend", "swatch": "ef-diagram-legend__swatch", "appearance": ["fill", "stroke", "accent"] },
@@ -74,6 +79,7 @@
74
79
  "h": "--ef-diagram-h",
75
80
  "canvasWidth": "--ef-diagram-canvas-w",
76
81
  "canvasHeight": "--ef-diagram-canvas-h",
82
+ "portOffset": "--ef-diagram-port-offset",
77
83
  "unit": "px",
78
84
  "axes": "physical (does not mirror in right-to-left documents)"
79
85
  },
@@ -0,0 +1,25 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "contract": "forma.workflow-capabilities",
4
+ "contractVersion": "1.0.0",
5
+ "formatVersions": ["1.0.0"],
6
+ "formaVersion": "0.4.0",
7
+ "decision": "docs/decisions/ADR-0006-portable-workflow-capability.md",
8
+ "requirements": "requirements/PORTABLE-WORKFLOW-INTERCHANGE.md",
9
+ "rule": "A workflow targeting a Forma version earlier than a capability's 'since' version, or requiring a capability this list does not contain, is valid format using unavailable Forma capabilities.",
10
+ "capabilities": {
11
+ "workflow.core": { "since": "0.4.0", "description": "Nodes, edges, labels, kinds, descriptions and the accessible relationship list." },
12
+ "workflow.shapes": { "since": "0.4.0", "description": "Node variants rectangle, rounded, pill, ellipse and diamond (diagram contract 2.0.0)." },
13
+ "workflow.color": { "since": "0.4.0", "description": "Authored fill, stroke, accent, foreground and connector stroke as tokens or literals." },
14
+ "workflow.palette": { "since": "0.4.0", "description": "Named palette slots referenced as palette:<slot>." },
15
+ "workflow.groups": { "since": "0.4.0", "description": "Groups and containers with explicit membership." },
16
+ "workflow.swimlanes": { "since": "0.4.0", "description": "Swimlanes and phases." },
17
+ "workflow.ports": { "since": "0.4.0", "description": "Named connection points on node sides (diagram contract 2.1.0)." },
18
+ "workflow.status": { "since": "0.4.0", "description": "Semantic status shown as text on nodes, edges and groups (diagram contract 2.1.0)." },
19
+ "workflow.references": { "since": "0.4.0", "description": "Typed domain references displayed as text or links." },
20
+ "workflow.interactions": { "since": "0.4.0", "description": "Declarative interaction intents reported to the host." },
21
+ "workflow.legend": { "since": "0.4.0", "description": "A key explaining authored colour and line meaning." },
22
+ "workflow.layout": { "since": "0.4.0", "description": "Authored geometry, waypoints and the deterministic default layout." },
23
+ "workflow.embed": { "since": "0.4.0", "description": "The @echelon-foundry/forma-workflow embeddable renderer and editor." }
24
+ }
25
+ }
package/dist/all.css CHANGED
@@ -642,6 +642,8 @@
642
642
 
643
643
  .ef-stack { display: flex; flex-direction: column; gap: var(--ef-stack-gap, var(--ef-primitive-spacing-4, 1rem)); }
644
644
  .ef-cluster { display: flex; flex-wrap: wrap; gap: var(--ef-cluster-gap, var(--ef-primitive-spacing-3, 0.75rem)); align-items: center; }
645
+ /* .ef-actions (catalog: button) is an application-layer row of related actions; it was styled only in marketing.css (forma#93 gap). */
646
+ .ef-actions { display: flex; flex-wrap: wrap; gap: var(--ef-primitive-spacing-3, 0.75rem); align-items: center; }
645
647
  .ef-grid { display: grid; gap: var(--ef-grid-gap, var(--ef-primitive-spacing-4, 1rem)); grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--ef-grid-min, 16rem)), 1fr)); }
646
648
  .ef-section { padding-block: var(--ef-section-space, var(--ef-primitive-spacing-7, 3rem)); }
647
649
  .ef-surface { min-inline-size: 0; padding: var(--ef-surface-space, var(--ef-primitive-spacing-5, 1.5rem)); background: var(--ef-color-surface-secondary, #e3e0d7); color: var(--ef-color-text-on-secondary-surface, #3a403c); border: 1px solid var(--ef-color-border-subtle, #e3e0d7); }
@@ -774,6 +776,8 @@
774
776
  font-size: 0.8125rem;
775
777
  line-height: 1.4;
776
778
  }
779
+ /* On a secondary surface, text-secondary falls below 4.5:1; use the surface's own text colour (forma#93). */
780
+ .ef-surface .ef-field__description { color: var(--ef-color-text-on-secondary-surface, #3a403c); }
777
781
 
778
782
  /* Motion model vocabulary --------------------------------------------- */
779
783
  /*
@@ -5294,6 +5298,61 @@
5294
5298
  .ef-diagram { break-inside: avoid; }
5295
5299
  }
5296
5300
 
5301
+ /*
5302
+ * Diagram contract 2.1.0 (workflow capability, forma#93; ADR-0006): ports,
5303
+ * node status, node description and the membership list. Presentation only.
5304
+ * Ports are connection-point markers placed by the consumer on a node side
5305
+ * (data-ef-side) at an offset along it (--ef-diagram-port-offset, a
5306
+ * percentage). Status is text in the node's metadata; data-ef-status only
5307
+ * selects a secondary, non-colour cue (outline, border style).
5308
+ */
5309
+ .ef-diagram-node__description { font-size: 0.875rem; line-height: 1.35; overflow-wrap: anywhere; }
5310
+ .ef-diagram-port {
5311
+ position: absolute;
5312
+ inline-size: 0.75rem;
5313
+ block-size: 0.75rem;
5314
+ box-sizing: border-box;
5315
+ border: 2px solid var(--ef-diagram-node-stroke, var(--ef-color-border-functional, #3a403c));
5316
+ border-radius: 50%;
5317
+ background: var(--ef-color-surface-primary, #f2efe7);
5318
+ transform: translate(-50%, -50%);
5319
+ }
5320
+ .ef-diagram-port[data-ef-side="top"] { left: var(--ef-diagram-port-offset, 50%); top: 0; }
5321
+ .ef-diagram-port[data-ef-side="bottom"] { left: var(--ef-diagram-port-offset, 50%); top: 100%; }
5322
+ .ef-diagram-port[data-ef-side="left"] { left: 0; top: var(--ef-diagram-port-offset, 50%); }
5323
+ .ef-diagram-port[data-ef-side="right"] { left: 100%; top: var(--ef-diagram-port-offset, 50%); }
5324
+ .ef-diagram-port[data-ef-direction="in"] { border-radius: 0; }
5325
+ .ef-diagram-port[data-ef-direction="inout"] { border-radius: 2px; transform: translate(-50%, -50%) rotate(45deg); }
5326
+ .ef-diagram-port__label {
5327
+ position: absolute;
5328
+ inset-block-start: 100%;
5329
+ inset-inline-start: 50%;
5330
+ transform: translateX(-50%);
5331
+ padding: 0 0.125rem;
5332
+ background: var(--ef-color-surface-primary, #f2efe7);
5333
+ color: var(--ef-color-text-primary, #171a18);
5334
+ font-size: 0.6875rem;
5335
+ line-height: 1.2;
5336
+ white-space: nowrap;
5337
+ }
5338
+ .ef-diagram-node[data-ef-status="active"] { box-shadow: 0 0 0 3px var(--ef-color-surface-primary, #f2efe7), 0 0 0 6px var(--ef-diagram-node-stroke); }
5339
+ .ef-diagram-node[data-ef-status="blocked"],
5340
+ .ef-diagram-node[data-ef-status="failed"] { border-style: double; border-width: 4px; border-inline-start-width: 6px; }
5341
+ .ef-diagram-node[data-ef-status="skipped"],
5342
+ .ef-diagram-node[data-ef-status="cancelled"] { border-style: dashed; border-inline-start-style: solid; }
5343
+ .ef-diagram-node[data-ef-status="pending"],
5344
+ .ef-diagram-node[data-ef-status="waiting"] { border-style: dotted; border-inline-start-style: solid; }
5345
+ .ef-diagram__membership { display: grid; grid-template-columns: minmax(0, max-content) minmax(0, 1fr); gap: var(--ef-primitive-spacing-1, 0.25rem) var(--ef-primitive-spacing-3, 0.75rem); margin: 0; }
5346
+ .ef-diagram__membership :where(dt) { font-weight: 700; overflow-wrap: anywhere; }
5347
+ .ef-diagram__membership :where(dd) { margin: 0; overflow-wrap: anywhere; }
5348
+ @media (max-width: 30rem) {
5349
+ .ef-diagram__membership { grid-template-columns: minmax(0, 1fr); }
5350
+ }
5351
+ @media (forced-colors: active) {
5352
+ .ef-diagram-port, .ef-diagram-port__label { border-color: CanvasText; background: Canvas; color: CanvasText; }
5353
+ .ef-diagram-node[data-ef-status="active"] { box-shadow: none; outline: 3px solid CanvasText; outline-offset: 3px; }
5354
+ }
5355
+
5297
5356
  /* BEGIN generated: character-grid coordinates (tools/character-grid-css.mjs) */
5298
5357
  .ef-character-grid[data-ef-rows="1"] { --ef-grid-rows: 1; }
5299
5358
  .ef-character-grid[data-ef-rows="2"] { --ef-grid-rows: 2; }
@@ -177,13 +177,13 @@
177
177
  "tokens.css"
178
178
  ],
179
179
  "bytes": {
180
- "all.css": 306099,
180
+ "all.css": 309758,
181
181
  "assessment.css": 34482,
182
182
  "brands/echelon.css": 3621,
183
183
  "brands/example-harbor.css": 3637,
184
184
  "brands/index.json": 335,
185
- "components.css": 194978,
186
- "foundations.css": 10639,
185
+ "components.css": 198383,
186
+ "foundations.css": 10893,
187
187
  "marketing.css": 45073,
188
188
  "patterns/active-filter-summary.html": 532,
189
189
  "patterns/alert.html": 384,
@@ -35,6 +35,8 @@
35
35
  font-size: 0.8125rem;
36
36
  line-height: 1.4;
37
37
  }
38
+ /* On a secondary surface, text-secondary falls below 4.5:1; use the surface's own text colour (forma#93). */
39
+ .ef-surface .ef-field__description { color: var(--ef-color-text-on-secondary-surface, #3a403c); }
38
40
 
39
41
  /* Motion model vocabulary --------------------------------------------- */
40
42
  /*
@@ -4555,6 +4557,61 @@
4555
4557
  .ef-diagram { break-inside: avoid; }
4556
4558
  }
4557
4559
 
4560
+ /*
4561
+ * Diagram contract 2.1.0 (workflow capability, forma#93; ADR-0006): ports,
4562
+ * node status, node description and the membership list. Presentation only.
4563
+ * Ports are connection-point markers placed by the consumer on a node side
4564
+ * (data-ef-side) at an offset along it (--ef-diagram-port-offset, a
4565
+ * percentage). Status is text in the node's metadata; data-ef-status only
4566
+ * selects a secondary, non-colour cue (outline, border style).
4567
+ */
4568
+ .ef-diagram-node__description { font-size: 0.875rem; line-height: 1.35; overflow-wrap: anywhere; }
4569
+ .ef-diagram-port {
4570
+ position: absolute;
4571
+ inline-size: 0.75rem;
4572
+ block-size: 0.75rem;
4573
+ box-sizing: border-box;
4574
+ border: 2px solid var(--ef-diagram-node-stroke, var(--ef-color-border-functional, #3a403c));
4575
+ border-radius: 50%;
4576
+ background: var(--ef-color-surface-primary, #f2efe7);
4577
+ transform: translate(-50%, -50%);
4578
+ }
4579
+ .ef-diagram-port[data-ef-side="top"] { left: var(--ef-diagram-port-offset, 50%); top: 0; }
4580
+ .ef-diagram-port[data-ef-side="bottom"] { left: var(--ef-diagram-port-offset, 50%); top: 100%; }
4581
+ .ef-diagram-port[data-ef-side="left"] { left: 0; top: var(--ef-diagram-port-offset, 50%); }
4582
+ .ef-diagram-port[data-ef-side="right"] { left: 100%; top: var(--ef-diagram-port-offset, 50%); }
4583
+ .ef-diagram-port[data-ef-direction="in"] { border-radius: 0; }
4584
+ .ef-diagram-port[data-ef-direction="inout"] { border-radius: 2px; transform: translate(-50%, -50%) rotate(45deg); }
4585
+ .ef-diagram-port__label {
4586
+ position: absolute;
4587
+ inset-block-start: 100%;
4588
+ inset-inline-start: 50%;
4589
+ transform: translateX(-50%);
4590
+ padding: 0 0.125rem;
4591
+ background: var(--ef-color-surface-primary, #f2efe7);
4592
+ color: var(--ef-color-text-primary, #171a18);
4593
+ font-size: 0.6875rem;
4594
+ line-height: 1.2;
4595
+ white-space: nowrap;
4596
+ }
4597
+ .ef-diagram-node[data-ef-status="active"] { box-shadow: 0 0 0 3px var(--ef-color-surface-primary, #f2efe7), 0 0 0 6px var(--ef-diagram-node-stroke); }
4598
+ .ef-diagram-node[data-ef-status="blocked"],
4599
+ .ef-diagram-node[data-ef-status="failed"] { border-style: double; border-width: 4px; border-inline-start-width: 6px; }
4600
+ .ef-diagram-node[data-ef-status="skipped"],
4601
+ .ef-diagram-node[data-ef-status="cancelled"] { border-style: dashed; border-inline-start-style: solid; }
4602
+ .ef-diagram-node[data-ef-status="pending"],
4603
+ .ef-diagram-node[data-ef-status="waiting"] { border-style: dotted; border-inline-start-style: solid; }
4604
+ .ef-diagram__membership { display: grid; grid-template-columns: minmax(0, max-content) minmax(0, 1fr); gap: var(--ef-primitive-spacing-1, 0.25rem) var(--ef-primitive-spacing-3, 0.75rem); margin: 0; }
4605
+ .ef-diagram__membership :where(dt) { font-weight: 700; overflow-wrap: anywhere; }
4606
+ .ef-diagram__membership :where(dd) { margin: 0; overflow-wrap: anywhere; }
4607
+ @media (max-width: 30rem) {
4608
+ .ef-diagram__membership { grid-template-columns: minmax(0, 1fr); }
4609
+ }
4610
+ @media (forced-colors: active) {
4611
+ .ef-diagram-port, .ef-diagram-port__label { border-color: CanvasText; background: Canvas; color: CanvasText; }
4612
+ .ef-diagram-node[data-ef-status="active"] { box-shadow: none; outline: 3px solid CanvasText; outline-offset: 3px; }
4613
+ }
4614
+
4558
4615
  /* BEGIN generated: character-grid coordinates (tools/character-grid-css.mjs) */
4559
4616
  .ef-character-grid[data-ef-rows="1"] { --ef-grid-rows: 1; }
4560
4617
  .ef-character-grid[data-ef-rows="2"] { --ef-grid-rows: 2; }
@@ -107,6 +107,8 @@
107
107
 
108
108
  .ef-stack { display: flex; flex-direction: column; gap: var(--ef-stack-gap, var(--ef-primitive-spacing-4, 1rem)); }
109
109
  .ef-cluster { display: flex; flex-wrap: wrap; gap: var(--ef-cluster-gap, var(--ef-primitive-spacing-3, 0.75rem)); align-items: center; }
110
+ /* .ef-actions (catalog: button) is an application-layer row of related actions; it was styled only in marketing.css (forma#93 gap). */
111
+ .ef-actions { display: flex; flex-wrap: wrap; gap: var(--ef-primitive-spacing-3, 0.75rem); align-items: center; }
110
112
  .ef-grid { display: grid; gap: var(--ef-grid-gap, var(--ef-primitive-spacing-4, 1rem)); grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--ef-grid-min, 16rem)), 1fr)); }
111
113
  .ef-section { padding-block: var(--ef-section-space, var(--ef-primitive-spacing-7, 3rem)); }
112
114
  .ef-surface { min-inline-size: 0; padding: var(--ef-surface-space, var(--ef-primitive-spacing-5, 1.5rem)); background: var(--ef-color-surface-secondary, #e3e0d7); color: var(--ef-color-text-on-secondary-surface, #3a403c); border: 1px solid var(--ef-color-border-subtle, #e3e0d7); }
@@ -1,4 +1,4 @@
1
- /*! Forma 0.3.0 | forma-all.css | bundle echelon-marketing
1
+ /*! Forma 0.4.0 | forma-all.css | bundle echelon-marketing
2
2
  Complete Forma surface (application components, assessment, and marketing layers) for sites that also present Forma application patterns such as data grids or status lozenges.
3
3
  Generated by tools/PresentationBundler. Do not edit; pin by version and sha256. */
4
4
 
@@ -647,6 +647,8 @@
647
647
 
648
648
  .ef-stack { display: flex; flex-direction: column; gap: var(--ef-stack-gap, var(--ef-primitive-spacing-4, 1rem)); }
649
649
  .ef-cluster { display: flex; flex-wrap: wrap; gap: var(--ef-cluster-gap, var(--ef-primitive-spacing-3, 0.75rem)); align-items: center; }
650
+ /* .ef-actions (catalog: button) is an application-layer row of related actions; it was styled only in marketing.css (forma#93 gap). */
651
+ .ef-actions { display: flex; flex-wrap: wrap; gap: var(--ef-primitive-spacing-3, 0.75rem); align-items: center; }
650
652
  .ef-grid { display: grid; gap: var(--ef-grid-gap, var(--ef-primitive-spacing-4, 1rem)); grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--ef-grid-min, 16rem)), 1fr)); }
651
653
  .ef-section { padding-block: var(--ef-section-space, var(--ef-primitive-spacing-7, 3rem)); }
652
654
  .ef-surface { min-inline-size: 0; padding: var(--ef-surface-space, var(--ef-primitive-spacing-5, 1.5rem)); background: var(--ef-color-surface-secondary, #e3e0d7); color: var(--ef-color-text-on-secondary-surface, #3a403c); border: 1px solid var(--ef-color-border-subtle, #e3e0d7); }
@@ -779,6 +781,8 @@
779
781
  font-size: 0.8125rem;
780
782
  line-height: 1.4;
781
783
  }
784
+ /* On a secondary surface, text-secondary falls below 4.5:1; use the surface's own text colour (forma#93). */
785
+ .ef-surface .ef-field__description { color: var(--ef-color-text-on-secondary-surface, #3a403c); }
782
786
 
783
787
  /* Motion model vocabulary --------------------------------------------- */
784
788
  /*
@@ -5299,6 +5303,61 @@
5299
5303
  .ef-diagram { break-inside: avoid; }
5300
5304
  }
5301
5305
 
5306
+ /*
5307
+ * Diagram contract 2.1.0 (workflow capability, forma#93; ADR-0006): ports,
5308
+ * node status, node description and the membership list. Presentation only.
5309
+ * Ports are connection-point markers placed by the consumer on a node side
5310
+ * (data-ef-side) at an offset along it (--ef-diagram-port-offset, a
5311
+ * percentage). Status is text in the node's metadata; data-ef-status only
5312
+ * selects a secondary, non-colour cue (outline, border style).
5313
+ */
5314
+ .ef-diagram-node__description { font-size: 0.875rem; line-height: 1.35; overflow-wrap: anywhere; }
5315
+ .ef-diagram-port {
5316
+ position: absolute;
5317
+ inline-size: 0.75rem;
5318
+ block-size: 0.75rem;
5319
+ box-sizing: border-box;
5320
+ border: 2px solid var(--ef-diagram-node-stroke, var(--ef-color-border-functional, #3a403c));
5321
+ border-radius: 50%;
5322
+ background: var(--ef-color-surface-primary, #f2efe7);
5323
+ transform: translate(-50%, -50%);
5324
+ }
5325
+ .ef-diagram-port[data-ef-side="top"] { left: var(--ef-diagram-port-offset, 50%); top: 0; }
5326
+ .ef-diagram-port[data-ef-side="bottom"] { left: var(--ef-diagram-port-offset, 50%); top: 100%; }
5327
+ .ef-diagram-port[data-ef-side="left"] { left: 0; top: var(--ef-diagram-port-offset, 50%); }
5328
+ .ef-diagram-port[data-ef-side="right"] { left: 100%; top: var(--ef-diagram-port-offset, 50%); }
5329
+ .ef-diagram-port[data-ef-direction="in"] { border-radius: 0; }
5330
+ .ef-diagram-port[data-ef-direction="inout"] { border-radius: 2px; transform: translate(-50%, -50%) rotate(45deg); }
5331
+ .ef-diagram-port__label {
5332
+ position: absolute;
5333
+ inset-block-start: 100%;
5334
+ inset-inline-start: 50%;
5335
+ transform: translateX(-50%);
5336
+ padding: 0 0.125rem;
5337
+ background: var(--ef-color-surface-primary, #f2efe7);
5338
+ color: var(--ef-color-text-primary, #171a18);
5339
+ font-size: 0.6875rem;
5340
+ line-height: 1.2;
5341
+ white-space: nowrap;
5342
+ }
5343
+ .ef-diagram-node[data-ef-status="active"] { box-shadow: 0 0 0 3px var(--ef-color-surface-primary, #f2efe7), 0 0 0 6px var(--ef-diagram-node-stroke); }
5344
+ .ef-diagram-node[data-ef-status="blocked"],
5345
+ .ef-diagram-node[data-ef-status="failed"] { border-style: double; border-width: 4px; border-inline-start-width: 6px; }
5346
+ .ef-diagram-node[data-ef-status="skipped"],
5347
+ .ef-diagram-node[data-ef-status="cancelled"] { border-style: dashed; border-inline-start-style: solid; }
5348
+ .ef-diagram-node[data-ef-status="pending"],
5349
+ .ef-diagram-node[data-ef-status="waiting"] { border-style: dotted; border-inline-start-style: solid; }
5350
+ .ef-diagram__membership { display: grid; grid-template-columns: minmax(0, max-content) minmax(0, 1fr); gap: var(--ef-primitive-spacing-1, 0.25rem) var(--ef-primitive-spacing-3, 0.75rem); margin: 0; }
5351
+ .ef-diagram__membership :where(dt) { font-weight: 700; overflow-wrap: anywhere; }
5352
+ .ef-diagram__membership :where(dd) { margin: 0; overflow-wrap: anywhere; }
5353
+ @media (max-width: 30rem) {
5354
+ .ef-diagram__membership { grid-template-columns: minmax(0, 1fr); }
5355
+ }
5356
+ @media (forced-colors: active) {
5357
+ .ef-diagram-port, .ef-diagram-port__label { border-color: CanvasText; background: Canvas; color: CanvasText; }
5358
+ .ef-diagram-node[data-ef-status="active"] { box-shadow: none; outline: 3px solid CanvasText; outline-offset: 3px; }
5359
+ }
5360
+
5302
5361
  /* BEGIN generated: character-grid coordinates (tools/character-grid-css.mjs) */
5303
5362
  .ef-character-grid[data-ef-rows="1"] { --ef-grid-rows: 1; }
5304
5363
  .ef-character-grid[data-ef-rows="2"] { --ef-grid-rows: 2; }
@@ -1,4 +1,4 @@
1
- /*! Forma 0.3.0 | forma-echelon-marketing.css | bundle echelon-marketing
1
+ /*! Forma 0.4.0 | forma-echelon-marketing.css | bundle echelon-marketing
2
2
  Single-file Echelon marketing presentation: forma-marketing.css followed by the Echelon theme. Most sites load only this file.
3
3
  Generated by tools/PresentationBundler. Do not edit; pin by version and sha256. */
4
4
 
@@ -747,6 +747,8 @@
747
747
 
748
748
  .ef-stack { display: flex; flex-direction: column; gap: var(--ef-stack-gap, var(--ef-primitive-spacing-4, 1rem)); }
749
749
  .ef-cluster { display: flex; flex-wrap: wrap; gap: var(--ef-cluster-gap, var(--ef-primitive-spacing-3, 0.75rem)); align-items: center; }
750
+ /* .ef-actions (catalog: button) is an application-layer row of related actions; it was styled only in marketing.css (forma#93 gap). */
751
+ .ef-actions { display: flex; flex-wrap: wrap; gap: var(--ef-primitive-spacing-3, 0.75rem); align-items: center; }
750
752
  .ef-grid { display: grid; gap: var(--ef-grid-gap, var(--ef-primitive-spacing-4, 1rem)); grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--ef-grid-min, 16rem)), 1fr)); }
751
753
  .ef-section { padding-block: var(--ef-section-space, var(--ef-primitive-spacing-7, 3rem)); }
752
754
  .ef-surface { min-inline-size: 0; padding: var(--ef-surface-space, var(--ef-primitive-spacing-5, 1.5rem)); background: var(--ef-color-surface-secondary, #e3e0d7); color: var(--ef-color-text-on-secondary-surface, #3a403c); border: 1px solid var(--ef-color-border-subtle, #e3e0d7); }
@@ -1,4 +1,4 @@
1
- /*! Forma 0.3.0 | forma-marketing-theme-echelon.css | bundle echelon-marketing
1
+ /*! Forma 0.4.0 | forma-marketing-theme-echelon.css | bundle echelon-marketing
2
2
  Echelon Marketing Theme: compiled theme tokens and theme hooks. Load after forma-marketing.css.
3
3
  Generated by tools/PresentationBundler. Do not edit; pin by version and sha256. */
4
4
 
@@ -1,4 +1,4 @@
1
- /*! Forma 0.3.0 | forma-marketing.css | bundle echelon-marketing
1
+ /*! Forma 0.4.0 | forma-marketing.css | bundle echelon-marketing
2
2
  Brand-neutral marketing layer: core tokens, role defaults, Forma foundations, marketing foundations, components, and layouts.
3
3
  Generated by tools/PresentationBundler. Do not edit; pin by version and sha256. */
4
4
 
@@ -746,6 +746,8 @@
746
746
 
747
747
  .ef-stack { display: flex; flex-direction: column; gap: var(--ef-stack-gap, var(--ef-primitive-spacing-4, 1rem)); }
748
748
  .ef-cluster { display: flex; flex-wrap: wrap; gap: var(--ef-cluster-gap, var(--ef-primitive-spacing-3, 0.75rem)); align-items: center; }
749
+ /* .ef-actions (catalog: button) is an application-layer row of related actions; it was styled only in marketing.css (forma#93 gap). */
750
+ .ef-actions { display: flex; flex-wrap: wrap; gap: var(--ef-primitive-spacing-3, 0.75rem); align-items: center; }
749
751
  .ef-grid { display: grid; gap: var(--ef-grid-gap, var(--ef-primitive-spacing-4, 1rem)); grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--ef-grid-min, 16rem)), 1fr)); }
750
752
  .ef-section { padding-block: var(--ef-section-space, var(--ef-primitive-spacing-7, 3rem)); }
751
753
  .ef-surface { min-inline-size: 0; padding: var(--ef-surface-space, var(--ef-primitive-spacing-5, 1.5rem)); background: var(--ef-color-surface-secondary, #e3e0d7); color: var(--ef-color-text-on-secondary-surface, #3a403c); border: 1px solid var(--ef-color-border-subtle, #e3e0d7); }
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "forma": "0.3.0",
3
+ "forma": "0.4.0",
4
4
  "bundle": "echelon-marketing",
5
5
  "description": "Forma presentation artifacts for Echelon marketing sites. Every artifact is a single self-contained CSS file with no imports or relative URLs.",
6
- "releaseTag": "v0.3.0",
6
+ "releaseTag": "v0.4.0",
7
7
  "artifacts": [
8
8
  {
9
9
  "file": "forma-marketing.css",
10
10
  "description": "Brand-neutral marketing layer: core tokens, role defaults, Forma foundations, marketing foundations, components, and layouts.",
11
- "bytes": 77198,
12
- "sha256": "9aec7b369ef6b60ceb5c8db51f088d232d2da07816cf9074ef1859fe035f4a65",
11
+ "bytes": 77452,
12
+ "sha256": "e9f2f952cbec98bbdf270d8a6ed639fa8abc57d45053a78acbb85775102f35ea",
13
13
  "sources": [
14
14
  "dist/tokens.css",
15
15
  "src/marketing/roles.css",
@@ -23,7 +23,7 @@
23
23
  "file": "forma-marketing-theme-echelon.css",
24
24
  "description": "Echelon Marketing Theme: compiled theme tokens and theme hooks. Load after forma-marketing.css.",
25
25
  "bytes": 6295,
26
- "sha256": "157f1ec870a8825a0bbbfed4e719db7b92962e2625a8165f73abc4214c01e907",
26
+ "sha256": "295da09137526ef046ec46101db12231755d13b5ce5377a180f34ea1064417c6",
27
27
  "sources": [
28
28
  "build/marketing/echelon-marketing.tokens.css",
29
29
  "themes/echelon/marketing.css"
@@ -32,8 +32,8 @@
32
32
  {
33
33
  "file": "forma-echelon-marketing.css",
34
34
  "description": "Single-file Echelon marketing presentation: forma-marketing.css followed by the Echelon theme. Most sites load only this file.",
35
- "bytes": 83338,
36
- "sha256": "383948a077c070ad4bda90688a84f65bd319fde1e326197d5917f0dde632f947",
35
+ "bytes": 83592,
36
+ "sha256": "c0634b649d91f3a4fda166c3b926655fd1dc0270406e0a2bb43dc38303fa7709",
37
37
  "sources": [
38
38
  "artifact:forma-marketing.css",
39
39
  "artifact:forma-marketing-theme-echelon.css"
@@ -42,8 +42,8 @@
42
42
  {
43
43
  "file": "forma-all.css",
44
44
  "description": "Complete Forma surface (application components, assessment, and marketing layers) for sites that also present Forma application patterns such as data grids or status lozenges.",
45
- "bytes": 306461,
46
- "sha256": "976b95c7e990ecc746f627be981a6d91721132ef8d242cd60992df94dbfccb24",
45
+ "bytes": 310120,
46
+ "sha256": "0d74d63c0cf65c732e32c1cdc66d241cec660b198b1151c54b2edafc5e32fa68",
47
47
  "sources": [
48
48
  "dist/all.css"
49
49
  ]
@@ -1,4 +1,4 @@
1
- 9aec7b369ef6b60ceb5c8db51f088d232d2da07816cf9074ef1859fe035f4a65 forma-marketing.css
2
- 157f1ec870a8825a0bbbfed4e719db7b92962e2625a8165f73abc4214c01e907 forma-marketing-theme-echelon.css
3
- 383948a077c070ad4bda90688a84f65bd319fde1e326197d5917f0dde632f947 forma-echelon-marketing.css
4
- 976b95c7e990ecc746f627be981a6d91721132ef8d242cd60992df94dbfccb24 forma-all.css
1
+ e9f2f952cbec98bbdf270d8a6ed639fa8abc57d45053a78acbb85775102f35ea forma-marketing.css
2
+ 295da09137526ef046ec46101db12231755d13b5ce5377a180f34ea1064417c6 forma-marketing-theme-echelon.css
3
+ c0634b649d91f3a4fda166c3b926655fd1dc0270406e0a2bb43dc38303fa7709 forma-echelon-marketing.css
4
+ 0d74d63c0cf65c732e32c1cdc66d241cec660b198b1151c54b2edafc5e32fa68 forma-all.css
@@ -0,0 +1,148 @@
1
+ # Embedding the workflow renderer and editor
2
+
3
+ `@echelon-foundry/forma-workflow` is the reusable workflow component. It
4
+ provides the public `<forma-workflow>` element and runs the F# engine
5
+ (`Forma.Workflow`) on .NET WebAssembly. It is a separate, opt-in package. The
6
+ design-system package stays zero-runtime (ADR-0006). You do not need Forma
7
+ Studio.
8
+
9
+ A complete host application that is not Studio lives in
10
+ [`examples/workflow-embedding/`](../../examples/workflow-embedding/). It shows
11
+ runtime, edit, pick and isolated modes and a partner brand. Its browser tests
12
+ are in [`tests/browser/workflow-embed.spec.mjs`](../../tests/browser/workflow-embed.spec.mjs).
13
+
14
+ ## Same-page embedding (no iframe)
15
+
16
+ ```html
17
+ <link rel="stylesheet" href="/assets/forma/tokens.css">
18
+ <link rel="stylesheet" href="/assets/forma/foundations.css">
19
+ <link rel="stylesheet" href="/assets/forma/components.css">
20
+ <link rel="stylesheet" href="/assets/forma-workflow/forma-workflow.css">
21
+ <script type="module" src="/assets/forma-workflow/forma-workflow.js"></script>
22
+
23
+ <forma-workflow id="designer" mode="edit"></forma-workflow>
24
+ <script type="module">
25
+ const designer = document.getElementById("designer");
26
+ const workflow = await (await fetch("/forma/workflows/launch.forma-workflow.json")).json();
27
+ await designer.load(workflow);
28
+ designer.addEventListener("forma-workflow-change", (e) => save(e.detail.workflow));
29
+ </script>
30
+ ```
31
+
32
+ You can also put the document inline as an inert data block. The element then
33
+ loads it on start, and the static figure inside the element is the
34
+ no-script fallback:
35
+
36
+ ```html
37
+ <forma-workflow mode="view">
38
+ <script type="application/json" class="forma-workflow-source">{ "format": "forma-workflow", … }</script>
39
+ <figure class="ef-diagram">…static rendering…</figure>
40
+ </forma-workflow>
41
+ ```
42
+
43
+ The element inherits Forma tokens, brands (`data-ef-brand` on any ancestor),
44
+ themes, forced colours, reduced motion and responsive behaviour from the host
45
+ page.
46
+
47
+ ## Host modes
48
+
49
+ | `mode` | What people can do |
50
+ |---|---|
51
+ | `view` | Read. Zoom. Activate declared intents. |
52
+ | `inspect` | Select objects and read their metadata, references, status and extensions. |
53
+ | `edit` | Create, connect, reconnect, move, resize, rename and recolour. Set status, ports, groups and lanes, metadata, references, interactions and accessibility. Arrange. Undo and redo. |
54
+ | `pick` | Choose one or more objects; the host receives them on "Use selection". |
55
+ | `runtime` | Read. The host projects live state with `setRuntimeState`. The document is never changed. |
56
+
57
+ ## Host contract `forma-workflow-host/1`
58
+
59
+ The contract is small and versioned. Hosts never see the engine's internal state.
60
+
61
+ **Attributes:** `mode`, `instance` (optional stable id), `engine` (optional URL of the engine directory).
62
+
63
+ **Methods** (each returns a promise):
64
+
65
+ | Method | Effect |
66
+ |---|---|
67
+ | `load(workflow)` | Load a document (object or JSON text). Emits `forma-workflow-load`. |
68
+ | `getWorkflow()` | The current document, canonical JSON. |
69
+ | `execute(command)` | Run a validated command, for example `{ name: "addNode", kind: "task", label: "Review" }`, `{ name: "connect", source: { node: "a" }, target: { node: "b" } }`, `{ name: "setLabel", object: "node:a", value: "…" }`, `{ name: "setMetadata", object: "node:a", key: "owner", value: "Ops" }`, `{ name: "setStatus", object: "node:a", status: { state: "active" } }`, `{ name: "setColor", object: "node:a", property: "fill", value: "#e8f0ff" }`, `{ name: "move", nodes: ["a"], dx: 8, dy: 0 }`, `{ name: "delete", objects: ["edge:e1"] }`, `{ name: "autoLayout" }`. |
70
+ | `undo()`, `redo()` | History. |
71
+ | `select(objects)` | `["node:a", { type: "edge", id: "e1" }]`. |
72
+ | `focusObject(object)` | Move keyboard focus to an object. |
73
+ | `setRuntimeState(states)` | `{ "nodeId": { state: "active", label: "In progress" }, "other": null }`. |
74
+ | `validate(workflow)` | The validation report, without loading. |
75
+
76
+ **Events** (bubbling `CustomEvent`s; `detail` is JSON):
77
+
78
+ | Event | `detail` |
79
+ |---|---|
80
+ | `forma-workflow-ready` | Engine started. |
81
+ | `forma-workflow-load` | `valid`, `class`, `findings`, `workflow`. |
82
+ | `forma-workflow-change` | `description`, `workflow` (the new document), `valid`, `class`, `findings`. Only legal, validated changes arrive. |
83
+ | `forma-workflow-selection` | `objects: [{ type, id }]`. |
84
+ | `forma-workflow-focus` | `object`. |
85
+ | `forma-workflow-intent` | `object`, `action`, `target`, `command`, `label`, `parameters`. The host decides what to do. |
86
+ | `forma-workflow-pick` | `objects`. |
87
+ | `forma-workflow-refused` | `message`. A command was refused and nothing changed. |
88
+ | `forma-workflow-error` | `message` (the engine failed to start). |
89
+
90
+ A command that would introduce a new validation error is refused. Examples are a
91
+ dangling connection, a port over its limit, or a blank name. Unknown metadata
92
+ and extensions survive every edit.
93
+
94
+ ## Interaction model
95
+
96
+ - **Keyboard.** Every object is a button in the Objects list. Arrow keys move the selected steps (Shift moves further). Delete removes them. Escape cancels. All inspector fields are native form controls.
97
+ - **Pointer.** Click selects. Ctrl, Cmd or Shift-click adds to the selection. Drag a selected step to move it; it is one change when released. Drag the corner handle to resize. Drag the round handle onto another step to connect.
98
+ - **Touch and mobile.** Tap selects. No operation needs dragging: "Connect" then tap a target, the Connect form, the Move buttons and the Position and size fields all work without it. Handles have a 44px hit area. Below 64rem the layout stacks, with the canvas first, and the canvas scrolls inside its own region.
99
+ - **Zoom and pan.** Use the zoom buttons. Pan by scrolling the canvas. Zoom is view state and is never saved.
100
+ - **Announcements.** A polite live region reports each change, refusal and selection.
101
+
102
+ ## .NET hosts
103
+
104
+ A host that already runs .NET, such as Forma Studio, links `Forma.Workflow`.
105
+ It supplies the element's transport itself instead of loading a second runtime:
106
+
107
+ ```js
108
+ element.transport = {
109
+ async dispatch(json) { return myExports.WorkflowDispatch(json); }, // backed by EmbedHost.dispatch
110
+ async document(id) { return myExports.WorkflowDocument(id); },
111
+ async dispose(id) { myExports.WorkflowDispose(id); },
112
+ async validate(text) { return JSON.parse(myExports.WorkflowValidate(text)); }
113
+ };
114
+ ```
115
+
116
+ ## Isolated mode (iframe)
117
+
118
+ Use isolated mode only when you need stronger separation. It carries the same
119
+ portable document over the same `forma-workflow-host/1` contract, through
120
+ `postMessage`:
121
+
122
+ ```html
123
+ <iframe src="/assets/forma-workflow/frame.html?parent=https://app.example.org&forma=https://app.example.org/assets/forma/&brand=echelon"></iframe>
124
+ ```
125
+
126
+ | From the parent | Fields |
127
+ |---|---|
128
+ | `{ protocol, id, type: "load", document }` | |
129
+ | `{ protocol, id, type: "mode", mode }` | |
130
+ | `{ protocol, id, type: "command", command }` | |
131
+ | `{ protocol, id, type: "select", objects }`, `"focus"`, `"runtime"`, `"undo"`, `"redo"`, `"get"` | |
132
+
133
+ | From the frame | Fields |
134
+ |---|---|
135
+ | `{ protocol, type: "frame-ready" }` | |
136
+ | `{ protocol, type: "reply", id, ok, workflow?, error? }` | |
137
+ | `{ protocol, type: "event", event: { type, … } }` | the same events as above |
138
+
139
+ The frame accepts messages only from the origin in `?parent=`, and only from
140
+ `window.parent`. It posts only to that origin.
141
+
142
+ ## Security
143
+
144
+ Workflow files are data. The engine builds every view from a typed HTML tree
145
+ that escapes all text and attributes. It never emits event-handler attributes
146
+ or `javascript:` URLs. Intents are reported, never executed. The host element
147
+ has no `eval`, no `new Function` and no decision logic; a package test checks
148
+ this.