@unofficialbox/box-open-elements 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/actions/menu-item.d.ts +2 -0
- package/dist/components/actions/menu-item.js +4 -0
- package/dist/components/collections/card.js +6 -0
- package/dist/components/collections/result-blocks.js +4 -1
- package/dist/components/feedback/alert.d.ts +2 -0
- package/dist/components/feedback/alert.js +17 -13
- package/dist/components/feedback/toast.d.ts +10 -1
- package/dist/components/feedback/toast.js +69 -27
- package/dist/components/forms/number-input.d.ts +1 -0
- package/dist/components/forms/number-input.js +9 -6
- package/dist/components/forms/search-field.js +16 -0
- package/dist/components/overlays/context-menu.d.ts +2 -0
- package/dist/components/overlays/context-menu.js +7 -0
- package/dist/components/overlays/dialog.js +19 -3
- package/dist/components/overlays/popover.js +6 -1
- package/dist/element-maps.d.ts +11 -1
- package/dist/foundations/a11y/announcer.d.ts +12 -0
- package/dist/foundations/a11y/announcer.js +75 -0
- package/dist/foundations/a11y/focus.js +46 -25
- package/dist/foundations/a11y/index.d.ts +2 -0
- package/dist/foundations/a11y/index.js +1 -0
- package/dist/foundations/icons/box-iconography.d.ts +472 -472
- package/dist/foundations/icons/box-iconography.generated.d.ts +1151 -679
- package/dist/foundations/icons/box-iconography.generated.js +1006 -3588
- package/dist/foundations/icons/glyphs/index.d.ts +1 -0
- package/dist/foundations/icons/glyphs/index.js +2 -0
- package/dist/foundations/icons/glyphs/package.json +1 -0
- package/dist/foundations/overlay/positioning.js +7 -0
- package/dist/foundations/status/index.d.ts +1 -1
- package/dist/foundations/status/index.js +8 -6
- package/dist/foundations/tokens/box-dark.js +4 -1
- package/dist/foundations/tokens/box-defaults.js +3 -0
- package/dist/foundations/tokens/registry.d.ts +3 -0
- package/dist/foundations/tokens/registry.js +3 -0
- package/dist/foundations/tokens/types.d.ts +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/patterns/agent-chat/agent-chat.js +12 -4
- package/dist/patterns/call-console/call-console.d.ts +3 -0
- package/dist/patterns/call-console/call-console.js +114 -20
- package/dist/patterns/content-explorer/adapters/action-menu.js +7 -5
- package/dist/patterns/content-explorer/adapters/toolbar.js +6 -2
- package/dist/patterns/file-request/file-request-builder.js +3 -0
- package/dist/patterns/flow-builder/flow-builder.d.ts +79 -0
- package/dist/patterns/flow-builder/flow-builder.js +243 -0
- package/dist/patterns/flow-builder/index.d.ts +3 -0
- package/dist/patterns/flow-builder/index.js +3 -0
- package/dist/patterns/flow-builder/model.d.ts +81 -0
- package/dist/patterns/flow-builder/model.js +40 -0
- package/dist/patterns/flow-builder/primitives.d.ts +57 -0
- package/dist/patterns/flow-builder/primitives.js +256 -0
- package/dist/patterns/lineage/lineage-graph.js +26 -6
- package/dist/patterns/notifications/notification-inbox.js +19 -3
- package/dist/patterns/preview/annotation-inspector.js +2 -2
- package/dist/patterns/undo/index.d.ts +19 -0
- package/dist/patterns/undo/index.js +106 -0
- package/package.json +11 -2
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { type FlowKind, type FlowModel, type FlowNode, type FlowNodeBase, type NodePath } from "./model.js";
|
|
3
|
+
export type InspectorRenderer<N extends FlowNodeBase = FlowNode> = (node: N, container: HTMLElement) => void | (() => void);
|
|
4
|
+
export interface ValidationOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Show the message in the builder's own error line (default `true`). Pass
|
|
7
|
+
* `false` when the host already shows the error persistently, so the
|
|
8
|
+
* message isn't on screen twice; the step is still marked and announced.
|
|
9
|
+
*/
|
|
10
|
+
showMessage?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Host-owned document editing with accessible navigation and responsive inspection.
|
|
14
|
+
* `N` is the host's node type; it defaults to {@link FlowNode}, which the default
|
|
15
|
+
* model reads. Hosts with their own document shape set {@link model}.
|
|
16
|
+
*/
|
|
17
|
+
export declare class FlowBuilder<N extends FlowNodeBase = FlowNode> extends BaseElement {
|
|
18
|
+
static readonly tagName = "box-flow-builder";
|
|
19
|
+
private nodesValue;
|
|
20
|
+
private modelValue;
|
|
21
|
+
private catalogValue;
|
|
22
|
+
private selectedValue;
|
|
23
|
+
private invalidValue;
|
|
24
|
+
private renderer?;
|
|
25
|
+
private cleanupInspector?;
|
|
26
|
+
private inspected;
|
|
27
|
+
private headingEl?;
|
|
28
|
+
private renderedRenderer?;
|
|
29
|
+
private observer?;
|
|
30
|
+
private narrow;
|
|
31
|
+
private insertion?;
|
|
32
|
+
private invoker?;
|
|
33
|
+
private spine;
|
|
34
|
+
private palette;
|
|
35
|
+
private picker;
|
|
36
|
+
private popup;
|
|
37
|
+
private aside;
|
|
38
|
+
private inspector;
|
|
39
|
+
private drawer;
|
|
40
|
+
private error;
|
|
41
|
+
static get observedAttributes(): string[];
|
|
42
|
+
get nodes(): N[];
|
|
43
|
+
set nodes(value: N[]);
|
|
44
|
+
/** How to read the host document; defaults to `body` / `branches` ({@link FlowNode}). */
|
|
45
|
+
get model(): FlowModel<N>;
|
|
46
|
+
set model(value: FlowModel<N>);
|
|
47
|
+
/** Text above the first step (`start-label`). */
|
|
48
|
+
get startLabel(): string;
|
|
49
|
+
set startLabel(value: string);
|
|
50
|
+
/** Text below the last step (`end-label`). */
|
|
51
|
+
get endLabel(): string;
|
|
52
|
+
set endLabel(value: string);
|
|
53
|
+
get catalog(): readonly FlowKind<N>[];
|
|
54
|
+
set catalog(value: readonly FlowKind<N>[]);
|
|
55
|
+
get selected(): N | null;
|
|
56
|
+
set selected(node: N | null);
|
|
57
|
+
get renderInspector(): InspectorRenderer<N> | undefined;
|
|
58
|
+
set renderInspector(renderer: InspectorRenderer<N> | undefined);
|
|
59
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
60
|
+
connectedCallback(): void;
|
|
61
|
+
disconnectedCallback(): void;
|
|
62
|
+
private setNarrow;
|
|
63
|
+
protected renderTemplate(): void;
|
|
64
|
+
protected setupListeners(): void;
|
|
65
|
+
private closePicker;
|
|
66
|
+
private insert;
|
|
67
|
+
select(node: N | null): void;
|
|
68
|
+
/** Mark validation without opening a sheet over the error on a narrow screen. */
|
|
69
|
+
setValidation(message: string, path?: NodePath, document?: unknown, options?: ValidationOptions): void;
|
|
70
|
+
refresh(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Re-render one step's card and the inspector heading after the host edits
|
|
73
|
+
* its fields. Structural edits (adding, removing, moving steps) need
|
|
74
|
+
* {@link refresh}.
|
|
75
|
+
*/
|
|
76
|
+
refreshNode(node: N): void;
|
|
77
|
+
protected update(): void;
|
|
78
|
+
private updateInspector;
|
|
79
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { announce } from "../../foundations/a11y/index.js";
|
|
3
|
+
import { defaultFlowModel, nodeAtPath, nodeDescription, nodeTitle } from "./model.js";
|
|
4
|
+
import { FLOW_SPINE_PARTS, InsertPoint, KIND_PICKER_PARTS } from "./primitives.js";
|
|
5
|
+
/**
|
|
6
|
+
* Host-owned document editing with accessible navigation and responsive inspection.
|
|
7
|
+
* `N` is the host's node type; it defaults to {@link FlowNode}, which the default
|
|
8
|
+
* model reads. Hosts with their own document shape set {@link model}.
|
|
9
|
+
*/
|
|
10
|
+
export class FlowBuilder extends BaseElement {
|
|
11
|
+
static tagName = "box-flow-builder";
|
|
12
|
+
nodesValue = [];
|
|
13
|
+
modelValue = defaultFlowModel;
|
|
14
|
+
catalogValue = [];
|
|
15
|
+
selectedValue = null;
|
|
16
|
+
invalidValue = null;
|
|
17
|
+
renderer;
|
|
18
|
+
cleanupInspector;
|
|
19
|
+
inspected;
|
|
20
|
+
headingEl;
|
|
21
|
+
renderedRenderer;
|
|
22
|
+
observer;
|
|
23
|
+
narrow = false;
|
|
24
|
+
insertion;
|
|
25
|
+
invoker;
|
|
26
|
+
spine;
|
|
27
|
+
palette;
|
|
28
|
+
picker;
|
|
29
|
+
popup;
|
|
30
|
+
aside;
|
|
31
|
+
inspector;
|
|
32
|
+
drawer;
|
|
33
|
+
error;
|
|
34
|
+
static get observedAttributes() { return ["start-label", "end-label"]; }
|
|
35
|
+
get nodes() { return this.nodesValue; }
|
|
36
|
+
set nodes(value) { this.nodesValue = value; this.refresh(); }
|
|
37
|
+
/** How to read the host document; defaults to `body` / `branches` ({@link FlowNode}). */
|
|
38
|
+
get model() { return this.modelValue; }
|
|
39
|
+
set model(value) { this.modelValue = value; this.refresh(); }
|
|
40
|
+
/** Text above the first step (`start-label`). */
|
|
41
|
+
get startLabel() { return this.getAttribute("start-label") ?? "Flow starts"; }
|
|
42
|
+
set startLabel(value) { this.setAttribute("start-label", value); }
|
|
43
|
+
/** Text below the last step (`end-label`). */
|
|
44
|
+
get endLabel() { return this.getAttribute("end-label") ?? "Flow ends"; }
|
|
45
|
+
set endLabel(value) { this.setAttribute("end-label", value); }
|
|
46
|
+
get catalog() { return this.catalogValue; }
|
|
47
|
+
set catalog(value) { this.catalogValue = value; this.refresh(); }
|
|
48
|
+
get selected() { return this.selectedValue; }
|
|
49
|
+
set selected(node) { this.select(node); }
|
|
50
|
+
get renderInspector() { return this.renderer; }
|
|
51
|
+
set renderInspector(renderer) { this.renderer = renderer; if (this.isRendered)
|
|
52
|
+
this.updateInspector(); }
|
|
53
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
54
|
+
super.attributeChangedCallback(name, oldValue, newValue);
|
|
55
|
+
if (oldValue !== newValue)
|
|
56
|
+
this.refresh();
|
|
57
|
+
}
|
|
58
|
+
connectedCallback() {
|
|
59
|
+
super.connectedCallback();
|
|
60
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
61
|
+
this.observer = new ResizeObserver(entries => this.setNarrow(entries[0].contentRect.width < 800));
|
|
62
|
+
this.observer.observe(this);
|
|
63
|
+
}
|
|
64
|
+
this.setNarrow((this.getBoundingClientRect().width || this.ownerDocument.defaultView?.innerWidth || 1024) < 800);
|
|
65
|
+
}
|
|
66
|
+
disconnectedCallback() {
|
|
67
|
+
this.observer?.disconnect();
|
|
68
|
+
this.cleanupInspector?.();
|
|
69
|
+
this.cleanupInspector = undefined;
|
|
70
|
+
this.inspected = undefined;
|
|
71
|
+
this.closePicker(false);
|
|
72
|
+
this.selectedValue = null;
|
|
73
|
+
this.drawer.open = false;
|
|
74
|
+
}
|
|
75
|
+
setNarrow(narrow) {
|
|
76
|
+
if (this.narrow === narrow)
|
|
77
|
+
return;
|
|
78
|
+
this.narrow = narrow;
|
|
79
|
+
this.shadowRoot.querySelector("[part=layout]").dataset.narrow = String(narrow);
|
|
80
|
+
this.updateInspector();
|
|
81
|
+
}
|
|
82
|
+
renderTemplate() {
|
|
83
|
+
this.shadowRoot.innerHTML = `<style>
|
|
84
|
+
:host{display:block;min-width:0;font:inherit;color:var(--boe-token-text-text,#222)}
|
|
85
|
+
:host([hidden]){display:none!important}
|
|
86
|
+
[part=layout]{display:grid;grid-template-columns:minmax(0,1fr) minmax(240px,320px);gap:24px;align-items:start}
|
|
87
|
+
[part=layout][data-narrow=true]{grid-template-columns:minmax(0,1fr)}
|
|
88
|
+
aside{min-width:0;padding:16px;border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:12px;background:var(--boe-token-surface-surface,#fff)}
|
|
89
|
+
[part=picker-popup]{position:fixed;z-index:1000;max-width:min(360px,calc(100vw - 24px));max-height:65vh;overflow:auto;padding:16px;background:var(--boe-token-surface-surface,#fff);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:12px;box-shadow:0 8px 32px #0003}
|
|
90
|
+
[part=error]{color:var(--boe-token-text-status-text-error,#b92340);margin-bottom:16px;overflow-wrap:anywhere}
|
|
91
|
+
[hidden]{display:none!important} h2{font:inherit;font-weight:650;margin:0 0 12px}
|
|
92
|
+
</style><div part="error" hidden></div><div part="layout"><box-flow-spine exportparts="${FLOW_SPINE_PARTS.join(", ")}"></box-flow-spine><aside part="inspector"><box-kind-picker exportparts="${KIND_PICKER_PARTS.join(", ")}"></box-kind-picker><div part="editor"></div></aside></div><div part="picker-popup" hidden><box-kind-picker exportparts="${KIND_PICKER_PARTS.join(", ")}"></box-kind-picker></div><box-drawer position="bottom" size="large"></box-drawer>`;
|
|
93
|
+
this.spine = this.shadowRoot.querySelector("box-flow-spine");
|
|
94
|
+
this.aside = this.shadowRoot.querySelector("aside");
|
|
95
|
+
this.palette = this.aside.querySelector("box-kind-picker");
|
|
96
|
+
this.inspector = this.aside.querySelector("[part=editor]");
|
|
97
|
+
this.popup = this.shadowRoot.querySelector("[part=picker-popup]");
|
|
98
|
+
this.picker = this.popup.querySelector("box-kind-picker");
|
|
99
|
+
this.picker.variant = "menu";
|
|
100
|
+
this.drawer = this.shadowRoot.querySelector("box-drawer");
|
|
101
|
+
this.error = this.shadowRoot.querySelector("[part=error]");
|
|
102
|
+
}
|
|
103
|
+
setupListeners() {
|
|
104
|
+
this.spine.addEventListener("node-select", event => this.select(event.detail.node));
|
|
105
|
+
this.spine.addEventListener("insert-request", event => {
|
|
106
|
+
this.insertion = event.detail;
|
|
107
|
+
this.invoker = event.composedPath().find(item => item instanceof InsertPoint);
|
|
108
|
+
const rect = this.invoker.getBoundingClientRect();
|
|
109
|
+
this.popup.hidden = false;
|
|
110
|
+
this.picker.catalog = this.catalog;
|
|
111
|
+
this.picker.refresh();
|
|
112
|
+
const viewport = this.ownerDocument.defaultView;
|
|
113
|
+
const popupRect = this.popup.getBoundingClientRect();
|
|
114
|
+
this.popup.style.left = `${Math.max(12, Math.min(rect.left, viewport.innerWidth - popupRect.width - 12))}px`;
|
|
115
|
+
this.popup.style.top = `${Math.max(12, Math.min(rect.bottom + 8, viewport.innerHeight - popupRect.height - 12))}px`;
|
|
116
|
+
this.picker.focus();
|
|
117
|
+
});
|
|
118
|
+
this.picker.addEventListener("picker-cancel", () => this.closePicker(true));
|
|
119
|
+
this.picker.addEventListener("focusout", event => {
|
|
120
|
+
const next = event.relatedTarget;
|
|
121
|
+
if (next instanceof Node && (this.picker.contains(next) || this.picker.shadowRoot?.contains(next)))
|
|
122
|
+
return;
|
|
123
|
+
this.closePicker(false);
|
|
124
|
+
});
|
|
125
|
+
this.picker.addEventListener("kind-pick", event => {
|
|
126
|
+
const insertion = this.insertion;
|
|
127
|
+
if (!insertion)
|
|
128
|
+
return;
|
|
129
|
+
this.insert(event.detail.kind, insertion);
|
|
130
|
+
});
|
|
131
|
+
this.palette.addEventListener("kind-pick", event => this.insert(event.detail.kind, { list: this.nodes, index: this.nodes.length, label: "Append to flow" }));
|
|
132
|
+
this.drawer.addEventListener("open-changed", event => {
|
|
133
|
+
if (!event.detail.open && this.narrow && this.selectedValue)
|
|
134
|
+
this.select(null);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
closePicker(restoreFocus) {
|
|
138
|
+
if (!this.popup)
|
|
139
|
+
return;
|
|
140
|
+
this.popup.hidden = true;
|
|
141
|
+
this.insertion = undefined;
|
|
142
|
+
if (restoreFocus)
|
|
143
|
+
this.invoker?.focus();
|
|
144
|
+
}
|
|
145
|
+
insert(kind, insertion) {
|
|
146
|
+
const node = kind.create();
|
|
147
|
+
insertion.list.splice(insertion.index, 0, node);
|
|
148
|
+
this.closePicker(false);
|
|
149
|
+
this.selectedValue = this.narrow ? null : node;
|
|
150
|
+
this.refresh();
|
|
151
|
+
// Keep the newly inserted card visible on small screens; explicit selection opens editing.
|
|
152
|
+
this.spine.focusNode(node);
|
|
153
|
+
announce(`Added ${nodeTitle(node, this.catalog, this.model)}`, "polite", this.ownerDocument);
|
|
154
|
+
this.dispatchEvent(new CustomEvent("flow-changed", { detail: { nodes: this.nodes, node, reason: "insert" }, bubbles: true, composed: true }));
|
|
155
|
+
}
|
|
156
|
+
select(node) {
|
|
157
|
+
this.selectedValue = node;
|
|
158
|
+
if (this.isRendered) {
|
|
159
|
+
this.spine.select(node, this.invalidValue);
|
|
160
|
+
this.updateInspector();
|
|
161
|
+
}
|
|
162
|
+
this.dispatchEvent(new CustomEvent("selection-changed", { detail: { node }, bubbles: true, composed: true }));
|
|
163
|
+
}
|
|
164
|
+
/** Mark validation without opening a sheet over the error on a narrow screen. */
|
|
165
|
+
setValidation(message, path = [], document = { body: this.nodes }, options = {}) {
|
|
166
|
+
this.invalidValue = message ? nodeAtPath(document, path) : null;
|
|
167
|
+
const show = options.showMessage ?? true;
|
|
168
|
+
this.error.textContent = show ? message : "";
|
|
169
|
+
this.error.hidden = !message || !show;
|
|
170
|
+
this.spine.select(this.selected, this.invalidValue);
|
|
171
|
+
if (message) {
|
|
172
|
+
if (!this.narrow && this.invalidValue)
|
|
173
|
+
this.select(this.invalidValue);
|
|
174
|
+
else if (this.narrow)
|
|
175
|
+
this.select(null);
|
|
176
|
+
announce(message, "assertive", this.ownerDocument);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
refresh() { if (this.isRendered)
|
|
180
|
+
this.update(); }
|
|
181
|
+
/**
|
|
182
|
+
* Re-render one step's card and the inspector heading after the host edits
|
|
183
|
+
* its fields. Structural edits (adding, removing, moving steps) need
|
|
184
|
+
* {@link refresh}.
|
|
185
|
+
*/
|
|
186
|
+
refreshNode(node) {
|
|
187
|
+
if (!this.isRendered)
|
|
188
|
+
return;
|
|
189
|
+
this.spine.refreshNode(node);
|
|
190
|
+
if (node === this.selected) {
|
|
191
|
+
const title = nodeTitle(node, this.catalog, this.model);
|
|
192
|
+
if (this.headingEl)
|
|
193
|
+
this.headingEl.textContent = title;
|
|
194
|
+
this.drawer.heading = title;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
update() {
|
|
198
|
+
this.spine.nodes = this.nodes;
|
|
199
|
+
this.spine.catalog = this.catalog;
|
|
200
|
+
this.spine.model = this.model;
|
|
201
|
+
this.spine.startLabel = this.startLabel;
|
|
202
|
+
this.spine.endLabel = this.endLabel;
|
|
203
|
+
this.spine.selected = this.selected;
|
|
204
|
+
this.spine.invalid = this.invalidValue;
|
|
205
|
+
this.spine.refresh();
|
|
206
|
+
this.palette.catalog = this.catalog;
|
|
207
|
+
this.palette.refresh();
|
|
208
|
+
this.updateInspector();
|
|
209
|
+
}
|
|
210
|
+
updateInspector() {
|
|
211
|
+
this.aside.hidden = this.narrow;
|
|
212
|
+
this.palette.hidden = Boolean(this.selected);
|
|
213
|
+
const target = this.narrow ? this.drawer : this.aside;
|
|
214
|
+
if (this.inspector.parentElement !== target)
|
|
215
|
+
target.append(this.inspector);
|
|
216
|
+
if (this.inspected === this.selected && this.renderedRenderer === this.renderer) {
|
|
217
|
+
this.drawer.open = this.narrow && Boolean(this.selected);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.cleanupInspector?.();
|
|
221
|
+
this.cleanupInspector = undefined;
|
|
222
|
+
this.inspector.replaceChildren();
|
|
223
|
+
this.inspected = this.selected;
|
|
224
|
+
this.renderedRenderer = this.renderer;
|
|
225
|
+
if (this.selected) {
|
|
226
|
+
const heading = document.createElement("h2");
|
|
227
|
+
heading.setAttribute("part", "inspector-heading");
|
|
228
|
+
heading.textContent = nodeTitle(this.selected, this.catalog, this.model);
|
|
229
|
+
this.inspector.append(heading);
|
|
230
|
+
this.headingEl = heading;
|
|
231
|
+
if (this.renderer)
|
|
232
|
+
this.cleanupInspector = this.renderer(this.selected, this.inspector) || undefined;
|
|
233
|
+
else {
|
|
234
|
+
const description = document.createElement("p");
|
|
235
|
+
description.textContent = nodeDescription(this.selected, this.catalog, this.model) || "Select a step to inspect its configuration.";
|
|
236
|
+
this.inspector.append(description);
|
|
237
|
+
}
|
|
238
|
+
this.drawer.heading = nodeTitle(this.selected, this.catalog, this.model);
|
|
239
|
+
}
|
|
240
|
+
this.drawer.open = this.narrow && Boolean(this.selected);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
FlowBuilder.register();
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The least the flow builder needs from a node. Hosts keep their own
|
|
3
|
+
* document shape; a {@link FlowModel} tells the builder how to read it.
|
|
4
|
+
*/
|
|
5
|
+
export interface FlowNodeBase {
|
|
6
|
+
kind: string;
|
|
7
|
+
}
|
|
8
|
+
/** The document shape the default model reads. */
|
|
9
|
+
export interface FlowNode extends FlowNodeBase {
|
|
10
|
+
id?: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
status?: string;
|
|
14
|
+
body?: FlowNode[];
|
|
15
|
+
branches?: FlowBranch[];
|
|
16
|
+
}
|
|
17
|
+
export interface FlowBranch {
|
|
18
|
+
label: string;
|
|
19
|
+
body: FlowNode[];
|
|
20
|
+
}
|
|
21
|
+
/** One list of steps nested under a node. */
|
|
22
|
+
export interface FlowChildList {
|
|
23
|
+
/** A branch label. Unlabeled lists render as the node's inline body; labeled lists render as branch columns. */
|
|
24
|
+
label?: string;
|
|
25
|
+
/** The host's own array. The builder splices new steps into it. */
|
|
26
|
+
list: FlowNodeBase[];
|
|
27
|
+
/** Show a remove control on this branch; the builder emits `branch-remove-request`. */
|
|
28
|
+
removable?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* How the builder reads a host document. Every traversal, title and insert
|
|
32
|
+
* label goes through the model, so hosts pass their own nodes — identity
|
|
33
|
+
* (selection, undo, validation paths) is preserved.
|
|
34
|
+
*/
|
|
35
|
+
export interface FlowModel<N extends FlowNodeBase = FlowNodeBase> {
|
|
36
|
+
/** Nested lists under `node`, in display order. */
|
|
37
|
+
children(node: N): FlowChildList[];
|
|
38
|
+
/** A distinct name for the node; falls back to its kind's label. */
|
|
39
|
+
title?(node: N): string | undefined;
|
|
40
|
+
/** One sentence about what the node does at run time; falls back to its kind's description. */
|
|
41
|
+
description?(node: N): string | undefined;
|
|
42
|
+
/** A short status line shown on the card. */
|
|
43
|
+
status?(node: N): string | undefined;
|
|
44
|
+
/** When it returns a label, the node's branches get an add control; the builder emits `branch-add-request`. */
|
|
45
|
+
addBranchLabel?(node: N): string | undefined;
|
|
46
|
+
}
|
|
47
|
+
/** Reads {@link FlowNode}: `body` renders inline and `branches` render as labeled columns. */
|
|
48
|
+
export declare const defaultFlowModel: FlowModel<FlowNode>;
|
|
49
|
+
export interface FlowKind<N extends FlowNodeBase = FlowNodeBase> {
|
|
50
|
+
kind: string;
|
|
51
|
+
label: string;
|
|
52
|
+
description: string;
|
|
53
|
+
group?: string;
|
|
54
|
+
/** Supply an authored SVG node (for example from a named glyph import). */
|
|
55
|
+
icon?: () => Node;
|
|
56
|
+
/**
|
|
57
|
+
* Icon color. `accent` (the default) uses the brand color; `neutral` uses
|
|
58
|
+
* secondary text, so a host can reserve the accent for its primary object.
|
|
59
|
+
*/
|
|
60
|
+
tone?: "accent" | "neutral";
|
|
61
|
+
create(): N;
|
|
62
|
+
}
|
|
63
|
+
export interface FlowInsertDetail {
|
|
64
|
+
list: FlowNodeBase[];
|
|
65
|
+
index: number;
|
|
66
|
+
label: string;
|
|
67
|
+
}
|
|
68
|
+
export interface FlowBranchRequestDetail {
|
|
69
|
+
node: FlowNodeBase;
|
|
70
|
+
/** The branch's position among the node's labeled lists (remove requests only). */
|
|
71
|
+
index?: number;
|
|
72
|
+
/** The branch's own list (remove requests only). */
|
|
73
|
+
list?: FlowNodeBase[];
|
|
74
|
+
}
|
|
75
|
+
export type NodePath = readonly (string | number)[];
|
|
76
|
+
export declare const nodeTitle: (node: FlowNodeBase, catalog?: readonly FlowKind[], model?: FlowModel<FlowNodeBase>) => string;
|
|
77
|
+
export declare const nodeDescription: (node: FlowNodeBase, catalog?: readonly FlowKind[], model?: FlowModel<FlowNodeBase>) => string;
|
|
78
|
+
export declare function cardLabel(node: FlowNodeBase, catalog?: readonly FlowKind[], invalid?: boolean, model?: FlowModel<FlowNodeBase>): string;
|
|
79
|
+
export declare function insertLabel(list: readonly FlowNodeBase[], index: number, where?: string, catalog?: readonly FlowKind[], model?: FlowModel<FlowNodeBase>): string;
|
|
80
|
+
/** Return the deepest node on a validation path, including paths ending in a field. */
|
|
81
|
+
export declare function nodeAtPath(document: unknown, path: NodePath): FlowNodeBase | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Reads {@link FlowNode}: `body` renders inline and `branches` render as labeled columns. */
|
|
2
|
+
export const defaultFlowModel = {
|
|
3
|
+
children: node => [
|
|
4
|
+
...(node.body ? [{ list: node.body }] : []),
|
|
5
|
+
...(node.branches ?? []).map(branch => ({ label: branch.label, list: branch.body })),
|
|
6
|
+
],
|
|
7
|
+
title: node => node.title,
|
|
8
|
+
description: node => node.description,
|
|
9
|
+
status: node => node.status,
|
|
10
|
+
};
|
|
11
|
+
export const nodeTitle = (node, catalog = [], model = defaultFlowModel) => model.title?.(node) || catalog.find(kind => kind.kind === node.kind)?.label || "Step";
|
|
12
|
+
export const nodeDescription = (node, catalog = [], model = defaultFlowModel) => model.description?.(node) || catalog.find(kind => kind.kind === node.kind)?.description || "";
|
|
13
|
+
export function cardLabel(node, catalog = [], invalid = false, model = defaultFlowModel) {
|
|
14
|
+
const kind = catalog.find(entry => entry.kind === node.kind);
|
|
15
|
+
const title = nodeTitle(node, catalog, model);
|
|
16
|
+
return [title, kind?.label !== title ? kind?.label : "", nodeDescription(node, catalog, model), model.status?.(node), invalid ? "Needs attention" : ""].filter(Boolean).join(". ");
|
|
17
|
+
}
|
|
18
|
+
export function insertLabel(list, index, where = "in the flow", catalog = [], model = defaultFlowModel) {
|
|
19
|
+
const title = (node) => nodeTitle(node, catalog, model);
|
|
20
|
+
if (!list.length)
|
|
21
|
+
return `Add the first step ${where}`;
|
|
22
|
+
if (index === 0)
|
|
23
|
+
return `Insert before ${title(list[0])} ${where}`;
|
|
24
|
+
if (index >= list.length)
|
|
25
|
+
return `Insert after ${title(list[list.length - 1])} ${where}`;
|
|
26
|
+
return `Insert between ${title(list[index - 1])} and ${title(list[index])} ${where}`;
|
|
27
|
+
}
|
|
28
|
+
/** Return the deepest node on a validation path, including paths ending in a field. */
|
|
29
|
+
export function nodeAtPath(document, path) {
|
|
30
|
+
let current = document;
|
|
31
|
+
let result = null;
|
|
32
|
+
for (const segment of path) {
|
|
33
|
+
if (!current || typeof current !== "object" || !Object.hasOwn(current, segment))
|
|
34
|
+
break;
|
|
35
|
+
current = current[segment];
|
|
36
|
+
if (current && typeof current === "object" && "kind" in current && typeof current.kind === "string")
|
|
37
|
+
result = current;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { type FlowInsertDetail, type FlowKind, type FlowModel, type FlowNodeBase } from "./model.js";
|
|
3
|
+
/** Parts a card exposes; the spine and builder forward them unchanged. */
|
|
4
|
+
export declare const FLOW_CARD_PARTS: readonly ["card", "icon", "title", "kind", "description", "status", "error"];
|
|
5
|
+
/** Parts the spine exposes (its own plus forwarded card and insert-point parts). */
|
|
6
|
+
export declare const FLOW_SPINE_PARTS: readonly ["spine", "endpoint", "body", "branches", "branch", "branch-header", "branch-label", "branch-remove", "branch-add", "insert", "card", "icon", "title", "kind", "description", "status", "error"];
|
|
7
|
+
/** Parts a kind picker exposes. */
|
|
8
|
+
export declare const KIND_PICKER_PARTS: readonly ["choices", "group-heading", "choice", "choice-icon", "choice-label", "choice-description"];
|
|
9
|
+
export declare class FlowCard extends BaseElement {
|
|
10
|
+
static readonly tagName = "box-flow-card";
|
|
11
|
+
node: FlowNodeBase;
|
|
12
|
+
catalog: readonly FlowKind[];
|
|
13
|
+
model: FlowModel<FlowNodeBase>;
|
|
14
|
+
selected: boolean;
|
|
15
|
+
invalid: boolean;
|
|
16
|
+
protected renderTemplate(): void;
|
|
17
|
+
protected setupListeners(): void;
|
|
18
|
+
refresh(): void;
|
|
19
|
+
focus(options?: FocusOptions): void;
|
|
20
|
+
protected update(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare class InsertPoint extends BaseElement {
|
|
23
|
+
static readonly tagName = "box-insert-point";
|
|
24
|
+
detail: FlowInsertDetail;
|
|
25
|
+
protected renderTemplate(): void;
|
|
26
|
+
protected setupListeners(): void;
|
|
27
|
+
focus(options?: FocusOptions): void;
|
|
28
|
+
protected update(): void;
|
|
29
|
+
}
|
|
30
|
+
export declare class KindPicker extends BaseElement {
|
|
31
|
+
static readonly tagName = "box-kind-picker";
|
|
32
|
+
catalog: readonly FlowKind[];
|
|
33
|
+
variant: "inline" | "menu";
|
|
34
|
+
protected renderTemplate(): void;
|
|
35
|
+
protected setupListeners(): void;
|
|
36
|
+
refresh(): void;
|
|
37
|
+
focus(options?: FocusOptions): void;
|
|
38
|
+
protected update(): void;
|
|
39
|
+
}
|
|
40
|
+
export declare class FlowSpine extends BaseElement {
|
|
41
|
+
static readonly tagName = "box-flow-spine";
|
|
42
|
+
nodes: FlowNodeBase[];
|
|
43
|
+
catalog: readonly FlowKind[];
|
|
44
|
+
model: FlowModel<FlowNodeBase>;
|
|
45
|
+
selected: FlowNodeBase | null;
|
|
46
|
+
invalid: FlowNodeBase | null;
|
|
47
|
+
startLabel: string;
|
|
48
|
+
endLabel: string;
|
|
49
|
+
private cards;
|
|
50
|
+
protected renderTemplate(): void;
|
|
51
|
+
refresh(): void;
|
|
52
|
+
/** Re-render one card after its fields change, without rebuilding the spine. */
|
|
53
|
+
refreshNode(node: FlowNodeBase): void;
|
|
54
|
+
focusNode(node: FlowNodeBase): void;
|
|
55
|
+
select(node: FlowNodeBase | null, invalid?: FlowNodeBase | null): void;
|
|
56
|
+
protected update(): void;
|
|
57
|
+
}
|