@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.
- package/README.md +20 -0
- package/contracts/diagram-presentation.json +10 -4
- package/contracts/workflow-capabilities.json +25 -0
- package/dist/all.css +59 -0
- package/dist/build-metrics.json +3 -3
- package/dist/components.css +57 -0
- package/dist/foundations.css +2 -0
- package/dist/marketing/forma-all.css +60 -1
- package/dist/marketing/forma-echelon-marketing.css +3 -1
- package/dist/marketing/forma-marketing-theme-echelon.css +1 -1
- package/dist/marketing/forma-marketing.css +3 -1
- package/dist/marketing/forma-marketing.manifest.json +9 -9
- package/dist/marketing/forma-marketing.sha256 +4 -4
- package/docs/workflow/EMBEDDING.md +148 -0
- package/docs/workflow/EXTERNAL-PRODUCERS.md +34 -0
- package/docs/workflow/FORMAT.md +196 -0
- package/docs/workflow/RENDERING.md +90 -0
- package/docs/workflow/REQUIREMENTS-AUDIT-2026-10-01.md +55 -0
- package/docs/workflow/VALIDATION.md +91 -0
- package/examples/workflows/fixtures.json +101 -0
- package/examples/workflows/forma/workflows/accessible.forma-workflow.json +90 -0
- package/examples/workflows/forma/workflows/branching.forma-workflow.json +91 -0
- package/examples/workflows/forma/workflows/branded.forma-workflow.json +85 -0
- package/examples/workflows/forma/workflows/color-mapping.forma-workflow.json +111 -0
- package/examples/workflows/forma/workflows/colored.forma-workflow.json +131 -0
- package/examples/workflows/forma/workflows/edge-metadata.forma-workflow.json +66 -0
- package/examples/workflows/forma/workflows/embedded-edit.forma-workflow.json +117 -0
- package/examples/workflows/forma/workflows/embedded-view.forma-workflow.json +93 -0
- package/examples/workflows/forma/workflows/extensions.forma-workflow.json +90 -0
- package/examples/workflows/forma/workflows/groups.forma-workflow.json +89 -0
- package/examples/workflows/forma/workflows/interactive-export.forma-workflow.json +62 -0
- package/examples/workflows/forma/workflows/minimal.forma-workflow.json +17 -0
- package/examples/workflows/forma/workflows/mobile.forma-workflow.json +80 -0
- package/examples/workflows/forma/workflows/multi-step.forma-workflow.json +82 -0
- package/examples/workflows/forma/workflows/no-layout.forma-workflow.json +90 -0
- package/examples/workflows/forma/workflows/node-metadata.forma-workflow.json +80 -0
- package/examples/workflows/forma/workflows/runtime-ports.forma-workflow.json +118 -0
- package/examples/workflows/forma/workflows/static-export.forma-workflow.json +77 -0
- package/examples/workflows/forma/workflows/swimlanes.forma-workflow.json +130 -0
- package/package.json +21 -6
- 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.
|
|
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; }
|
package/dist/build-metrics.json
CHANGED
|
@@ -177,13 +177,13 @@
|
|
|
177
177
|
"tokens.css"
|
|
178
178
|
],
|
|
179
179
|
"bytes": {
|
|
180
|
-
"all.css":
|
|
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":
|
|
186
|
-
"foundations.css":
|
|
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,
|
package/dist/components.css
CHANGED
|
@@ -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; }
|
package/dist/foundations.css
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
"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.
|
|
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":
|
|
12
|
-
"sha256": "
|
|
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": "
|
|
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":
|
|
36
|
-
"sha256": "
|
|
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":
|
|
46
|
-
"sha256": "
|
|
45
|
+
"bytes": 310120,
|
|
46
|
+
"sha256": "0d74d63c0cf65c732e32c1cdc66d241cec660b198b1151c54b2edafc5e32fa68",
|
|
47
47
|
"sources": [
|
|
48
48
|
"dist/all.css"
|
|
49
49
|
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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.
|