@unofficialbox/box-open-elements 0.20.0 → 0.21.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 +48 -0
- package/dist/patterns/flow-builder/flow-builder.js +200 -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 +33 -0
- package/dist/patterns/flow-builder/model.js +28 -0
- package/dist/patterns/flow-builder/primitives.d.ts +47 -0
- package/dist/patterns/flow-builder/primitives.js +192 -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,200 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { announce } from "../../foundations/a11y/index.js";
|
|
3
|
+
import { nodeAtPath, nodeTitle } from "./model.js";
|
|
4
|
+
import { InsertPoint } from "./primitives.js";
|
|
5
|
+
/** Host-owned document editing with accessible navigation and responsive inspection. */
|
|
6
|
+
export class FlowBuilder extends BaseElement {
|
|
7
|
+
static tagName = "box-flow-builder";
|
|
8
|
+
nodesValue = [];
|
|
9
|
+
catalogValue = [];
|
|
10
|
+
selectedValue = null;
|
|
11
|
+
invalidValue = null;
|
|
12
|
+
renderer;
|
|
13
|
+
cleanupInspector;
|
|
14
|
+
inspected;
|
|
15
|
+
renderedRenderer;
|
|
16
|
+
observer;
|
|
17
|
+
narrow = false;
|
|
18
|
+
insertion;
|
|
19
|
+
invoker;
|
|
20
|
+
spine;
|
|
21
|
+
palette;
|
|
22
|
+
picker;
|
|
23
|
+
popup;
|
|
24
|
+
aside;
|
|
25
|
+
inspector;
|
|
26
|
+
drawer;
|
|
27
|
+
error;
|
|
28
|
+
get nodes() { return this.nodesValue; }
|
|
29
|
+
set nodes(value) { this.nodesValue = value; this.refresh(); }
|
|
30
|
+
get catalog() { return this.catalogValue; }
|
|
31
|
+
set catalog(value) { this.catalogValue = value; this.refresh(); }
|
|
32
|
+
get selected() { return this.selectedValue; }
|
|
33
|
+
set selected(node) { this.select(node); }
|
|
34
|
+
get renderInspector() { return this.renderer; }
|
|
35
|
+
set renderInspector(renderer) { this.renderer = renderer; if (this.isRendered)
|
|
36
|
+
this.updateInspector(); }
|
|
37
|
+
connectedCallback() {
|
|
38
|
+
super.connectedCallback();
|
|
39
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
40
|
+
this.observer = new ResizeObserver(entries => this.setNarrow(entries[0].contentRect.width < 800));
|
|
41
|
+
this.observer.observe(this);
|
|
42
|
+
}
|
|
43
|
+
this.setNarrow((this.getBoundingClientRect().width || this.ownerDocument.defaultView?.innerWidth || 1024) < 800);
|
|
44
|
+
}
|
|
45
|
+
disconnectedCallback() {
|
|
46
|
+
this.observer?.disconnect();
|
|
47
|
+
this.cleanupInspector?.();
|
|
48
|
+
this.cleanupInspector = undefined;
|
|
49
|
+
this.inspected = undefined;
|
|
50
|
+
this.closePicker(false);
|
|
51
|
+
this.selectedValue = null;
|
|
52
|
+
this.drawer.open = false;
|
|
53
|
+
}
|
|
54
|
+
setNarrow(narrow) {
|
|
55
|
+
if (this.narrow === narrow)
|
|
56
|
+
return;
|
|
57
|
+
this.narrow = narrow;
|
|
58
|
+
this.shadowRoot.querySelector("[part=layout]").dataset.narrow = String(narrow);
|
|
59
|
+
this.updateInspector();
|
|
60
|
+
}
|
|
61
|
+
renderTemplate() {
|
|
62
|
+
this.shadowRoot.innerHTML = `<style>
|
|
63
|
+
:host{display:block;min-width:0;font:inherit;color:var(--boe-token-text-text,#222)}
|
|
64
|
+
:host([hidden]){display:none!important}
|
|
65
|
+
[part=layout]{display:grid;grid-template-columns:minmax(0,1fr) minmax(240px,320px);gap:24px;align-items:start}
|
|
66
|
+
[part=layout][data-narrow=true]{grid-template-columns:minmax(0,1fr)}
|
|
67
|
+
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)}
|
|
68
|
+
[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}
|
|
69
|
+
[part=error]{color:var(--boe-token-text-status-text-error,#b92340);margin-bottom:16px;overflow-wrap:anywhere}
|
|
70
|
+
[hidden]{display:none!important} h2{font:inherit;font-weight:650;margin:0 0 12px}
|
|
71
|
+
</style><div part="error" hidden></div><div part="layout"><box-flow-spine></box-flow-spine><aside part="inspector"><box-kind-picker></box-kind-picker><div part="editor"></div></aside></div><div part="picker-popup" hidden><box-kind-picker></box-kind-picker></div><box-drawer position="bottom" size="large"></box-drawer>`;
|
|
72
|
+
this.spine = this.shadowRoot.querySelector("box-flow-spine");
|
|
73
|
+
this.aside = this.shadowRoot.querySelector("aside");
|
|
74
|
+
this.palette = this.aside.querySelector("box-kind-picker");
|
|
75
|
+
this.inspector = this.aside.querySelector("[part=editor]");
|
|
76
|
+
this.popup = this.shadowRoot.querySelector("[part=picker-popup]");
|
|
77
|
+
this.picker = this.popup.querySelector("box-kind-picker");
|
|
78
|
+
this.picker.variant = "menu";
|
|
79
|
+
this.drawer = this.shadowRoot.querySelector("box-drawer");
|
|
80
|
+
this.error = this.shadowRoot.querySelector("[part=error]");
|
|
81
|
+
}
|
|
82
|
+
setupListeners() {
|
|
83
|
+
this.spine.addEventListener("node-select", event => this.select(event.detail.node));
|
|
84
|
+
this.spine.addEventListener("insert-request", event => {
|
|
85
|
+
this.insertion = event.detail;
|
|
86
|
+
this.invoker = event.composedPath().find(item => item instanceof InsertPoint);
|
|
87
|
+
const rect = this.invoker.getBoundingClientRect();
|
|
88
|
+
this.popup.hidden = false;
|
|
89
|
+
this.picker.catalog = this.catalog;
|
|
90
|
+
this.picker.refresh();
|
|
91
|
+
const viewport = this.ownerDocument.defaultView;
|
|
92
|
+
const popupRect = this.popup.getBoundingClientRect();
|
|
93
|
+
this.popup.style.left = `${Math.max(12, Math.min(rect.left, viewport.innerWidth - popupRect.width - 12))}px`;
|
|
94
|
+
this.popup.style.top = `${Math.max(12, Math.min(rect.bottom + 8, viewport.innerHeight - popupRect.height - 12))}px`;
|
|
95
|
+
this.picker.focus();
|
|
96
|
+
});
|
|
97
|
+
this.picker.addEventListener("picker-cancel", () => this.closePicker(true));
|
|
98
|
+
this.picker.addEventListener("focusout", event => {
|
|
99
|
+
const next = event.relatedTarget;
|
|
100
|
+
if (next instanceof Node && (this.picker.contains(next) || this.picker.shadowRoot?.contains(next)))
|
|
101
|
+
return;
|
|
102
|
+
this.closePicker(false);
|
|
103
|
+
});
|
|
104
|
+
this.picker.addEventListener("kind-pick", event => {
|
|
105
|
+
const insertion = this.insertion;
|
|
106
|
+
if (!insertion)
|
|
107
|
+
return;
|
|
108
|
+
this.insert(event.detail.kind, insertion);
|
|
109
|
+
});
|
|
110
|
+
this.palette.addEventListener("kind-pick", event => this.insert(event.detail.kind, { list: this.nodes, index: this.nodes.length, label: "Append to flow" }));
|
|
111
|
+
this.drawer.addEventListener("open-changed", event => {
|
|
112
|
+
if (!event.detail.open && this.narrow && this.selectedValue)
|
|
113
|
+
this.select(null);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
closePicker(restoreFocus) {
|
|
117
|
+
if (!this.popup)
|
|
118
|
+
return;
|
|
119
|
+
this.popup.hidden = true;
|
|
120
|
+
this.insertion = undefined;
|
|
121
|
+
if (restoreFocus)
|
|
122
|
+
this.invoker?.focus();
|
|
123
|
+
}
|
|
124
|
+
insert(kind, insertion) {
|
|
125
|
+
const node = kind.create();
|
|
126
|
+
insertion.list.splice(insertion.index, 0, node);
|
|
127
|
+
this.closePicker(false);
|
|
128
|
+
this.selectedValue = this.narrow ? null : node;
|
|
129
|
+
this.refresh();
|
|
130
|
+
// Keep the newly inserted card visible on small screens; explicit selection opens editing.
|
|
131
|
+
this.spine.focusNode(node);
|
|
132
|
+
announce(`Added ${nodeTitle(node, this.catalog)}`, "polite", this.ownerDocument);
|
|
133
|
+
this.dispatchEvent(new CustomEvent("flow-changed", { detail: { nodes: this.nodes, node, reason: "insert" }, bubbles: true, composed: true }));
|
|
134
|
+
}
|
|
135
|
+
select(node) {
|
|
136
|
+
this.selectedValue = node;
|
|
137
|
+
if (this.isRendered) {
|
|
138
|
+
this.spine.select(node, this.invalidValue);
|
|
139
|
+
this.updateInspector();
|
|
140
|
+
}
|
|
141
|
+
this.dispatchEvent(new CustomEvent("selection-changed", { detail: { node }, bubbles: true, composed: true }));
|
|
142
|
+
}
|
|
143
|
+
/** Mark validation without opening a sheet over the error on a narrow screen. */
|
|
144
|
+
setValidation(message, path = [], document = { body: this.nodes }) {
|
|
145
|
+
this.invalidValue = message ? nodeAtPath(document, path) : null;
|
|
146
|
+
this.error.textContent = message;
|
|
147
|
+
this.error.hidden = !message;
|
|
148
|
+
this.spine.select(this.selected, this.invalidValue);
|
|
149
|
+
if (message) {
|
|
150
|
+
if (!this.narrow && this.invalidValue)
|
|
151
|
+
this.select(this.invalidValue);
|
|
152
|
+
else if (this.narrow)
|
|
153
|
+
this.select(null);
|
|
154
|
+
announce(message, "assertive", this.ownerDocument);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
refresh() { if (this.isRendered)
|
|
158
|
+
this.update(); }
|
|
159
|
+
update() {
|
|
160
|
+
this.spine.nodes = this.nodes;
|
|
161
|
+
this.spine.catalog = this.catalog;
|
|
162
|
+
this.spine.selected = this.selected;
|
|
163
|
+
this.spine.invalid = this.invalidValue;
|
|
164
|
+
this.spine.refresh();
|
|
165
|
+
this.palette.catalog = this.catalog;
|
|
166
|
+
this.palette.refresh();
|
|
167
|
+
this.updateInspector();
|
|
168
|
+
}
|
|
169
|
+
updateInspector() {
|
|
170
|
+
this.aside.hidden = this.narrow;
|
|
171
|
+
this.palette.hidden = Boolean(this.selected);
|
|
172
|
+
const target = this.narrow ? this.drawer : this.aside;
|
|
173
|
+
if (this.inspector.parentElement !== target)
|
|
174
|
+
target.append(this.inspector);
|
|
175
|
+
if (this.inspected === this.selected && this.renderedRenderer === this.renderer) {
|
|
176
|
+
this.drawer.open = this.narrow && Boolean(this.selected);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
this.cleanupInspector?.();
|
|
180
|
+
this.cleanupInspector = undefined;
|
|
181
|
+
this.inspector.replaceChildren();
|
|
182
|
+
this.inspected = this.selected;
|
|
183
|
+
this.renderedRenderer = this.renderer;
|
|
184
|
+
if (this.selected) {
|
|
185
|
+
const heading = document.createElement("h2");
|
|
186
|
+
heading.textContent = nodeTitle(this.selected, this.catalog);
|
|
187
|
+
this.inspector.append(heading);
|
|
188
|
+
if (this.renderer)
|
|
189
|
+
this.cleanupInspector = this.renderer(this.selected, this.inspector) || undefined;
|
|
190
|
+
else {
|
|
191
|
+
const description = document.createElement("p");
|
|
192
|
+
description.textContent = this.selected.description || this.catalog.find(kind => kind.kind === this.selected.kind)?.description || "Select a step to inspect its configuration.";
|
|
193
|
+
this.inspector.append(description);
|
|
194
|
+
}
|
|
195
|
+
this.drawer.heading = nodeTitle(this.selected, this.catalog);
|
|
196
|
+
}
|
|
197
|
+
this.drawer.open = this.narrow && Boolean(this.selected);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
FlowBuilder.register();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface FlowNode {
|
|
2
|
+
id?: string;
|
|
3
|
+
kind: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
status?: string;
|
|
7
|
+
body?: FlowNode[];
|
|
8
|
+
branches?: FlowBranch[];
|
|
9
|
+
}
|
|
10
|
+
export interface FlowBranch {
|
|
11
|
+
label: string;
|
|
12
|
+
body: FlowNode[];
|
|
13
|
+
}
|
|
14
|
+
export interface FlowKind {
|
|
15
|
+
kind: string;
|
|
16
|
+
label: string;
|
|
17
|
+
description: string;
|
|
18
|
+
group?: string;
|
|
19
|
+
/** Supply an authored SVG node (for example from a named glyph import). */
|
|
20
|
+
icon?: () => Node;
|
|
21
|
+
create(): FlowNode;
|
|
22
|
+
}
|
|
23
|
+
export interface FlowInsertDetail {
|
|
24
|
+
list: FlowNode[];
|
|
25
|
+
index: number;
|
|
26
|
+
label: string;
|
|
27
|
+
}
|
|
28
|
+
export type NodePath = readonly (string | number)[];
|
|
29
|
+
export declare const nodeTitle: (node: FlowNode, catalog?: readonly FlowKind[]) => string;
|
|
30
|
+
export declare function cardLabel(node: FlowNode, catalog?: readonly FlowKind[], invalid?: boolean): string;
|
|
31
|
+
export declare function insertLabel(list: readonly FlowNode[], index: number, where?: string, catalog?: readonly FlowKind[]): string;
|
|
32
|
+
/** Return the deepest node on a validation path, including paths ending in a field. */
|
|
33
|
+
export declare function nodeAtPath(document: unknown, path: NodePath): FlowNode | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const nodeTitle = (node, catalog = []) => node.title || catalog.find(kind => kind.kind === node.kind)?.label || "Step";
|
|
2
|
+
export function cardLabel(node, catalog = [], invalid = false) {
|
|
3
|
+
const kind = catalog.find(entry => entry.kind === node.kind);
|
|
4
|
+
const title = nodeTitle(node, catalog);
|
|
5
|
+
return [title, kind?.label !== title ? kind?.label : "", node.description || kind?.description, node.status, invalid ? "Needs attention" : ""].filter(Boolean).join(". ");
|
|
6
|
+
}
|
|
7
|
+
export function insertLabel(list, index, where = "in the flow", catalog = []) {
|
|
8
|
+
if (!list.length)
|
|
9
|
+
return `Add the first step ${where}`;
|
|
10
|
+
if (index === 0)
|
|
11
|
+
return `Insert before ${nodeTitle(list[0], catalog)} ${where}`;
|
|
12
|
+
if (index >= list.length)
|
|
13
|
+
return `Insert after ${nodeTitle(list[list.length - 1], catalog)} ${where}`;
|
|
14
|
+
return `Insert between ${nodeTitle(list[index - 1], catalog)} and ${nodeTitle(list[index], catalog)} ${where}`;
|
|
15
|
+
}
|
|
16
|
+
/** Return the deepest node on a validation path, including paths ending in a field. */
|
|
17
|
+
export function nodeAtPath(document, path) {
|
|
18
|
+
let current = document;
|
|
19
|
+
let result = null;
|
|
20
|
+
for (const segment of path) {
|
|
21
|
+
if (!current || typeof current !== "object" || !Object.hasOwn(current, segment))
|
|
22
|
+
break;
|
|
23
|
+
current = current[segment];
|
|
24
|
+
if (current && typeof current === "object" && "kind" in current && typeof current.kind === "string")
|
|
25
|
+
result = current;
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { type FlowInsertDetail, type FlowKind, type FlowNode } from "./model.js";
|
|
3
|
+
export declare class FlowCard extends BaseElement {
|
|
4
|
+
static readonly tagName = "box-flow-card";
|
|
5
|
+
node: FlowNode;
|
|
6
|
+
catalog: readonly FlowKind[];
|
|
7
|
+
selected: boolean;
|
|
8
|
+
invalid: boolean;
|
|
9
|
+
protected renderTemplate(): void;
|
|
10
|
+
protected setupListeners(): void;
|
|
11
|
+
refresh(): void;
|
|
12
|
+
focus(options?: FocusOptions): void;
|
|
13
|
+
protected update(): void;
|
|
14
|
+
}
|
|
15
|
+
export declare class InsertPoint extends BaseElement {
|
|
16
|
+
static readonly tagName = "box-insert-point";
|
|
17
|
+
detail: FlowInsertDetail;
|
|
18
|
+
protected renderTemplate(): void;
|
|
19
|
+
protected setupListeners(): void;
|
|
20
|
+
focus(options?: FocusOptions): void;
|
|
21
|
+
protected update(): void;
|
|
22
|
+
}
|
|
23
|
+
export declare class KindPicker extends BaseElement {
|
|
24
|
+
static readonly tagName = "box-kind-picker";
|
|
25
|
+
catalog: readonly FlowKind[];
|
|
26
|
+
variant: "inline" | "menu";
|
|
27
|
+
protected renderTemplate(): void;
|
|
28
|
+
protected setupListeners(): void;
|
|
29
|
+
refresh(): void;
|
|
30
|
+
focus(options?: FocusOptions): void;
|
|
31
|
+
protected update(): void;
|
|
32
|
+
}
|
|
33
|
+
export declare class FlowSpine extends BaseElement {
|
|
34
|
+
static readonly tagName = "box-flow-spine";
|
|
35
|
+
nodes: FlowNode[];
|
|
36
|
+
catalog: readonly FlowKind[];
|
|
37
|
+
selected: FlowNode | null;
|
|
38
|
+
invalid: FlowNode | null;
|
|
39
|
+
startLabel: string;
|
|
40
|
+
endLabel: string;
|
|
41
|
+
private cards;
|
|
42
|
+
protected renderTemplate(): void;
|
|
43
|
+
refresh(): void;
|
|
44
|
+
focusNode(node: FlowNode): void;
|
|
45
|
+
select(node: FlowNode | null, invalid?: FlowNode | null): void;
|
|
46
|
+
protected update(): void;
|
|
47
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { boeFocusVisibleStyles } from "../../foundations/tokens/index.js";
|
|
3
|
+
import { cardLabel, insertLabel, nodeTitle } from "./model.js";
|
|
4
|
+
const styles = `
|
|
5
|
+
:host { display:block; min-width:0; color:var(--boe-token-text-text,#222); font:inherit; }
|
|
6
|
+
:host([hidden]) { display:none!important; }
|
|
7
|
+
button { font:inherit; color:inherit; cursor:pointer; }
|
|
8
|
+
${boeFocusVisibleStyles("button")}
|
|
9
|
+
`;
|
|
10
|
+
function emit(host, name, detail) { host.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })); }
|
|
11
|
+
export class FlowCard extends BaseElement {
|
|
12
|
+
static tagName = "box-flow-card";
|
|
13
|
+
node = { id: "", kind: "" };
|
|
14
|
+
catalog = [];
|
|
15
|
+
selected = false;
|
|
16
|
+
invalid = false;
|
|
17
|
+
renderTemplate() {
|
|
18
|
+
this.shadowRoot.innerHTML = `<style>${styles}
|
|
19
|
+
button { width:100%; text-align:start; display:grid; gap:6px; padding:16px; border:1px solid var(--boe-token-stroke-stroke,#ddd); border-radius:var(--boe-profile-radius-med,8px); background:var(--boe-token-surface-surface,#fff); overflow-wrap:anywhere; }
|
|
20
|
+
button[aria-pressed=true] { border-color:var(--boe-token-surface-surface-brand,#0061d5); background:color-mix(in srgb,var(--boe-token-surface-surface-brand,#0061d5) 8%,var(--boe-token-surface-surface,#fff)); }
|
|
21
|
+
button[data-invalid=true] { border-color:var(--boe-token-text-status-text-error,#b92340); }
|
|
22
|
+
button.has-icon{grid-template-columns:20px minmax(0,1fr);column-gap:12px} button.has-icon [part=icon]{grid-column:1;grid-row:1/span 4} button.has-icon [part=title],button.has-icon [part=kind],button.has-icon [part=description],button.has-icon [part=status],button.has-icon [part=error]{grid-column:2}
|
|
23
|
+
[part=icon]{color:var(--boe-token-surface-surface-brand,#0061d5);width:20px;height:20px} [part=icon] svg{display:block;width:20px;height:20px} [part=title] { font-weight:650; } [part=kind],[part=description] { color:var(--boe-token-text-text-secondary,#666); } [part=error] { color:var(--boe-token-text-status-text-error,#b92340); }
|
|
24
|
+
[hidden] { display:none!important; }
|
|
25
|
+
</style><button type="button" part="card"><span part="icon" aria-hidden="true"></span><span part="title"></span><span part="kind"></span><span part="description"></span><span part="status"></span><span part="error">Needs attention</span></button>`;
|
|
26
|
+
}
|
|
27
|
+
setupListeners() { this.shadowRoot.querySelector("button").addEventListener("click", () => emit(this, "node-select", { node: this.node })); }
|
|
28
|
+
refresh() { if (this.isRendered)
|
|
29
|
+
this.update(); }
|
|
30
|
+
focus(options) { this.shadowRoot?.querySelector("button")?.focus(options); }
|
|
31
|
+
update() {
|
|
32
|
+
const kind = this.catalog.find(kind => kind.kind === this.node.kind);
|
|
33
|
+
const title = nodeTitle(this.node, this.catalog);
|
|
34
|
+
const button = this.shadowRoot.querySelector("button");
|
|
35
|
+
button.setAttribute("aria-pressed", String(this.selected));
|
|
36
|
+
button.classList.toggle("has-icon", Boolean(kind?.icon));
|
|
37
|
+
button.setAttribute("aria-label", cardLabel(this.node, this.catalog, this.invalid));
|
|
38
|
+
button.dataset.invalid = String(this.invalid);
|
|
39
|
+
const iconEl = this.shadowRoot.querySelector("[part=icon]");
|
|
40
|
+
iconEl.replaceChildren(kind?.icon?.() ?? "");
|
|
41
|
+
iconEl.hidden = !kind?.icon;
|
|
42
|
+
for (const [part, text] of Object.entries({ title, kind: kind?.label !== title ? kind?.label ?? "" : "", description: this.node.description || kind?.description || "", status: this.node.status || "" })) {
|
|
43
|
+
const element = this.shadowRoot.querySelector(`[part=${part}]`);
|
|
44
|
+
element.textContent = text;
|
|
45
|
+
element.hidden = !text;
|
|
46
|
+
}
|
|
47
|
+
this.shadowRoot.querySelector("[part=error]").hidden = !this.invalid;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export class InsertPoint extends BaseElement {
|
|
51
|
+
static tagName = "box-insert-point";
|
|
52
|
+
detail = { list: [], index: 0, label: "Add the first step" };
|
|
53
|
+
renderTemplate() { this.shadowRoot.innerHTML = `<style>${styles} :host{display:flex;flex-direction:column;align-items:center;text-align:center;min-width:0} :host::before,:host::after{content:"";height:10px;border-inline-start:2px solid var(--boe-token-stroke-stroke,#ddd)} button{display:grid;place-items:center;min-width:32px;min-height:32px;border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:50%;background:var(--boe-token-surface-surface,#fff);font-weight:700;line-height:1;padding:0 9px} button:hover{border-color:var(--boe-token-surface-surface-brand,#0061d5);color:var(--boe-token-surface-surface-brand,#0061d5)} :host([data-empty]) button{border-radius:16px;font-weight:600;padding:6px 12px}</style><button type="button" part="button" aria-haspopup="menu"></button>`; }
|
|
54
|
+
setupListeners() { this.shadowRoot.querySelector("button").addEventListener("click", () => emit(this, "insert-request", this.detail)); }
|
|
55
|
+
focus(options) { this.shadowRoot?.querySelector("button")?.focus(options); }
|
|
56
|
+
update() { const button = this.shadowRoot.querySelector("button"); const empty = !this.detail.list.length; this.toggleAttribute("data-empty", empty); button.textContent = empty ? this.detail.label : "+"; button.setAttribute("aria-label", this.detail.label); }
|
|
57
|
+
}
|
|
58
|
+
export class KindPicker extends BaseElement {
|
|
59
|
+
static tagName = "box-kind-picker";
|
|
60
|
+
catalog = [];
|
|
61
|
+
variant = "inline";
|
|
62
|
+
renderTemplate() { this.shadowRoot.innerHTML = `<style>${styles} [part=choices]{display:grid;gap:8px} button{text-align:start;padding:10px;border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:8px;background:var(--boe-token-surface-surface,#fff)} small{display:block;color:var(--boe-token-text-text-secondary,#666)} h3{font:inherit;font-weight:650;margin:12px 0 4px}</style><div part="choices"></div>`; }
|
|
63
|
+
setupListeners() {
|
|
64
|
+
this.shadowRoot.addEventListener("keydown", event => {
|
|
65
|
+
const e = event;
|
|
66
|
+
if (this.variant !== "menu")
|
|
67
|
+
return;
|
|
68
|
+
if (e.key === "Escape") {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
emit(this, "picker-cancel", {});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const buttons = Array.from(this.shadowRoot.querySelectorAll("button"));
|
|
74
|
+
if (!buttons.length)
|
|
75
|
+
return;
|
|
76
|
+
const current = buttons.indexOf(this.shadowRoot.activeElement);
|
|
77
|
+
const next = e.key === "Home" ? 0 : e.key === "End" ? buttons.length - 1 : ["ArrowDown", "ArrowRight"].includes(e.key) ? (current + 1) % buttons.length : ["ArrowUp", "ArrowLeft"].includes(e.key) ? (current - 1 + buttons.length) % buttons.length : -1;
|
|
78
|
+
if (next < 0)
|
|
79
|
+
return;
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
buttons.forEach((button, index) => button.tabIndex = index === next ? 0 : -1);
|
|
82
|
+
buttons[next].focus();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
refresh() { if (this.isRendered)
|
|
86
|
+
this.update(); }
|
|
87
|
+
focus(options) { this.shadowRoot?.querySelector("button")?.focus(options); }
|
|
88
|
+
update() {
|
|
89
|
+
const choices = this.shadowRoot.querySelector("[part=choices]");
|
|
90
|
+
choices.setAttribute("role", this.variant === "menu" ? "menu" : "group");
|
|
91
|
+
choices.setAttribute("aria-label", "Choose a step type");
|
|
92
|
+
choices.replaceChildren();
|
|
93
|
+
let group = "";
|
|
94
|
+
this.catalog.forEach((kind, index) => {
|
|
95
|
+
if (kind.group && group !== kind.group) {
|
|
96
|
+
const heading = document.createElement("h3");
|
|
97
|
+
heading.textContent = kind.group;
|
|
98
|
+
heading.setAttribute("role", "presentation");
|
|
99
|
+
choices.append(heading);
|
|
100
|
+
group = kind.group;
|
|
101
|
+
}
|
|
102
|
+
const button = document.createElement("button");
|
|
103
|
+
button.type = "button";
|
|
104
|
+
button.textContent = kind.label;
|
|
105
|
+
if (this.variant === "menu") {
|
|
106
|
+
button.setAttribute("role", "menuitem");
|
|
107
|
+
button.tabIndex = index === 0 ? 0 : -1;
|
|
108
|
+
}
|
|
109
|
+
const description = document.createElement("small");
|
|
110
|
+
description.textContent = kind.description;
|
|
111
|
+
button.append(description);
|
|
112
|
+
button.addEventListener("click", () => emit(this, "kind-pick", { kind }));
|
|
113
|
+
choices.append(button);
|
|
114
|
+
});
|
|
115
|
+
if (!this.catalog.length)
|
|
116
|
+
choices.textContent = "No step types available.";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export class FlowSpine extends BaseElement {
|
|
120
|
+
static tagName = "box-flow-spine";
|
|
121
|
+
nodes = [];
|
|
122
|
+
catalog = [];
|
|
123
|
+
selected = null;
|
|
124
|
+
invalid = null;
|
|
125
|
+
startLabel = "Flow starts";
|
|
126
|
+
endLabel = "Flow ends";
|
|
127
|
+
cards = new Map();
|
|
128
|
+
renderTemplate() { this.shadowRoot.innerHTML = `<style>${styles} [part=spine]{display:grid;gap:0;min-width:0} [part=branches]{display:flex;gap:16px;overflow-x:auto;max-width:100%;padding:8px} [part=branch]{flex:1 0 240px;min-width:0} [part=body]{margin:0 0 0 16px;border-inline-start:2px solid var(--boe-token-stroke-stroke,#ddd);padding:8px 12px} [part=endpoint]{text-align:center;font-weight:600;padding:12px} h3{font:inherit;font-weight:600}</style><div part="spine"></div>`; }
|
|
129
|
+
refresh() { if (this.isRendered)
|
|
130
|
+
this.update(); }
|
|
131
|
+
focusNode(node) { this.cards.get(node)?.focus(); }
|
|
132
|
+
select(node, invalid = this.invalid) { this.selected = node; this.invalid = invalid; for (const [item, card] of this.cards) {
|
|
133
|
+
card.selected = item === node;
|
|
134
|
+
card.invalid = item === invalid;
|
|
135
|
+
card.refresh();
|
|
136
|
+
} }
|
|
137
|
+
update() {
|
|
138
|
+
const root = this.shadowRoot.querySelector("[part=spine]");
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
const listView = (list, where, ancestors) => {
|
|
141
|
+
const fragment = document.createDocumentFragment();
|
|
142
|
+
for (let index = 0; index <= list.length; index++) {
|
|
143
|
+
const point = document.createElement(InsertPoint.tagName);
|
|
144
|
+
point.detail = { list, index, label: insertLabel(list, index, where, this.catalog) };
|
|
145
|
+
fragment.append(point);
|
|
146
|
+
if (index === list.length)
|
|
147
|
+
break;
|
|
148
|
+
const node = list[index];
|
|
149
|
+
if (ancestors.has(node) || seen.has(node))
|
|
150
|
+
continue;
|
|
151
|
+
seen.add(node);
|
|
152
|
+
const card = this.cards.get(node) ?? document.createElement(FlowCard.tagName);
|
|
153
|
+
this.cards.set(node, card);
|
|
154
|
+
card.node = node;
|
|
155
|
+
card.catalog = this.catalog;
|
|
156
|
+
card.selected = node === this.selected;
|
|
157
|
+
card.invalid = node === this.invalid;
|
|
158
|
+
card.refresh();
|
|
159
|
+
fragment.append(card);
|
|
160
|
+
const nextAncestors = new Set([...ancestors, node]);
|
|
161
|
+
if (node.body) {
|
|
162
|
+
const body = document.createElement("section");
|
|
163
|
+
body.setAttribute("part", "body");
|
|
164
|
+
body.setAttribute("aria-label", `${nodeTitle(node, this.catalog)} steps`);
|
|
165
|
+
body.append(listView(node.body, `inside ${nodeTitle(node, this.catalog)}`, nextAncestors));
|
|
166
|
+
fragment.append(body);
|
|
167
|
+
}
|
|
168
|
+
if (node.branches) {
|
|
169
|
+
const branches = document.createElement("div");
|
|
170
|
+
branches.setAttribute("part", "branches");
|
|
171
|
+
node.branches.forEach(branch => { const column = document.createElement("section"); column.setAttribute("part", "branch"); column.setAttribute("aria-label", branch.label); const heading = document.createElement("h3"); heading.textContent = branch.label; column.append(heading, listView(branch.body, `in ${branch.label}`, nextAncestors)); branches.append(column); });
|
|
172
|
+
fragment.append(branches);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return fragment;
|
|
176
|
+
};
|
|
177
|
+
const start = document.createElement("div");
|
|
178
|
+
start.setAttribute("part", "endpoint");
|
|
179
|
+
start.textContent = this.startLabel;
|
|
180
|
+
const end = document.createElement("div");
|
|
181
|
+
end.setAttribute("part", "endpoint");
|
|
182
|
+
end.textContent = this.endLabel;
|
|
183
|
+
root.replaceChildren(start, listView(this.nodes, "in the flow", new Set()), end);
|
|
184
|
+
for (const node of this.cards.keys())
|
|
185
|
+
if (!seen.has(node))
|
|
186
|
+
this.cards.delete(node);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
FlowCard.register();
|
|
190
|
+
InsertPoint.register();
|
|
191
|
+
KindPicker.register();
|
|
192
|
+
FlowSpine.register();
|
|
@@ -35,6 +35,7 @@ const elementStyles = `
|
|
|
35
35
|
|
|
36
36
|
:host {
|
|
37
37
|
display: block;
|
|
38
|
+
min-inline-size: 0;
|
|
38
39
|
color: inherit;
|
|
39
40
|
font: inherit;
|
|
40
41
|
}
|
|
@@ -47,6 +48,7 @@ const elementStyles = `
|
|
|
47
48
|
|
|
48
49
|
section[part="panel"] {
|
|
49
50
|
display: grid;
|
|
51
|
+
grid-template-columns: minmax(0, 1fr);
|
|
50
52
|
gap: ${boePanel.gap};
|
|
51
53
|
padding: ${boePanel.padding};
|
|
52
54
|
border: 1px solid color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 82%, transparent);
|
|
@@ -55,6 +57,7 @@ const elementStyles = `
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
[part="title"] {
|
|
60
|
+
overflow-wrap: anywhere;
|
|
58
61
|
margin: 0;
|
|
59
62
|
font: inherit;
|
|
60
63
|
font-size: 1.1rem;
|
|
@@ -65,11 +68,24 @@ const elementStyles = `
|
|
|
65
68
|
[part="graph"] {
|
|
66
69
|
position: relative;
|
|
67
70
|
overflow-x: auto;
|
|
71
|
+
min-inline-size: 0;
|
|
72
|
+
padding-block: 3px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
[part="graph"]:focus-visible {
|
|
76
|
+
outline: 2px solid var(--boe-token-surface-surface-brand, #0061d5);
|
|
77
|
+
outline-offset: 2px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
[part="scroll-hint"] {
|
|
81
|
+
margin: 0;
|
|
82
|
+
font-size: 0.78rem;
|
|
83
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
68
84
|
}
|
|
69
85
|
|
|
70
86
|
[part="edges"] {
|
|
71
87
|
position: absolute;
|
|
72
|
-
inset-block-start:
|
|
88
|
+
inset-block-start: 3px;
|
|
73
89
|
inset-inline-start: 0;
|
|
74
90
|
pointer-events: none;
|
|
75
91
|
}
|
|
@@ -104,6 +120,8 @@ const elementStyles = `
|
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
[part="rows"] {
|
|
123
|
+
inline-size: max-content;
|
|
124
|
+
min-inline-size: 100%;
|
|
107
125
|
list-style: none;
|
|
108
126
|
margin: 0;
|
|
109
127
|
padding: 0;
|
|
@@ -113,7 +131,8 @@ const elementStyles = `
|
|
|
113
131
|
[part="row"] {
|
|
114
132
|
position: relative;
|
|
115
133
|
display: flex;
|
|
116
|
-
flex-wrap:
|
|
134
|
+
flex-wrap: nowrap;
|
|
135
|
+
white-space: nowrap;
|
|
117
136
|
align-items: center;
|
|
118
137
|
gap: 0.45rem;
|
|
119
138
|
block-size: ${ROW_HEIGHT}px;
|
|
@@ -192,13 +211,13 @@ const elementStyles = `
|
|
|
192
211
|
}
|
|
193
212
|
|
|
194
213
|
[part="edge-chip"][data-deviation="minor"] {
|
|
195
|
-
border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-
|
|
196
|
-
color:
|
|
214
|
+
border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 55%, transparent);
|
|
215
|
+
color: var(--boe-token-text-status-text-warning, #805600);
|
|
197
216
|
}
|
|
198
217
|
|
|
199
218
|
[part="edge-chip"][data-deviation="major"] {
|
|
200
219
|
border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 45%, transparent);
|
|
201
|
-
color:
|
|
220
|
+
color: var(--boe-token-text-status-text-error, #b92340);
|
|
202
221
|
}
|
|
203
222
|
|
|
204
223
|
[part="edge-chip"]:focus-visible {
|
|
@@ -481,7 +500,8 @@ export class LineageGraph extends BaseElement {
|
|
|
481
500
|
<h2 part="title">${escapeHtml(this.heading)}</h2>
|
|
482
501
|
${layout.placements.length > 0
|
|
483
502
|
? `
|
|
484
|
-
<
|
|
503
|
+
<p part="scroll-hint">Scroll horizontally to explore nodes and comparisons.</p>
|
|
504
|
+
<div part="graph" role="region" aria-label="${escapeHtml(this.heading)} — scroll horizontally to explore" tabindex="0">
|
|
485
505
|
<svg part="edges" width="${String(graphWidth)}" height="${String(graphHeight)}" viewBox="0 0 ${String(graphWidth)} ${String(graphHeight)}" aria-hidden="true"><defs><marker id="boe-graph-arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="4" markerHeight="4" orient="auto-start-reverse"><path part="edge-arrow" d="M 0 0 L 8 4 L 0 8 z"></path></marker><marker id="boe-graph-arrow-minor" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="4" markerHeight="4" orient="auto-start-reverse"><path part="edge-arrow" data-deviation="minor" d="M 0 0 L 8 4 L 0 8 z"></path></marker><marker id="boe-graph-arrow-major" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="4" markerHeight="4" orient="auto-start-reverse"><path part="edge-arrow" data-deviation="major" d="M 0 0 L 8 4 L 0 8 z"></path></marker></defs>${edgePaths}</svg>
|
|
486
506
|
<ol part="rows" role="list">${rows}</ol>
|
|
487
507
|
</div>
|