@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.
Files changed (57) hide show
  1. package/dist/components/actions/menu-item.d.ts +2 -0
  2. package/dist/components/actions/menu-item.js +4 -0
  3. package/dist/components/collections/card.js +6 -0
  4. package/dist/components/collections/result-blocks.js +4 -1
  5. package/dist/components/feedback/alert.d.ts +2 -0
  6. package/dist/components/feedback/alert.js +17 -13
  7. package/dist/components/feedback/toast.d.ts +10 -1
  8. package/dist/components/feedback/toast.js +69 -27
  9. package/dist/components/forms/number-input.d.ts +1 -0
  10. package/dist/components/forms/number-input.js +9 -6
  11. package/dist/components/forms/search-field.js +16 -0
  12. package/dist/components/overlays/context-menu.d.ts +2 -0
  13. package/dist/components/overlays/context-menu.js +7 -0
  14. package/dist/components/overlays/dialog.js +19 -3
  15. package/dist/components/overlays/popover.js +6 -1
  16. package/dist/element-maps.d.ts +11 -1
  17. package/dist/foundations/a11y/announcer.d.ts +12 -0
  18. package/dist/foundations/a11y/announcer.js +75 -0
  19. package/dist/foundations/a11y/focus.js +46 -25
  20. package/dist/foundations/a11y/index.d.ts +2 -0
  21. package/dist/foundations/a11y/index.js +1 -0
  22. package/dist/foundations/icons/box-iconography.d.ts +472 -472
  23. package/dist/foundations/icons/box-iconography.generated.d.ts +1151 -679
  24. package/dist/foundations/icons/box-iconography.generated.js +1006 -3588
  25. package/dist/foundations/icons/glyphs/index.d.ts +1 -0
  26. package/dist/foundations/icons/glyphs/index.js +2 -0
  27. package/dist/foundations/icons/glyphs/package.json +1 -0
  28. package/dist/foundations/overlay/positioning.js +7 -0
  29. package/dist/foundations/status/index.d.ts +1 -1
  30. package/dist/foundations/status/index.js +8 -6
  31. package/dist/foundations/tokens/box-dark.js +4 -1
  32. package/dist/foundations/tokens/box-defaults.js +3 -0
  33. package/dist/foundations/tokens/registry.d.ts +3 -0
  34. package/dist/foundations/tokens/registry.js +3 -0
  35. package/dist/foundations/tokens/types.d.ts +3 -0
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.js +2 -0
  38. package/dist/patterns/agent-chat/agent-chat.js +12 -4
  39. package/dist/patterns/call-console/call-console.d.ts +3 -0
  40. package/dist/patterns/call-console/call-console.js +114 -20
  41. package/dist/patterns/content-explorer/adapters/action-menu.js +7 -5
  42. package/dist/patterns/content-explorer/adapters/toolbar.js +6 -2
  43. package/dist/patterns/file-request/file-request-builder.js +3 -0
  44. package/dist/patterns/flow-builder/flow-builder.d.ts +48 -0
  45. package/dist/patterns/flow-builder/flow-builder.js +200 -0
  46. package/dist/patterns/flow-builder/index.d.ts +3 -0
  47. package/dist/patterns/flow-builder/index.js +3 -0
  48. package/dist/patterns/flow-builder/model.d.ts +33 -0
  49. package/dist/patterns/flow-builder/model.js +28 -0
  50. package/dist/patterns/flow-builder/primitives.d.ts +47 -0
  51. package/dist/patterns/flow-builder/primitives.js +192 -0
  52. package/dist/patterns/lineage/lineage-graph.js +26 -6
  53. package/dist/patterns/notifications/notification-inbox.js +19 -3
  54. package/dist/patterns/preview/annotation-inspector.js +2 -2
  55. package/dist/patterns/undo/index.d.ts +19 -0
  56. package/dist/patterns/undo/index.js +106 -0
  57. 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,3 @@
1
+ export * from "./model.js";
2
+ export * from "./primitives.js";
3
+ export * from "./flow-builder.js";
@@ -0,0 +1,3 @@
1
+ export * from "./model.js";
2
+ export * from "./primitives.js";
3
+ export * from "./flow-builder.js";
@@ -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: 0;
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: 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-warning, #f5b31b) 55%, transparent);
196
- color: color-mix(in srgb, var(--boe-token-surface-status-surface-warning, #f5b31b) 46%, black 54%);
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: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 74%, black 26%);
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
- <div part="graph">
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>