@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.
Files changed (48) hide show
  1. package/custom-elements-manifest.config.js +2 -4
  2. package/dist/custom-elements.json +1172 -78
  3. package/dist/vscode.html-custom-data.json +34 -13
  4. package/dist/web-types.json +135 -26
  5. package/dist/zn.d.ts +867 -1
  6. package/dist/zn.min.js +893 -481
  7. package/docs/_includes/default.njk +1 -0
  8. package/docs/_includes/full-page.njk +38 -0
  9. package/docs/pages/components/flow-builder-demo.njk +194 -0
  10. package/docs/pages/components/flow-builder-troubleshooter-demo.njk +282 -0
  11. package/docs/pages/components/flow-builder.md +377 -0
  12. package/package.json +1 -1
  13. package/src/components/button/button.component.ts +1 -2
  14. package/src/components/button/button.scss +6 -16
  15. package/src/components/collapsible/collapsible.component.ts +11 -6
  16. package/src/components/data-table/data-table.component.ts +12 -12
  17. package/src/components/flow-builder/flow-builder.component.ts +1171 -0
  18. package/src/components/flow-builder/flow-builder.scss +489 -0
  19. package/src/components/flow-builder/flow-builder.test.ts +59 -0
  20. package/src/components/flow-builder/flow-layout.ts +139 -0
  21. package/src/components/flow-builder/flow-registry.ts +52 -0
  22. package/src/components/flow-builder/flow.types.ts +407 -0
  23. package/src/components/flow-builder/index.ts +14 -0
  24. package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +1086 -0
  25. package/src/components/flow-builder/modules/flow-canvas/flow-canvas.scss +371 -0
  26. package/src/components/flow-builder/modules/flow-canvas/flow-canvas.test.ts +39 -0
  27. package/src/components/flow-builder/modules/flow-canvas/index.ts +12 -0
  28. package/src/components/flow-builder/modules/flow-node/flow-node.component.ts +174 -0
  29. package/src/components/flow-builder/modules/flow-node/flow-node.scss +180 -0
  30. package/src/components/flow-builder/modules/flow-node/flow-node.test.ts +50 -0
  31. package/src/components/flow-builder/modules/flow-node/index.ts +12 -0
  32. package/src/components/flow-builder/modules/flow-step/flow-step.component.ts +88 -0
  33. package/src/components/flow-builder/modules/flow-step/flow-step.scss +52 -0
  34. package/src/components/flow-builder/modules/flow-step/flow-step.test.ts +21 -0
  35. package/src/components/flow-builder/modules/flow-step/index.ts +12 -0
  36. package/src/components/flow-builder/modules/flow-step-group/flow-step-group.component.ts +35 -0
  37. package/src/components/flow-builder/modules/flow-step-group/flow-step-group.scss +28 -0
  38. package/src/components/flow-builder/modules/flow-step-group/flow-step-group.test.ts +20 -0
  39. package/src/components/flow-builder/modules/flow-step-group/index.ts +12 -0
  40. package/src/components/flow-builder/modules/flow-steps/flow-steps.component.ts +88 -0
  41. package/src/components/flow-builder/modules/flow-steps/flow-steps.scss +24 -0
  42. package/src/components/flow-builder/modules/flow-steps/flow-steps.test.ts +17 -0
  43. package/src/components/flow-builder/modules/flow-steps/index.ts +12 -0
  44. package/src/events/events.ts +3 -0
  45. package/src/events/zn-flow-change.ts +9 -0
  46. package/src/events/zn-flow-connect.ts +9 -0
  47. package/src/events/zn-flow-selection-change.ts +7 -0
  48. package/src/zinc.ts +4 -0
