@kubex/zinc 1.1.22 → 1.1.24
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/custom-elements-manifest.config.js +2 -4
- package/dist/custom-elements.json +1172 -78
- package/dist/vscode.html-custom-data.json +34 -13
- package/dist/web-types.json +135 -26
- package/dist/zn.d.ts +867 -1
- package/dist/zn.min.js +893 -481
- package/docs/_includes/default.njk +1 -0
- package/docs/_includes/full-page.njk +38 -0
- package/docs/pages/components/flow-builder-demo.njk +194 -0
- package/docs/pages/components/flow-builder-troubleshooter-demo.njk +282 -0
- package/docs/pages/components/flow-builder.md +377 -0
- package/package.json +1 -1
- package/src/components/button/button.component.ts +1 -2
- package/src/components/button/button.scss +6 -16
- package/src/components/collapsible/collapsible.component.ts +11 -6
- package/src/components/data-table/data-table.component.ts +12 -12
- package/src/components/flow-builder/flow-builder.component.ts +1171 -0
- package/src/components/flow-builder/flow-builder.scss +489 -0
- package/src/components/flow-builder/flow-builder.test.ts +59 -0
- package/src/components/flow-builder/flow-layout.ts +139 -0
- package/src/components/flow-builder/flow-registry.ts +52 -0
- package/src/components/flow-builder/flow.types.ts +407 -0
- package/src/components/flow-builder/index.ts +14 -0
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +1086 -0
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.scss +371 -0
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.test.ts +39 -0
- package/src/components/flow-builder/modules/flow-canvas/index.ts +12 -0
- package/src/components/flow-builder/modules/flow-node/flow-node.component.ts +174 -0
- package/src/components/flow-builder/modules/flow-node/flow-node.scss +180 -0
- package/src/components/flow-builder/modules/flow-node/flow-node.test.ts +50 -0
- package/src/components/flow-builder/modules/flow-node/index.ts +12 -0
- package/src/components/flow-builder/modules/flow-step/flow-step.component.ts +88 -0
- package/src/components/flow-builder/modules/flow-step/flow-step.scss +52 -0
- package/src/components/flow-builder/modules/flow-step/flow-step.test.ts +21 -0
- package/src/components/flow-builder/modules/flow-step/index.ts +12 -0
- package/src/components/flow-builder/modules/flow-step-group/flow-step-group.component.ts +35 -0
- package/src/components/flow-builder/modules/flow-step-group/flow-step-group.scss +28 -0
- package/src/components/flow-builder/modules/flow-step-group/flow-step-group.test.ts +20 -0
- package/src/components/flow-builder/modules/flow-step-group/index.ts +12 -0
- package/src/components/flow-builder/modules/flow-steps/flow-steps.component.ts +88 -0
- package/src/components/flow-builder/modules/flow-steps/flow-steps.scss +24 -0
- package/src/components/flow-builder/modules/flow-steps/flow-steps.test.ts +17 -0
- package/src/components/flow-builder/modules/flow-steps/index.ts +12 -0
- package/src/events/events.ts +3 -0
- package/src/events/zn-flow-change.ts +9 -0
- package/src/events/zn-flow-connect.ts +9 -0
- package/src/events/zn-flow-selection-change.ts +7 -0
- package/src/zinc.ts +4 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
---
|
|
2
|
+
meta:
|
|
3
|
+
title: Flow Builder
|
|
4
|
+
description: A drag-and-drop visual automation builder with a steps panel, a pan/zoom canvas, and a configuration inspector. Extend it with your own node types via a JS registry.
|
|
5
|
+
layout: component
|
|
6
|
+
fullWidth: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
The Flow Builder is a three-panel editor for visual automations, with an optional header action bar:
|
|
10
|
+
|
|
11
|
+
- **Header** (optional) — a full-width action bar rendered only when `slot="header-left"` / `slot="header-right"`
|
|
12
|
+
content is provided (e.g. Close / Undo All Changes on the left, Apply Changes on the right).
|
|
13
|
+
- **Left panel** — the steps panel (searchable, tabbed by step group).
|
|
14
|
+
- **Canvas** (center) — a pannable, zoomable surface. Connections are **append-only**: click any **output port**
|
|
15
|
+
on a node to start a **stray branch** — the wire follows your cursor until you click a node (or its output
|
|
16
|
+
port) to attach it. Fan-in is allowed, and so are **loops** — a branch may point back to an earlier step (e.g.
|
|
17
|
+
an answer that restarts the questioning); only wiring a node directly to itself is refused. Cancel by clicking
|
|
18
|
+
empty canvas, pressing Esc, or leaving the window. A connected output port spawns a brand-new branch on click, so extra arrows never require
|
|
19
|
+
config forms. Open outputs draw nothing while idle — their **`+`** targets appear only while dragging a step in
|
|
20
|
+
or moving one. Every existing wire has a **`+`** at its midpoint to insert a step between two nodes. New
|
|
21
|
+
steps are created by dragging them from the panel onto the canvas or a `+`. Reposition nodes by dragging, and add
|
|
22
|
+
resizable sticky notes. Scroll to pan (side-scroll or Shift+scroll pans horizontally); Ctrl/Cmd+scroll or
|
|
23
|
+
trackpad pinch zooms toward the cursor. Toolbar: undo, redo, add-note, **untangle** (auto-arranges a messy flow
|
|
24
|
+
into evenly spaced layers that follow the connections — undoable), zoom-in, reset, zoom-out.
|
|
25
|
+
- **Right panel** — the sidebar by default: a slot (`slot="sidebar"`) for status / version history, with a built-in
|
|
26
|
+
Configuration Errors summary derived from `errorNodes`. Selecting a node slides that step's configuration in from
|
|
27
|
+
the right (driven by the node type's `renderConfig`); clicking a **branch pill** on a wire slides in the branch
|
|
28
|
+
editor instead — rename the branch and configure the filters/conditions for taking that path (the node type's
|
|
29
|
+
`renderBranchConfig`, persisted on the output port's `data`).
|
|
30
|
+
|
|
31
|
+
A node's **`…`** menu offers Delete, Duplicate, and **Move** — Move detaches the step (with its subtree) so you can
|
|
32
|
+
re-attach it to a different `+` slot.
|
|
33
|
+
|
|
34
|
+
Every node except an entrypoint (a node with `inputs=""`) can have as many inputs and outputs as it likes — outputs
|
|
35
|
+
are per-node overridable, so a step's config can add and remove branches at runtime.
|
|
36
|
+
|
|
37
|
+
It ships with **no built-in node types** — every step you see below is registered by the consumer. This keeps the
|
|
38
|
+
component generic and lets it be extended for any future workflow without changing the library.
|
|
39
|
+
|
|
40
|
+
<zn-button href="/components/flow-builder-demo/" target="_blank" icon="open_in_new" style="margin-bottom: 1.5rem;">
|
|
41
|
+
Open the full-page demo
|
|
42
|
+
</zn-button>
|
|
43
|
+
<zn-button href="/components/flow-builder-troubleshooter-demo/" target="_blank" icon="open_in_new" style="margin-bottom: 1.5rem;">
|
|
44
|
+
Open the Troubleshoot Q&A demo
|
|
45
|
+
</zn-button>
|
|
46
|
+
|
|
47
|
+
```html:preview
|
|
48
|
+
|
|
49
|
+
<div style="height: 900px;">
|
|
50
|
+
<zn-flow-builder
|
|
51
|
+
id="flow-demo"
|
|
52
|
+
heading="Retention – Annual Cancellation_v1"
|
|
53
|
+
subheading="Last updated 16th June 2026"
|
|
54
|
+
triggers-hint="Automations require a trigger to start. Choose a trigger below and drag it to the canvas to begin."
|
|
55
|
+
actions-hint="Actions run as the automation progresses. Drag one onto the canvas or a + slot."
|
|
56
|
+
rules-hint="Rules branch the flow based on conditions you configure on each step.">
|
|
57
|
+
|
|
58
|
+
<zn-flow-step type="webhook" group="entrypoint" category="Events"
|
|
59
|
+
icon="webhook@lu" color="rgb(236, 68, 91)"
|
|
60
|
+
description="User hits webhook XYZ">Cancellation Requested
|
|
61
|
+
</zn-flow-step>
|
|
62
|
+
<zn-flow-step type="manual" group="entrypoint" category="Contacts"
|
|
63
|
+
icon="user-plus@lu" color="rgb(236, 68, 91)">Contact added manually
|
|
64
|
+
</zn-flow-step>
|
|
65
|
+
<zn-flow-step type="list-add" group="entrypoint" category="Contacts"
|
|
66
|
+
icon="list-plus@lu" color="rgb(236, 68, 91)">Contact added to a list
|
|
67
|
+
</zn-flow-step>
|
|
68
|
+
|
|
69
|
+
<zn-flow-step type="email-parser" group="action" category="Email"
|
|
70
|
+
icon="mail@lu" color="rgb(43, 192, 145)">IMAP Email Parser
|
|
71
|
+
</zn-flow-step>
|
|
72
|
+
<zn-flow-step type="send-reply" group="action" category="Email"
|
|
73
|
+
icon="send@lu" color="rgb(43, 192, 145)">Send Reply
|
|
74
|
+
</zn-flow-step>
|
|
75
|
+
<zn-flow-step type="claude" group="action" category="AI"
|
|
76
|
+
icon="bot@lu" color="rgb(43, 192, 145)">Claude
|
|
77
|
+
</zn-flow-step>
|
|
78
|
+
<zn-flow-step type="notify" group="action" category="Support"
|
|
79
|
+
icon="user@lu" color="rgb(43, 192, 145)">Notify User
|
|
80
|
+
</zn-flow-step>
|
|
81
|
+
|
|
82
|
+
<zn-flow-step type="split" group="rule" category="Logic"
|
|
83
|
+
icon="split@lu" color="rgb(105, 54, 245)"
|
|
84
|
+
outputs='[{"id":"true","label":"TRUE"},{"id":"false","label":"FALSE"}]'>Conditional Split
|
|
85
|
+
</zn-flow-step>
|
|
86
|
+
|
|
87
|
+
<div slot="sidebar" class="flow-sidebar-demo">
|
|
88
|
+
<label>Status
|
|
89
|
+
<zn-select>
|
|
90
|
+
<zn-option selected>Active</zn-option>
|
|
91
|
+
<zn-option>Paused</zn-option>
|
|
92
|
+
<zn-option>Draft</zn-option>
|
|
93
|
+
</zn-select>
|
|
94
|
+
</label>
|
|
95
|
+
<h4>Version History</h4>
|
|
96
|
+
<ul>
|
|
97
|
+
<li><strong>John Smith</strong><span>16th June 2026 · 14:03</span></li>
|
|
98
|
+
<li><strong>John Smith</strong><span>16th June 2026 · 11:20</span></li>
|
|
99
|
+
<li><strong>Jane Doe</strong><span>15th June 2026 · 09:48</span></li>
|
|
100
|
+
</ul>
|
|
101
|
+
</div>
|
|
102
|
+
</zn-flow-builder>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<style>
|
|
106
|
+
.flow-sidebar-demo {
|
|
107
|
+
margin-top: 16px;
|
|
108
|
+
font-size: 0.8125rem;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.flow-sidebar-demo label {
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
gap: 4px;
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.flow-sidebar-demo select {
|
|
119
|
+
padding: 6px 8px;
|
|
120
|
+
border: 1px solid rgb(var(--zn-border-color));
|
|
121
|
+
border-radius: 6px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.flow-sidebar-demo h4 {
|
|
125
|
+
margin: 16px 0 8px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.flow-sidebar-demo ul {
|
|
129
|
+
list-style: none;
|
|
130
|
+
margin: 0;
|
|
131
|
+
padding: 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.flow-sidebar-demo li {
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
padding: 8px 0;
|
|
138
|
+
border-top: 1px solid rgb(var(--zn-border-color));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.flow-sidebar-demo li span {
|
|
142
|
+
color: rgb(var(--zn-color-muted-text));
|
|
143
|
+
font-size: 0.75rem;
|
|
144
|
+
}
|
|
145
|
+
</style>
|
|
146
|
+
|
|
147
|
+
<script type="module">
|
|
148
|
+
const initial = {
|
|
149
|
+
nodes: [
|
|
150
|
+
{id: 'n1', type: 'webhook', x: 280, y: 60, data: {subtitle: 'User hits webhook XYZ'}},
|
|
151
|
+
{id: 'n2', type: 'split', x: 280, y: 220, data: {subtitle: 'ARR Threshold'}},
|
|
152
|
+
{id: 'n3', type: 'email-parser', x: 80, y: 480, data: {subtitle: 'Import Email Attachment'}},
|
|
153
|
+
{id: 'n4', type: 'notify', x: 80, y: 640, data: {subtitle: 'Notify Frank Furt'}},
|
|
154
|
+
{id: 'n5', type: 'claude', x: 480, y: 480, data: {subtitle: 'Generate Reply'}},
|
|
155
|
+
{id: 'n6', type: 'send-reply', x: 480, y: 640, data: {subtitle: 'Generate Reply'}}
|
|
156
|
+
],
|
|
157
|
+
connections: [
|
|
158
|
+
{id: 'c1', source: {node: 'n1', port: 'out'}, target: {node: 'n2', port: 'in'}},
|
|
159
|
+
{id: 'c2', source: {node: 'n2', port: 'true'}, target: {node: 'n3', port: 'in'}},
|
|
160
|
+
{id: 'c3', source: {node: 'n2', port: 'false'}, target: {node: 'n5', port: 'in'}},
|
|
161
|
+
{id: 'c4', source: {node: 'n3', port: 'out'}, target: {node: 'n4', port: 'in'}},
|
|
162
|
+
{id: 'c5', source: {node: 'n5', port: 'out'}, target: {node: 'n6', port: 'in'}}
|
|
163
|
+
],
|
|
164
|
+
notes: [
|
|
165
|
+
{id: 'note1', x: 760, y: 440, text: 'This is a sticky note.'}
|
|
166
|
+
]
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// The Conditional Split's outputs are user-configurable per node: its renderConfig
|
|
170
|
+
// edits node.outputs (add / rename / remove branches). The canvas re-renders the
|
|
171
|
+
// outputs and the builder prunes connections to any removed branch.
|
|
172
|
+
const splitType = {
|
|
173
|
+
type: 'split',
|
|
174
|
+
label: 'Conditional Split',
|
|
175
|
+
group: 'rule',
|
|
176
|
+
category: 'Logic',
|
|
177
|
+
icon: 'split@lu',
|
|
178
|
+
color: 'rgb(105, 54, 245)',
|
|
179
|
+
outputs: [{id: 'true', label: 'TRUE'}, {id: 'false', label: 'FALSE'}],
|
|
180
|
+
renderConfig: (node, update) =>
|
|
181
|
+
{
|
|
182
|
+
const branches = node.outputs ?? [{id: 'true', label: 'TRUE'}, {id: 'false', label: 'FALSE'}];
|
|
183
|
+
const setBranches = (next) =>
|
|
184
|
+
{
|
|
185
|
+
node.outputs = next;
|
|
186
|
+
update({});
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const wrap = document.createElement('div');
|
|
190
|
+
wrap.style.cssText = 'display:flex;flex-direction:column;gap:8px';
|
|
191
|
+
|
|
192
|
+
const heading = document.createElement('p');
|
|
193
|
+
heading.textContent = 'Branches';
|
|
194
|
+
heading.style.cssText = 'margin:0;font-weight:600;font-size:0.8125rem';
|
|
195
|
+
wrap.appendChild(heading);
|
|
196
|
+
|
|
197
|
+
branches.forEach((branch, i) =>
|
|
198
|
+
{
|
|
199
|
+
const row = document.createElement('div');
|
|
200
|
+
row.style.cssText = 'display:flex;gap:6px;align-items:center';
|
|
201
|
+
|
|
202
|
+
const input = document.createElement('zn-input');
|
|
203
|
+
input.setAttribute('size', 'small');
|
|
204
|
+
input.value = branch.label ?? branch.id;
|
|
205
|
+
input.style.flex = '1';
|
|
206
|
+
input.addEventListener('change', () =>
|
|
207
|
+
setBranches(branches.map((b, j) => (j === i ? {...b, label: input.value} : b))));
|
|
208
|
+
|
|
209
|
+
const remove = document.createElement('zn-button');
|
|
210
|
+
remove.setAttribute('icon', 'x@lu');
|
|
211
|
+
remove.setAttribute('icon-button', 'small');
|
|
212
|
+
remove.setAttribute('plain', '');
|
|
213
|
+
remove.addEventListener('click', () => setBranches(branches.filter((_, j) => j !== i)));
|
|
214
|
+
|
|
215
|
+
row.append(input, remove);
|
|
216
|
+
wrap.appendChild(row);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const add = document.createElement('zn-button');
|
|
220
|
+
add.setAttribute('size', 'small');
|
|
221
|
+
add.setAttribute('icon', 'plus@lu');
|
|
222
|
+
add.textContent = 'Add branch';
|
|
223
|
+
add.addEventListener('click', () =>
|
|
224
|
+
setBranches([...branches, {id: 'branch-' + (branches.length + 1), label: 'New branch'}]));
|
|
225
|
+
wrap.appendChild(add);
|
|
226
|
+
|
|
227
|
+
return wrap;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
customElements.whenDefined('zn-flow-builder').then(() =>
|
|
232
|
+
{
|
|
233
|
+
const el = document.getElementById('flow-demo');
|
|
234
|
+
el.registerNodeType(splitType);
|
|
235
|
+
el.setState(initial);
|
|
236
|
+
el.errorNodes = ['n3'];
|
|
237
|
+
});
|
|
238
|
+
</script>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Examples
|
|
242
|
+
|
|
243
|
+
### Empty builder
|
|
244
|
+
|
|
245
|
+
With no node types registered, the steps panel is empty. Register types to populate it.
|
|
246
|
+
|
|
247
|
+
```html:preview
|
|
248
|
+
|
|
249
|
+
<div style="height: 560px;">
|
|
250
|
+
<zn-flow-builder heading="New Flow" subheading="Drafting"></zn-flow-builder>
|
|
251
|
+
</div>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Declaring the steps panel
|
|
255
|
+
|
|
256
|
+
The builder owns the steps panel: **Entrypoints / Triggers / Actions / Rules** tabs with a search box.
|
|
257
|
+
You don't author the tabs or their layout — you just declare the node types, and each is routed to the
|
|
258
|
+
right tab by its `group` and grouped under a collapsible `category`. A tab only renders when its
|
|
259
|
+
group has at least one registered step, so unused groups never appear.
|
|
260
|
+
|
|
261
|
+
Declare each type as a `<zn-flow-step>` (the items themselves are never shown —
|
|
262
|
+
they're declarations that drive the rendered panel):
|
|
263
|
+
|
|
264
|
+
- `type` (required) — the node type id.
|
|
265
|
+
- `group` — `entrypoint` | `trigger` | `action` | `rule`; picks the tab (defaults to `action`).
|
|
266
|
+
- `category` — collapsible group within the tab (e.g. "Email").
|
|
267
|
+
- label — the slotted text (or a `label` attribute).
|
|
268
|
+
- `icon`, `icon-library`, `color`, `description`.
|
|
269
|
+
- `inputs` — a JSON array of input ports. Omit for a single default input; use `inputs=""` for a
|
|
270
|
+
trigger with no input.
|
|
271
|
+
- `outputs` — a JSON array of ports, each a string id or a `{"id","label"}` object (e.g.
|
|
272
|
+
`outputs='[{"id":"true","label":"TRUE"},{"id":"false","label":"FALSE"}]'`). Omit for a single default output.
|
|
273
|
+
|
|
274
|
+
Set an optional per-tab hint with the `triggers-hint` / `actions-hint` / `rules-hint` attributes on the
|
|
275
|
+
builder. (A node's inspector `renderConfig` can't be expressed in markup — supply it via
|
|
276
|
+
`registerNodeTypes()` for the same `type` id if you need a custom config panel.)
|
|
277
|
+
|
|
278
|
+
```html
|
|
279
|
+
|
|
280
|
+
<zn-flow-builder triggers-hint="Choose a trigger and drag it to the canvas.">
|
|
281
|
+
<zn-flow-step type="webhook" group="trigger" category="Events"
|
|
282
|
+
icon="webhook@lu" color="rgb(236,68,91)" inputs=""
|
|
283
|
+
description="User hits webhook XYZ">Cancellation Requested</zn-flow-step>
|
|
284
|
+
|
|
285
|
+
<zn-flow-step type="split" group="rule" category="Logic" icon="split@lu"
|
|
286
|
+
outputs='[{"id":"true","label":"TRUE"},{"id":"false","label":"FALSE"}]'>Conditional Split</zn-flow-step>
|
|
287
|
+
</zn-flow-builder>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Registering custom node types
|
|
291
|
+
|
|
292
|
+
Node types are the modular extension point. Instead of (or alongside) slotted steps, register an
|
|
293
|
+
array of `FlowNodeType` objects with `registerNodeTypes()` (or set the `.nodeTypes` property) — the two
|
|
294
|
+
sources merge into the same panel. Each type's `group`/`category` place it in the tabs; its icon,
|
|
295
|
+
color, input/output ports, and optional `renderConfig` define the node's behavior and inspector.
|
|
296
|
+
|
|
297
|
+
```js
|
|
298
|
+
import {html} from 'lit';
|
|
299
|
+
|
|
300
|
+
const builder = document.querySelector('zn-flow-builder');
|
|
301
|
+
|
|
302
|
+
builder.registerNodeTypes([
|
|
303
|
+
{
|
|
304
|
+
type: 'send-email',
|
|
305
|
+
label: 'Send Email',
|
|
306
|
+
group: 'action', // 'trigger' | 'action' | 'rule' — which steps-panel tab
|
|
307
|
+
category: 'Email', // collapsible group within the tab
|
|
308
|
+
icon: 'mail@lu',
|
|
309
|
+
color: 'rgb(43, 192, 145)',
|
|
310
|
+
defaultData: {subject: ''},
|
|
311
|
+
// Inspector UI for a selected node of this type:
|
|
312
|
+
renderConfig: (node, update) => html`
|
|
313
|
+
<zn-input
|
|
314
|
+
label="Subject"
|
|
315
|
+
.value=${node.data.subject ?? ''}
|
|
316
|
+
@zn-input=${e => update({subject: e.target.value})}></zn-input>
|
|
317
|
+
`,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
type: 'ab-split',
|
|
321
|
+
label: 'A/B Split',
|
|
322
|
+
group: 'rule',
|
|
323
|
+
icon: 'split@lu',
|
|
324
|
+
// Multiple outputs become labelled connection points (TRUE / FALSE here):
|
|
325
|
+
outputs: [{id: 'true', label: 'TRUE'}, {id: 'false', label: 'FALSE'}],
|
|
326
|
+
},
|
|
327
|
+
]);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
A node type with `inputs: []` is a starting point (a trigger) with no incoming port. A type with
|
|
331
|
+
multiple `outputs` renders one labeled port per output, so a branch can fan out to different steps.
|
|
332
|
+
|
|
333
|
+
#### User-configurable outputs
|
|
334
|
+
|
|
335
|
+
A type's `outputs` are the default. For steps whose branches are defined by the user (e.g. a conditional
|
|
336
|
+
split), set **`node.outputs`** on the instance from `renderConfig` — the canvas renders those ports, and
|
|
337
|
+
the builder prunes any connection whose branch is removed. `node.inputs` works the same way. Set the new
|
|
338
|
+
ports then call `update({})` to commit:
|
|
339
|
+
|
|
340
|
+
```js
|
|
341
|
+
renderConfig: (node, update) => {
|
|
342
|
+
const branches = node.outputs ?? [{id: 'true', label: 'TRUE'}, {id: 'false', label: 'FALSE'}];
|
|
343
|
+
const addBranch = () => {
|
|
344
|
+
node.outputs = [...branches, {id: crypto.randomUUID(), label: 'New branch'}];
|
|
345
|
+
update({}); // commit — re-renders the outputs, prunes dropped branches' connections
|
|
346
|
+
};
|
|
347
|
+
// …render an editor for `branches` that calls addBranch / removes / renames…
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Reacting to changes
|
|
352
|
+
|
|
353
|
+
The builder emits events as the flow is edited. Use `zn-flow-change` to persist, or read `.value`
|
|
354
|
+
(a JSON string of the full `FlowState`) at any time.
|
|
355
|
+
|
|
356
|
+
```js
|
|
357
|
+
const builder = document.querySelector('zn-flow-builder');
|
|
358
|
+
|
|
359
|
+
builder.addEventListener('zn-flow-change', e =>
|
|
360
|
+
{
|
|
361
|
+
console.log('flow updated', e.detail.state);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
builder.addEventListener('zn-flow-selection-change', e =>
|
|
365
|
+
{
|
|
366
|
+
console.log('selected node', e.detail.nodeId);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
builder.addEventListener('zn-flow-connect', e =>
|
|
370
|
+
{
|
|
371
|
+
console.log('new connection', e.detail.connection);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Persist / restore:
|
|
375
|
+
localStorage.setItem('flow', builder.value);
|
|
376
|
+
builder.value = localStorage.getItem('flow');
|
|
377
|
+
```
|
package/package.json
CHANGED
|
@@ -60,7 +60,7 @@ export default class ZnButton extends ZincElement implements ZincFormControl {
|
|
|
60
60
|
@queryAll('.loading-countdown') countdownContainer: HTMLElement[];
|
|
61
61
|
|
|
62
62
|
@property({}) color: 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' |
|
|
63
|
-
'transparent' | 'star' |
|
|
63
|
+
'transparent' | 'star' | (string & Record<never, never>) = 'default';
|
|
64
64
|
@property({attribute: 'hover-color'}) hoverColor: string;
|
|
65
65
|
@property({type: Boolean}) text = false;
|
|
66
66
|
@property({type: Boolean}) outline = false;
|
|
@@ -388,7 +388,6 @@ export default class ZnButton extends ZincElement implements ZincFormControl {
|
|
|
388
388
|
'button--warning': this.color === 'warning',
|
|
389
389
|
'button--transparent': this.color === 'transparent',
|
|
390
390
|
'button--star': this.color === 'star',
|
|
391
|
-
'button--white': this.color === 'white',
|
|
392
391
|
'button--outline': this.outline,
|
|
393
392
|
'button--text': (this.text && !this.outline),
|
|
394
393
|
'button--icon-button': !!this.iconButton,
|
|
@@ -244,22 +244,6 @@
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
/**
|
|
248
|
-
* White: white panel background with a neutral border, matching the
|
|
249
|
-
* icon-button look but for normal (auto-width, text) buttons.
|
|
250
|
-
*
|
|
251
|
-
* Doubled-up selector so it out-cascades the standard/secondary combos.
|
|
252
|
-
*/
|
|
253
|
-
.button.button--white {
|
|
254
|
-
background: rgb(var(--zn-panel));
|
|
255
|
-
border-color: var(--zn-button-border-color, rgb(var(--zn-border-color)));
|
|
256
|
-
color: rgb(var(--zn-text));
|
|
257
|
-
|
|
258
|
-
&:hover:not([disabled]), &:active:not([disabled]) {
|
|
259
|
-
background: rgba(var(--zn-color-tab), 0.5);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
247
|
/**
|
|
264
248
|
* Icon buttons
|
|
265
249
|
*
|
|
@@ -489,4 +473,10 @@
|
|
|
489
473
|
|
|
490
474
|
.button--panel-bg {
|
|
491
475
|
background: rgb(var(--zn-panel));
|
|
476
|
+
border-color: var(--zn-button-border-color, rgb(var(--zn-border-color)));
|
|
477
|
+
color: rgb(var(--zn-text));
|
|
478
|
+
|
|
479
|
+
&:hover:not([disabled]), &:active:not([disabled]) {
|
|
480
|
+
background: rgba(var(--zn-color-tab), 0.5);
|
|
481
|
+
}
|
|
492
482
|
}
|
|
@@ -20,6 +20,8 @@ import styles from './collapsible.scss';
|
|
|
20
20
|
*
|
|
21
21
|
* @slot header - Clicking will toggle the show state of the data
|
|
22
22
|
*
|
|
23
|
+
* @csspart header - The header row (toggle).
|
|
24
|
+
* @csspart caption - The caption text.
|
|
23
25
|
*/
|
|
24
26
|
export default class ZnCollapsible extends ZincElement {
|
|
25
27
|
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
@@ -51,7 +53,7 @@ export default class ZnCollapsible extends ZincElement {
|
|
|
51
53
|
|
|
52
54
|
protected _store: Store;
|
|
53
55
|
|
|
54
|
-
private readonly hasSlotController = new HasSlotController(this, '[default]', 'header', 'caption', 'label');
|
|
56
|
+
private readonly hasSlotController = new HasSlotController(this, '[default]', 'header', 'caption', 'label', 'description');
|
|
55
57
|
|
|
56
58
|
private readonly observer = new MutationController(this, {
|
|
57
59
|
target: null,
|
|
@@ -137,14 +139,17 @@ export default class ZnCollapsible extends ZincElement {
|
|
|
137
139
|
|
|
138
140
|
return html`
|
|
139
141
|
<div @click="${() => (!this.expanded ? (this.expanded = true) : '')}">
|
|
140
|
-
<slot name="header" class="header" @click="${this.handleCollapse}">
|
|
142
|
+
<slot name="header" part="header" class="header" @click="${this.handleCollapse}">
|
|
141
143
|
<div class="header__left">
|
|
142
|
-
<p class="caption">
|
|
144
|
+
<p part="caption" class="caption">
|
|
143
145
|
<slot name="caption">${this.caption}</slot>
|
|
144
146
|
</p>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
${this.description || this.hasSlotController.test('description')
|
|
148
|
+
? html`
|
|
149
|
+
<p class="description">
|
|
150
|
+
<slot name="description">${this.description}</slot>
|
|
151
|
+
</p>`
|
|
152
|
+
: ''}
|
|
148
153
|
</div>
|
|
149
154
|
<div class="header__right">
|
|
150
155
|
<slot name="label"><p class="label">${this.label}</p></slot>
|
|
@@ -722,14 +722,14 @@ export default class ZnDataTable extends ZincElement {
|
|
|
722
722
|
|
|
723
723
|
return html`
|
|
724
724
|
<div class="table__footer__pagination-buttons">
|
|
725
|
-
<zn-button
|
|
726
|
-
@click="${this.page !== 1 ? this.goToFirstPage : undefined}"
|
|
725
|
+
<zn-button @click="${this.page !== 1 ? this.goToFirstPage : undefined}"
|
|
727
726
|
?disabled="${this.page === 1}"
|
|
728
|
-
icon="arrow_left@lu"
|
|
727
|
+
icon="arrow_left@lu"
|
|
728
|
+
panel-bg>First
|
|
729
729
|
</zn-button>
|
|
730
|
-
<zn-button
|
|
731
|
-
|
|
732
|
-
|
|
730
|
+
<zn-button @click="${this.page !== 1 ? this.goToPreviousPage : undefined}"
|
|
731
|
+
?disabled="${this.page === 1}"
|
|
732
|
+
panel-bg>Back
|
|
733
733
|
</zn-button>
|
|
734
734
|
${this.getPageRange().map((p) => p === 'ellipsis'
|
|
735
735
|
? html`<span class="table__footer__pagination-ellipsis">…</span>`
|
|
@@ -739,15 +739,15 @@ export default class ZnDataTable extends ZincElement {
|
|
|
739
739
|
class="${p === this.page ? 'table__footer__pagination-page--active' : ''}">${p}
|
|
740
740
|
</zn-button>`
|
|
741
741
|
)}
|
|
742
|
-
<zn-button
|
|
743
|
-
|
|
744
|
-
|
|
742
|
+
<zn-button @click="${this.page !== this.totalPages ? this.goToNextPage : undefined}"
|
|
743
|
+
?disabled="${this.page === this.totalPages}"
|
|
744
|
+
panel-bg>Next
|
|
745
745
|
</zn-button>
|
|
746
|
-
<zn-button
|
|
747
|
-
@click="${this.page !== this.totalPages ? this.goToLastPage : undefined}"
|
|
746
|
+
<zn-button @click="${this.page !== this.totalPages ? this.goToLastPage : undefined}"
|
|
748
747
|
?disabled="${this.page === this.totalPages}"
|
|
749
748
|
icon="arrow_right@lu"
|
|
750
|
-
icon-position="right"
|
|
749
|
+
icon-position="right"
|
|
750
|
+
panel-bg>Last
|
|
751
751
|
</zn-button>
|
|
752
752
|
</div>`;
|
|
753
753
|
}
|