@@ -0,0 +1,180 @@
1
+ @use "../../../../wc";
2
+
3
+ :host {
4
+ --node-accent: rgb(var(--zn-color-primary));
5
+ --port-size: 11px;
6
+
7
+ position: absolute;
8
+ top: 0;
9
+ left: 0;
10
+ display: block;
11
+ user-select: none;
12
+ touch-action: none;
13
+ }
14
+
15
+ :host([dragging]) .card {
16
+ cursor: grabbing;
17
+ }
18
+
19
+ .card {
20
+ position: relative;
21
+ display: flex;
22
+ flex-direction: column;
23
+ justify-content: center;
24
+ width: 100%;
25
+ height: 100%;
26
+ padding: 0 10px;
27
+ background: rgb(var(--zn-panel));
28
+ border: 1px solid rgb(var(--zn-border-color));
29
+ border-radius: 10px;
30
+ box-shadow: 0 1px 2px rgba(var(--zn-shadow), 0.4);
31
+ cursor: grab;
32
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
33
+
34
+ &:hover {
35
+ border-color: rgb(var(--zn-color-border-active));
36
+ }
37
+
38
+ &.selected {
39
+ border-color: var(--node-accent);
40
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--node-accent) 35%, transparent);
41
+ }
42
+
43
+ // A stray branch is snapped here — glow as the drop target.
44
+ &.link-target {
45
+ border-color: rgb(var(--zn-color-primary));
46
+ box-shadow: 0 0 0 3px color-mix(in srgb, rgb(var(--zn-color-primary)) 30%, transparent);
47
+ }
48
+
49
+ &.error {
50
+ border-color: rgb(var(--zn-color-error));
51
+ background: color-mix(in srgb, rgb(var(--zn-color-error)) 6%, rgb(var(--zn-panel)));
52
+ }
53
+ }
54
+
55
+ .body {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 10px;
59
+ }
60
+
61
+ .icon-tile {
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ flex: 0 0 auto;
66
+ width: 36px;
67
+ height: 36px;
68
+ border-radius: 8px;
69
+ color: var(--node-accent);
70
+ background: color-mix(in srgb, var(--node-accent) 14%, transparent);
71
+ }
72
+
73
+ .text {
74
+ display: flex;
75
+ flex-direction: column;
76
+ min-width: 0;
77
+ flex: 1 1 auto;
78
+
79
+ .title {
80
+ font-size: 0.8125rem;
81
+ font-weight: 600;
82
+ color: rgb(var(--zn-text));
83
+ white-space: nowrap;
84
+ overflow: hidden;
85
+ text-overflow: ellipsis;
86
+ }
87
+
88
+ .subtitle {
89
+ font-size: 0.75rem;
90
+ color: rgb(var(--zn-color-muted-text));
91
+ white-space: nowrap;
92
+ overflow: hidden;
93
+ text-overflow: ellipsis;
94
+ }
95
+ }
96
+
97
+ .menu {
98
+ flex: 0 0 auto;
99
+ margin-right: -4px;
100
+ }
101
+
102
+ zn-menu-item.danger {
103
+ color: rgb(var(--zn-color-error));
104
+ }
105
+
106
+ .ports {
107
+ position: absolute;
108
+ left: 0;
109
+ width: 100%;
110
+ height: 0;
111
+
112
+ &--in {
113
+ top: 0;
114
+ }
115
+
116
+ &--out {
117
+ bottom: 0;
118
+ }
119
+ }
120
+
121
+ // Anchored at the card edge, at its horizontal position (set inline as left:%).
122
+ .port-wrap {
123
+ position: absolute;
124
+ }
125
+
126
+ .port {
127
+ position: absolute;
128
+ left: 0;
129
+ top: 0;
130
+ transform: translate(-50%, -50%); // centre the dot on both the edge and the port x
131
+ width: var(--port-size);
132
+ height: var(--port-size);
133
+ border-radius: 50%;
134
+ background: rgb(var(--zn-panel));
135
+ border: 2px solid var(--node-accent);
136
+ box-sizing: border-box;
137
+ pointer-events: none;
138
+ }
139
+
140
+ // Input ports ease in and out of the snap highlight with the same timing as
141
+ // the output ports' hover effect.
142
+ .port--in {
143
+ transition: transform 0.12s ease, background 0.12s ease, box-shadow 0.12s ease;
144
+ }
145
+
146
+ // The snapped arrow lands on the input — light the port up while targeted.
147
+ :host([link-target]) .port--in {
148
+ transform: translate(-50%, -50%) scale(1.4);
149
+ background: rgb(var(--zn-color-primary));
150
+ border-color: rgb(var(--zn-color-primary));
151
+ box-shadow: 0 0 0 4px color-mix(in srgb, rgb(var(--zn-color-primary)) 25%, transparent);
152
+ }
153
+
154
+ // Output ports are the branch spawn points: hover to highlight, click to start
155
+ // a branch that follows the cursor to its target node.
156
+ .port--out {
157
+ pointer-events: auto;
158
+ cursor: pointer;
159
+ transition: transform 0.12s ease, background 0.12s ease, box-shadow 0.12s ease;
160
+
161
+ &:hover {
162
+ transform: translate(-50%, -50%) scale(1.4);
163
+ background: var(--node-accent);
164
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--node-accent) 25%, transparent);
165
+ }
166
+ }
167
+
168
+ // Output labels (e.g. TRUE / FALSE) sit just below the card edge, beside the
169
+ // outgoing arrow — never overlapping the card content.
170
+ .port-label {
171
+ position: absolute;
172
+ top: calc(var(--port-size) / 2 + 3px);
173
+ left: 8px;
174
+ font-size: 0.625rem;
175
+ font-weight: 600;
176
+ letter-spacing: 0.02em;
177
+ color: rgb(var(--zn-color-muted-text));
178
+ white-space: nowrap;
179
+ pointer-events: none;
180
+ }
@@ -0,0 +1,50 @@
1
+ import '../../../../../dist/zn.min.js';
2
+ import {expect, fixture, html} from '@open-wc/testing';
3
+ import type {FlowNodeInstance, FlowNodeType} from '../../flow.types';
4
+ import type ZnFlowNode from './flow-node.component';
5
+
6
+ const splitType: FlowNodeType = {
7
+ type: 'split',
8
+ label: 'Conditional Split',
9
+ group: 'rule',
10
+ outputs: [{id: 'true', label: 'TRUE'}, {id: 'false', label: 'FALSE'}],
11
+ };
12
+
13
+ const node: FlowNodeInstance = {id: 'n1', type: 'split', x: 0, y: 0, data: {}};
14
+
15
+ describe('<zn-flow-node>', () => {
16
+ it('should render a component', async () => {
17
+ const el = await fixture<ZnFlowNode>(html`
18
+ <zn-flow-node></zn-flow-node>`);
19
+ expect(el).to.exist;
20
+ });
21
+
22
+ it('should render the type label and one port per output', async () => {
23
+ const el = await fixture<ZnFlowNode>(html`
24
+ <zn-flow-node></zn-flow-node>`);
25
+ el.node = node;
26
+ el.type = splitType;
27
+ await el.updateComplete;
28
+
29
+ expect(el.shadowRoot?.querySelector('.title')?.textContent).to.contain('Conditional Split');
30
+ expect(el.shadowRoot?.querySelectorAll('.port--out')?.length).to.equal(2);
31
+ expect(el.shadowRoot?.querySelector('.port-label')?.textContent).to.contain('TRUE');
32
+ });
33
+
34
+ it('should emit flow-node-grab when the card body is pressed', async () => {
35
+ const el = await fixture<ZnFlowNode>(html`
36
+ <zn-flow-node></zn-flow-node>`);
37
+ el.node = node;
38
+ el.type = splitType;
39
+ await el.updateComplete;
40
+
41
+ let grabbedId = '';
42
+ el.addEventListener('flow-node-grab', (e: Event) => {
43
+ grabbedId = (e as CustomEvent<{ nodeId: string }>).detail.nodeId;
44
+ });
45
+ const card = el.shadowRoot?.querySelector('.card') as HTMLElement;
46
+ card.dispatchEvent(new PointerEvent('pointerdown', {bubbles: true, button: 0}));
47
+
48
+ expect(grabbedId).to.equal('n1');
49
+ });
50
+ });
@@ -0,0 +1,12 @@
1
+ import ZnFlowNode from './flow-node.component';
2
+
3
+ export * from './flow-node.component';
4
+ export default ZnFlowNode;
5
+
6
+ ZnFlowNode.define('zn-flow-node');
7
+
8
+ declare global {
9
+ interface HTMLElementTagNameMap {
10
+ 'zn-flow-node': ZnFlowNode;
11
+ }
12
+ }
@@ -0,0 +1,88 @@
1
+ import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
2
+ import {ifDefined} from 'lit/directives/if-defined.js';
3
+ import {property} from 'lit/decorators.js';
4
+ import ZincElement from '../../../../internal/zinc-element';
5
+ import ZnIcon from '../../../icon';
6
+
7
+ import {emptyDragImage, FLOW_TYPE_MIME} from '../../flow.types';
8
+
9
+ import styles from './flow-step.scss';
10
+
11
+ /**
12
+ * @summary A draggable step in the `<zn-flow-steps>`, representing a registered node type. Drag
13
+ * it onto the canvas (or a "+" slot) of a `<zn-flow-builder>` to add that step.
14
+ * @documentation https://zinc.style/components/flow-step
15
+ * @status experimental
16
+ * @since 1.0
17
+ *
18
+ * @dependency zn-icon
19
+ *
20
+ * @event flow-step-drag - Emitted on dragstart with `detail.type`, so the builder can preview the drop.
21
+ * @event flow-step-drag-end - Emitted on dragend.
22
+ *
23
+ * @slot - The step's label.
24
+ *
25
+ * @csspart base - The row.
26
+ */
27
+ export default class ZnFlowStep extends ZincElement {
28
+ static styles: CSSResultGroup = unsafeCSS(styles);
29
+
30
+ static dependencies = {'zn-icon': ZnIcon};
31
+
32
+ /** The node type id this item creates when dropped. */
33
+ @property() type: string;
34
+ /** Display label; falls back to the slotted text. Also used as the node's label. */
35
+ @property() label: string;
36
+ @property() icon: string;
37
+ @property({attribute: 'icon-library'}) iconLibrary: string;
38
+ @property() color: string;
39
+ @property() description: string;
40
+ /** JSON array of input ports — `""` for a trigger (no input), omit for a single default input. */
41
+ @property() inputs: string;
42
+ /** JSON array of outputs (`"a"` or `{"id","label"}`), e.g. `'[{"id":"true","label":"TRUE"}]'`. Omit for one default output. */
43
+ @property() outputs: string;
44
+
45
+ connectedCallback() {
46
+ super.connectedCallback();
47
+ this.draggable = true;
48
+ this.addEventListener('dragstart', this._onDragStart);
49
+ this.addEventListener('dragend', this._onDragEnd);
50
+ }
51
+
52
+ disconnectedCallback() {
53
+ this.removeEventListener('dragstart', this._onDragStart);
54
+ this.removeEventListener('dragend', this._onDragEnd);
55
+ super.disconnectedCallback();
56
+ }
57
+
58
+ protected updated(changed: PropertyValues) {
59
+ super.updated(changed);
60
+ this.style.setProperty('--node-accent', this.color ?? 'rgb(var(--zn-color-primary))');
61
+ }
62
+
63
+ private _onDragStart = (e: DragEvent) => {
64
+ if (!e.dataTransfer || !this.type) return;
65
+ e.dataTransfer.setData(FLOW_TYPE_MIME, this.type);
66
+ e.dataTransfer.effectAllowed = 'copyMove';
67
+ // Hide the native drag image — the builder shows an in-canvas preview instead.
68
+ e.dataTransfer.setDragImage(emptyDragImage(), 0, 0);
69
+ this.dispatchEvent(new CustomEvent('flow-step-drag', {
70
+ bubbles: true,
71
+ composed: true,
72
+ detail: {type: this.type}
73
+ }));
74
+ };
75
+
76
+ private _onDragEnd = () => {
77
+ this.dispatchEvent(new CustomEvent('flow-step-drag-end', {bubbles: true, composed: true}));
78
+ };
79
+
80
+ render() {
81
+ return html`
82
+ <span part="base" class="item__icon">
83
+ <zn-icon src="${this.icon ?? 'circle'}" library="${ifDefined(this.iconLibrary)}" size="16"></zn-icon>
84
+ </span>
85
+ <span class="item__label"><slot>${this.label ?? ''}</slot></span>
86
+ `;
87
+ }
88
+ }
@@ -0,0 +1,52 @@
1
+ @use "../../../../wc";
2
+
3
+ :host {
4
+ //@include text-style(input-label);
5
+
6
+ --node-accent: rgb(var(--zn-color-primary));
7
+
8
+ display: flex;
9
+ align-items: center;
10
+ gap: 10px;
11
+ padding: 8px 10px;
12
+ margin-bottom: 6px;
13
+ background: rgb(var(--zn-panel));
14
+ border: 1px solid rgb(var(--zn-border-color));
15
+ border-radius: 8px;
16
+ overflow: hidden;
17
+ cursor: grab;
18
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
19
+ max-height: 40px;
20
+ }
21
+
22
+ :host(:hover) {
23
+ border-color: rgb(var(--zn-color-border-active));
24
+ box-shadow: 0 1px 3px rgba(var(--zn-shadow), 0.4);
25
+ }
26
+
27
+ :host(:active) {
28
+ cursor: grabbing;
29
+ }
30
+
31
+ :host([hidden]) {
32
+ display: none;
33
+ }
34
+
35
+ .item__icon {
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ flex: 0 0 auto;
40
+ width: 28px;
41
+ height: 28px;
42
+ border-radius: 6px;
43
+ color: var(--node-accent);
44
+ background: color-mix(in srgb, var(--node-accent) 14%, transparent);
45
+ }
46
+
47
+ .item__label {
48
+ min-width: 0;
49
+ overflow: hidden;
50
+ text-overflow: ellipsis;
51
+ white-space: nowrap;
52
+ }
@@ -0,0 +1,21 @@
1
+ import '../../../../../dist/zn.min.js';
2
+ import {expect, fixture, html} from '@open-wc/testing';
3
+ import type ZnFlowStep from './flow-step.component';
4
+
5
+ describe('<zn-flow-step>', () => {
6
+ it('should render a component', async () => {
7
+ const el = await fixture<ZnFlowStep>(html`
8
+ <zn-flow-step type="webhook">Webhook</zn-flow-step>`);
9
+ expect(el).to.exist;
10
+ expect(el.draggable).to.equal(true);
11
+ });
12
+
13
+ it('should put its type on the dataTransfer at dragstart', async () => {
14
+ const el = await fixture<ZnFlowStep>(html`
15
+ <zn-flow-step type="webhook">Webhook</zn-flow-step>`);
16
+
17
+ const dt = new DataTransfer();
18
+ el.dispatchEvent(new DragEvent('dragstart', {dataTransfer: dt, bubbles: true}));
19
+ expect(dt.getData('application/x-zn-flow-type')).to.equal('webhook');
20
+ });
21
+ });
@@ -0,0 +1,12 @@
1
+ import ZnFlowStep from './flow-step.component';
2
+
3
+ export * from './flow-step.component';
4
+ export default ZnFlowStep;
5
+
6
+ ZnFlowStep.define('zn-flow-step');
7
+
8
+ declare global {
9
+ interface HTMLElementTagNameMap {
10
+ 'zn-flow-step': ZnFlowStep;
11
+ }
12
+ }
@@ -0,0 +1,35 @@
1
+ import {type CSSResultGroup, html, unsafeCSS} from 'lit';
2
+ import {property} from 'lit/decorators.js';
3
+ import ZincElement from '../../../../internal/zinc-element';
4
+ import ZnCollapsible from '../../../collapsible';
5
+
6
+ import styles from './flow-step-group.scss';
7
+
8
+ /**
9
+ * @summary A collapsible category of `<zn-flow-step>`s inside a `<zn-flow-steps>` (typically a `<zn-tabs>` panel).
10
+ * Wraps `<zn-collapsible>` for the standard expand/collapse behaviour and spacing.
11
+ * @documentation https://zinc.style/components/flow-step-group
12
+ * @status experimental
13
+ * @since 1.0
14
+ *
15
+ * @dependency zn-collapsible
16
+ *
17
+ * @slot - The group's `<zn-flow-step>`s.
18
+ */
19
+ export default class ZnFlowStepGroup extends ZincElement {
20
+ static styles: CSSResultGroup = unsafeCSS(styles);
21
+
22
+ static dependencies = {'zn-collapsible': ZnCollapsible};
23
+
24
+ @property() caption: string;
25
+ /** Start collapsed. Defaults to open. */
26
+ @property({type: Boolean, reflect: true}) collapsed = false;
27
+
28
+ render() {
29
+ return html`
30
+ <zn-collapsible caption="${this.caption ?? ''}" default="${this.collapsed ? 'closed' : 'open'}">
31
+ <slot></slot>
32
+ </zn-collapsible>
33
+ `;
34
+ }
35
+ }
@@ -0,0 +1,28 @@
1
+ @use "../../../../wc";
2
+
3
+ :host {
4
+ display: block;
5
+ }
6
+
7
+ :host([hidden]) {
8
+ display: none;
9
+ }
10
+
11
+ zn-collapsible {
12
+ // 16px block padding on the header (inline stays the collapsible's 24px). The
13
+ // !important beats the collapsible's internal `.header` class specificity.
14
+ // align-items centres the caption with the chevron regardless of the header's
15
+ // internal column layout.
16
+ &::part(header) {
17
+ align-items: center !important;
18
+ padding-block: calc(var(--zn-base-px) * 2) !important;
19
+ }
20
+
21
+ // Caption type: Inter SemiBold 14 / 16 (Primary text).
22
+ &::part(caption) {
23
+ font-size: 14px;
24
+ font-weight: 600;
25
+ line-height: 16px;
26
+ color: rgb(var(--zn-text));
27
+ }
28
+ }
@@ -0,0 +1,20 @@
1
+ import '../../../../../dist/zn.min.js';
2
+ import {expect, fixture, html} from '@open-wc/testing';
3
+ import type ZnFlowStepGroup from './flow-step-group.component';
4
+
5
+ describe('<zn-flow-step-group>', () => {
6
+ it('should render a component with its caption', async () => {
7
+ const el = await fixture<ZnFlowStepGroup>(html`
8
+ <zn-flow-step-group caption="Contacts"></zn-flow-step-group>`);
9
+ expect(el.shadowRoot?.querySelector('.header')?.textContent).to.contain('Contacts');
10
+ });
11
+
12
+ it('should toggle collapsed when the header is clicked', async () => {
13
+ const el = await fixture<ZnFlowStepGroup>(html`
14
+ <zn-flow-step-group caption="Contacts"></zn-flow-step-group>`);
15
+ const header = el.shadowRoot?.querySelector('.header') as HTMLElement;
16
+ header.click();
17
+ await el.updateComplete;
18
+ expect(el.collapsed).to.equal(true);
19
+ });
20
+ });
@@ -0,0 +1,12 @@
1
+ import ZnFlowStepGroup from './flow-step-group.component';
2
+
3
+ export * from './flow-step-group.component';
4
+ export default ZnFlowStepGroup;
5
+
6
+ ZnFlowStepGroup.define('zn-flow-step-group');
7
+
8
+ declare global {
9
+ interface HTMLElementTagNameMap {
10
+ 'zn-flow-step-group': ZnFlowStepGroup;
11
+ }
12
+ }
@@ -0,0 +1,88 @@
1
+ import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
2
+ import {property, state} from 'lit/decorators.js';
3
+ import ZincElement from '../../../../internal/zinc-element';
4
+ import ZnInput from '../../../input';
5
+
6
+ import styles from './flow-steps.scss';
7
+
8
+ /**
9
+ * @summary A search + scroll wrapper for building a standalone steps panel. Place a standard
10
+ * `<zn-tabs>` (with a `<zn-navbar slot="top">` and panels of `<zn-flow-step-group>` /
11
+ * `<zn-flow-step>`) inside it; the search box filters the slotted items.
12
+ * @documentation https://zinc.style/components/flow-steps
13
+ * @status experimental
14
+ * @since 1.0
15
+ *
16
+ * @dependency zn-input
17
+ *
18
+ * @slot - The steps panel content, typically a `<zn-tabs>`.
19
+ *
20
+ * @csspart search - The search input.
21
+ */
22
+ export default class ZnFlowSteps extends ZincElement {
23
+ static styles: CSSResultGroup = unsafeCSS(styles);
24
+
25
+ static dependencies = {'zn-input': ZnInput};
26
+
27
+ @property({type: Boolean}) searchable = true;
28
+ @property({attribute: 'search-placeholder'}) searchPlaceholder = 'Search by step name';
29
+
30
+ @state() private _search = '';
31
+
32
+ /** Re-applies the filter when zn-tabs moves `selected` to another panel. */
33
+ private _tabObserver = new MutationObserver(() => this._applyFilter());
34
+
35
+ connectedCallback() {
36
+ super.connectedCallback();
37
+ this._tabObserver.observe(this, {subtree: true, attributeFilter: ['selected']});
38
+ }
39
+
40
+ disconnectedCallback() {
41
+ this._tabObserver.disconnect();
42
+ super.disconnectedCallback();
43
+ }
44
+
45
+ protected updated(changed: PropertyValues) {
46
+ super.updated(changed);
47
+ this._applyFilter();
48
+ }
49
+
50
+ /** True when the element isn't tabbed, or sits in the active (selected) tab panel. */
51
+ private _inActiveTab(el: HTMLElement): boolean {
52
+ return !el.closest('zn-tabs') || !!el.closest('[selected]');
53
+ }
54
+
55
+ /** Filter the slotted items (and hide emptied groups) by the search term — active tab only. */
56
+ private _applyFilter() {
57
+ const term = this._search.trim().toLowerCase();
58
+
59
+ this.querySelectorAll<HTMLElement>('zn-flow-step').forEach(it => {
60
+ it.hidden = term !== '' && this._inActiveTab(it) && !(it.textContent ?? '').toLowerCase().includes(term);
61
+ });
62
+ this.querySelectorAll<HTMLElement>('zn-flow-step-group').forEach(g => {
63
+ const items = Array.from(g.querySelectorAll<HTMLElement>('zn-flow-step'));
64
+ g.hidden = term !== '' && this._inActiveTab(g) && items.length > 0 && items.every(it => it.hidden);
65
+ });
66
+ }
67
+
68
+ render() {
69
+ return html`
70
+ ${this.searchable
71
+ ? html`
72
+ <zn-input
73
+ part="search"
74
+ class="search"
75
+ placeholder="${this.searchPlaceholder}"
76
+ clearable
77
+ .value="${this._search}"
78
+ @zn-input="${(e: Event) => (this._search = String((e.target as ZnInput).value ?? ''))}"
79
+ @input="${(e: Event) => (this._search = String((e.target as ZnInput).value ?? ''))}"
80
+ ></zn-input>`
81
+ : ''}
82
+
83
+ <div class="body">
84
+ <slot></slot>
85
+ </div>
86
+ `;
87
+ }
88
+ }
@@ -0,0 +1,24 @@
1
+ @use "../../../../wc";
2
+
3
+ :host {
4
+ display: flex;
5
+ flex-direction: column;
6
+ min-height: 0;
7
+ height: 100%;
8
+ // Make the slotted zn-navbar's active tab panel-coloured (white) to match the
9
+ // steps-panel surface, rather than the navbar's default body colour.
10
+ --zn-active-nav-background: var(--zn-panel);
11
+ }
12
+
13
+ .search {
14
+ // Inline padding matches the standard --zn-base-gap (24px) used by the panels.
15
+ padding: 12px var(--zn-base-gap) 8px;
16
+ }
17
+
18
+ // Holds the slotted <zn-tabs>, which fills the remaining height and scrolls.
19
+ .body {
20
+ display: flex;
21
+ flex-direction: column;
22
+ flex: 1;
23
+ min-height: 0;
24
+ }
@@ -0,0 +1,17 @@
1
+ import '../../../../../dist/zn.min.js';
2
+ import {expect, fixture, html} from '@open-wc/testing';
3
+ import type ZnFlowSteps from './flow-steps.component';
4
+
5
+ describe('<zn-flow-steps>', () => {
6
+ it('should render a search box and slot its content', async () => {
7
+ const el = await fixture<ZnFlowSteps>(html`
8
+ <zn-flow-steps>
9
+ <zn-flow-step type="a">Apple</zn-flow-step>
10
+ <zn-flow-step type="b">Banana</zn-flow-step>
11
+ </zn-flow-steps>`);
12
+ await el.updateComplete;
13
+
14
+ expect(el.shadowRoot?.querySelector('.search')).to.exist;
15
+ expect(el.querySelectorAll('zn-flow-step')).to.have.length(2);
16
+ });
17
+ });
@@ -0,0 +1,12 @@
1
+ import ZnFlowSteps from './flow-steps.component';
2
+
3
+ export * from './flow-steps.component';
4
+ export default ZnFlowSteps;
5
+
6
+ ZnFlowSteps.define('zn-flow-steps');
7
+
8
+ declare global {
9
+ interface HTMLElementTagNameMap {
10
+ 'zn-flow-steps': ZnFlowSteps;
11
+ }
12
+ }
@@ -12,3 +12,6 @@ export type {ZnLanguageChangeEvent} from './zn-language-change';
12
12
  export type {ZnReorderEvent} from './zn-reorder';
13
13
  export type {ZnAcceptEvent} from './zn-accept';
14
14
  export type {ZnRejectEvent} from './zn-reject';
15
+ export type {ZnFlowChangeEvent} from './zn-flow-change';
16
+ export type {ZnFlowSelectionChangeEvent} from './zn-flow-selection-change';
17
+ export type {ZnFlowConnectEvent} from './zn-flow-connect';
@@ -0,0 +1,9 @@
1
+ import type {FlowState} from '../components/flow-builder/flow.types';
2
+
3
+ export type ZnFlowChangeEvent = CustomEvent<{ state: FlowState }>;
4
+
5
+ declare global {
6
+ interface GlobalEventHandlersEventMap {
7
+ 'zn-flow-change': ZnFlowChangeEvent;
8
+ }
9
+ }