@unofficialbox/box-open-elements 0.21.0 → 0.23.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 (115) hide show
  1. package/README.md +13 -12
  2. package/dist/components/actions/menu-item.js +1 -1
  3. package/dist/components/actions/menu.js +1 -1
  4. package/dist/components/collections/carousel.js +1 -1
  5. package/dist/components/collections/draggable-list.js +1 -1
  6. package/dist/components/collections/grid-view.js +1 -1
  7. package/dist/components/collections/pagination.js +1 -1
  8. package/dist/components/collections/result-blocks.d.ts +13 -1
  9. package/dist/components/collections/result-blocks.js +22 -10
  10. package/dist/components/collections/tree-grid.js +1 -1
  11. package/dist/components/feedback/alert.js +1 -1
  12. package/dist/components/feedback/chip.js +1 -1
  13. package/dist/components/feedback/empty-state.js +1 -1
  14. package/dist/components/feedback/nudge.js +1 -1
  15. package/dist/components/feedback/status-icon.d.ts +2 -0
  16. package/dist/components/feedback/status-icon.js +6 -5
  17. package/dist/components/feedback/toast.js +1 -1
  18. package/dist/components/files/drop-zone.js +1 -1
  19. package/dist/components/forms/category-selector.js +1 -1
  20. package/dist/components/forms/checkbox-group.js +1 -1
  21. package/dist/components/forms/checkbox.js +1 -1
  22. package/dist/components/forms/combobox.js +1 -1
  23. package/dist/components/forms/date-field.js +1 -1
  24. package/dist/components/forms/dropdown.js +1 -1
  25. package/dist/components/forms/multi-select.js +1 -1
  26. package/dist/components/forms/number-input.js +1 -1
  27. package/dist/components/forms/radio-group.js +1 -1
  28. package/dist/components/forms/range-slider.js +1 -1
  29. package/dist/components/forms/slider.js +1 -1
  30. package/dist/components/forms/tile-group.js +1 -1
  31. package/dist/components/layout/sidebar-toggle-button.js +1 -1
  32. package/dist/components/navigation/accordion.js +1 -1
  33. package/dist/components/navigation/tabs.js +1 -1
  34. package/dist/components/output/code-block.js +1 -1
  35. package/dist/components/overlays/dialog.js +1 -1
  36. package/dist/components/overlays/drawer.js +1 -1
  37. package/dist/components/overlays/guide-tooltip.js +1 -1
  38. package/dist/components/overlays/popover.js +1 -1
  39. package/dist/components/overlays/tooltip.js +1 -1
  40. package/dist/foundations/motion/scroll-pin.d.ts +10 -2
  41. package/dist/foundations/motion/scroll-pin.js +43 -7
  42. package/dist/foundations/motion/tokens.d.ts +1 -0
  43. package/dist/foundations/motion/tokens.js +1 -0
  44. package/dist/foundations/profiles/built-ins.js +8 -0
  45. package/dist/foundations/profiles/registry.d.ts +1 -0
  46. package/dist/foundations/profiles/registry.js +1 -0
  47. package/dist/foundations/profiles/types.d.ts +1 -0
  48. package/dist/foundations/status/index.d.ts +4 -2
  49. package/dist/foundations/status/index.js +10 -6
  50. package/dist/foundations/theming/controller.js +3 -1
  51. package/dist/foundations/tokens/box-defaults.d.ts +2 -0
  52. package/dist/foundations/tokens/box-defaults.js +32 -5
  53. package/dist/foundations/tokens/index.d.ts +1 -1
  54. package/dist/foundations/tokens/index.js +1 -1
  55. package/dist/index.d.ts +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/patterns/agent-chat/agent-chat.js +9 -10
  58. package/dist/patterns/agent-chat/controller.js +8 -4
  59. package/dist/patterns/agent-chat/ndjson.d.ts +1 -0
  60. package/dist/patterns/agent-chat/ndjson.js +20 -4
  61. package/dist/patterns/agent-chat/types.d.ts +7 -0
  62. package/dist/patterns/agent-workspace/agent-workspace.d.ts +13 -1
  63. package/dist/patterns/agent-workspace/agent-workspace.js +47 -15
  64. package/dist/patterns/agent-workspace/controller.d.ts +7 -1
  65. package/dist/patterns/agent-workspace/controller.js +41 -3
  66. package/dist/patterns/call-console/call-console.d.ts +10 -0
  67. package/dist/patterns/call-console/call-console.js +23 -8
  68. package/dist/patterns/call-console/controller.d.ts +2 -1
  69. package/dist/patterns/call-console/controller.js +5 -3
  70. package/dist/patterns/call-console/http-transport.js +13 -2
  71. package/dist/patterns/call-console/types.d.ts +1 -1
  72. package/dist/patterns/call-console/types.js +2 -2
  73. package/dist/patterns/comments/comment-thread.js +1 -1
  74. package/dist/patterns/content-explorer/adapters/action-menu.js +1 -1
  75. package/dist/patterns/content-explorer/adapters/list.js +1 -1
  76. package/dist/patterns/content-explorer/adapters/table.js +1 -1
  77. package/dist/patterns/content-explorer/adapters/toolbar.js +1 -1
  78. package/dist/patterns/content-uploader/content-uploader.js +1 -1
  79. package/dist/patterns/file-request/file-request-builder.js +1 -1
  80. package/dist/patterns/flow-builder/flow-builder.d.ts +45 -14
  81. package/dist/patterns/flow-builder/flow-builder.js +54 -11
  82. package/dist/patterns/flow-builder/model.d.ts +57 -9
  83. package/dist/patterns/flow-builder/model.js +20 -8
  84. package/dist/patterns/flow-builder/primitives.d.ts +17 -7
  85. package/dist/patterns/flow-builder/primitives.js +80 -16
  86. package/dist/patterns/governance/governance-panel.js +1 -1
  87. package/dist/patterns/insights/chart-panel.js +1 -1
  88. package/dist/patterns/insights/metric-card.js +1 -1
  89. package/dist/patterns/item/bulk-action-bar.js +1 -1
  90. package/dist/patterns/item/item-details-panel.js +1 -1
  91. package/dist/patterns/item/item-form.js +1 -1
  92. package/dist/patterns/item/preview-header.js +1 -1
  93. package/dist/patterns/metadata/metadata-filter-builder.js +1 -1
  94. package/dist/patterns/metadata/metadata-inspector.js +1 -1
  95. package/dist/patterns/preview/annotation-inspector.js +1 -1
  96. package/dist/patterns/preview/annotation-toolbar.js +1 -1
  97. package/dist/patterns/run/progress.d.ts +1 -1
  98. package/dist/patterns/run/progress.js +1 -1
  99. package/dist/patterns/run/run-summary.js +18 -4
  100. package/dist/patterns/run/run-trace.js +19 -5
  101. package/dist/patterns/search/filter-bar.js +1 -1
  102. package/dist/patterns/search/saved-view-picker.js +1 -1
  103. package/dist/patterns/search/search-results-header.js +1 -1
  104. package/dist/patterns/share/invite-collaborators-modal.js +1 -1
  105. package/dist/patterns/share/permission-matrix.js +1 -1
  106. package/dist/patterns/share/share-panel.js +1 -1
  107. package/dist/patterns/share/unified-share-modal.js +1 -1
  108. package/dist/patterns/task/review-queue-item.js +1 -1
  109. package/dist/patterns/task/task-assignment-panel.js +1 -1
  110. package/dist/patterns/undo/index.js +5 -3
  111. package/docs/api-guidelines.md +284 -0
  112. package/docs/foundations/motion.md +83 -0
  113. package/docs/patterns/agent-ui-guidelines.md +57 -0
  114. package/docs/patterns/agent-ui.md +197 -0
  115. package/package.json +6 -2
@@ -1,16 +1,30 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
- import { type FlowKind, type FlowNode, type NodePath } from "./model.js";
3
- export type InspectorRenderer = (node: FlowNode, container: HTMLElement) => void | (() => void);
4
- /** Host-owned document editing with accessible navigation and responsive inspection. */
5
- export declare class FlowBuilder extends BaseElement {
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 {
6
18
  static readonly tagName = "box-flow-builder";
7
19
  private nodesValue;
20
+ private modelValue;
8
21
  private catalogValue;
9
22
  private selectedValue;
10
23
  private invalidValue;
11
24
  private renderer?;
12
25
  private cleanupInspector?;
13
26
  private inspected;
27
+ private headingEl?;
14
28
  private renderedRenderer?;
15
29
  private observer?;
16
30
  private narrow;
@@ -24,14 +38,25 @@ export declare class FlowBuilder extends BaseElement {
24
38
  private inspector;
25
39
  private drawer;
26
40
  private error;
27
- get nodes(): FlowNode[];
28
- set nodes(value: FlowNode[]);
29
- get catalog(): readonly FlowKind[];
30
- set catalog(value: readonly FlowKind[]);
31
- get selected(): FlowNode | null;
32
- set selected(node: FlowNode | null);
33
- get renderInspector(): InspectorRenderer | undefined;
34
- set renderInspector(renderer: InspectorRenderer | undefined);
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;
35
60
  connectedCallback(): void;
36
61
  disconnectedCallback(): void;
37
62
  private setNarrow;
@@ -39,10 +64,16 @@ export declare class FlowBuilder extends BaseElement {
39
64
  protected setupListeners(): void;
40
65
  private closePicker;
41
66
  private insert;
42
- select(node: FlowNode | null): void;
67
+ select(node: N | null): void;
43
68
  /** Mark validation without opening a sheet over the error on a narrow screen. */
44
- setValidation(message: string, path?: NodePath, document?: unknown): void;
69
+ setValidation(message: string, path?: NodePath, document?: unknown, options?: ValidationOptions): void;
45
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;
46
77
  protected update(): void;
47
78
  private updateInspector;
48
79
  }
@@ -1,17 +1,23 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
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. */
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
+ */
6
10
  export class FlowBuilder extends BaseElement {
7
11
  static tagName = "box-flow-builder";
8
12
  nodesValue = [];
13
+ modelValue = defaultFlowModel;
9
14
  catalogValue = [];
10
15
  selectedValue = null;
11
16
  invalidValue = null;
12
17
  renderer;
13
18
  cleanupInspector;
14
19
  inspected;
20
+ headingEl;
15
21
  renderedRenderer;
16
22
  observer;
17
23
  narrow = false;
@@ -25,8 +31,18 @@ export class FlowBuilder extends BaseElement {
25
31
  inspector;
26
32
  drawer;
27
33
  error;
34
+ static get observedAttributes() { return ["start-label", "end-label"]; }
28
35
  get nodes() { return this.nodesValue; }
29
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); }
30
46
  get catalog() { return this.catalogValue; }
31
47
  set catalog(value) { this.catalogValue = value; this.refresh(); }
32
48
  get selected() { return this.selectedValue; }
@@ -34,6 +50,11 @@ export class FlowBuilder extends BaseElement {
34
50
  get renderInspector() { return this.renderer; }
35
51
  set renderInspector(renderer) { this.renderer = renderer; if (this.isRendered)
36
52
  this.updateInspector(); }
53
+ attributeChangedCallback(name, oldValue, newValue) {
54
+ super.attributeChangedCallback(name, oldValue, newValue);
55
+ if (oldValue !== newValue)
56
+ this.refresh();
57
+ }
37
58
  connectedCallback() {
38
59
  super.connectedCallback();
39
60
  if (typeof ResizeObserver !== "undefined") {
@@ -68,7 +89,7 @@ export class FlowBuilder extends BaseElement {
68
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}
69
90
  [part=error]{color:var(--boe-token-text-status-text-error,#b92340);margin-bottom:16px;overflow-wrap:anywhere}
70
91
  [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>`;
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>`;
72
93
  this.spine = this.shadowRoot.querySelector("box-flow-spine");
73
94
  this.aside = this.shadowRoot.querySelector("aside");
74
95
  this.palette = this.aside.querySelector("box-kind-picker");
@@ -129,7 +150,7 @@ export class FlowBuilder extends BaseElement {
129
150
  this.refresh();
130
151
  // Keep the newly inserted card visible on small screens; explicit selection opens editing.
131
152
  this.spine.focusNode(node);
132
- announce(`Added ${nodeTitle(node, this.catalog)}`, "polite", this.ownerDocument);
153
+ announce(`Added ${nodeTitle(node, this.catalog, this.model)}`, "polite", this.ownerDocument);
133
154
  this.dispatchEvent(new CustomEvent("flow-changed", { detail: { nodes: this.nodes, node, reason: "insert" }, bubbles: true, composed: true }));
134
155
  }
135
156
  select(node) {
@@ -141,10 +162,11 @@ export class FlowBuilder extends BaseElement {
141
162
  this.dispatchEvent(new CustomEvent("selection-changed", { detail: { node }, bubbles: true, composed: true }));
142
163
  }
143
164
  /** Mark validation without opening a sheet over the error on a narrow screen. */
144
- setValidation(message, path = [], document = { body: this.nodes }) {
165
+ setValidation(message, path = [], document = { body: this.nodes }, options = {}) {
145
166
  this.invalidValue = message ? nodeAtPath(document, path) : null;
146
- this.error.textContent = message;
147
- this.error.hidden = !message;
167
+ const show = options.showMessage ?? true;
168
+ this.error.textContent = show ? message : "";
169
+ this.error.hidden = !message || !show;
148
170
  this.spine.select(this.selected, this.invalidValue);
149
171
  if (message) {
150
172
  if (!this.narrow && this.invalidValue)
@@ -156,9 +178,28 @@ export class FlowBuilder extends BaseElement {
156
178
  }
157
179
  refresh() { if (this.isRendered)
158
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
+ }
159
197
  update() {
160
198
  this.spine.nodes = this.nodes;
161
199
  this.spine.catalog = this.catalog;
200
+ this.spine.model = this.model;
201
+ this.spine.startLabel = this.startLabel;
202
+ this.spine.endLabel = this.endLabel;
162
203
  this.spine.selected = this.selected;
163
204
  this.spine.invalid = this.invalidValue;
164
205
  this.spine.refresh();
@@ -183,16 +224,18 @@ export class FlowBuilder extends BaseElement {
183
224
  this.renderedRenderer = this.renderer;
184
225
  if (this.selected) {
185
226
  const heading = document.createElement("h2");
186
- heading.textContent = nodeTitle(this.selected, this.catalog);
227
+ heading.setAttribute("part", "inspector-heading");
228
+ heading.textContent = nodeTitle(this.selected, this.catalog, this.model);
187
229
  this.inspector.append(heading);
230
+ this.headingEl = heading;
188
231
  if (this.renderer)
189
232
  this.cleanupInspector = this.renderer(this.selected, this.inspector) || undefined;
190
233
  else {
191
234
  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.";
235
+ description.textContent = nodeDescription(this.selected, this.catalog, this.model) || "Select a step to inspect its configuration.";
193
236
  this.inspector.append(description);
194
237
  }
195
- this.drawer.heading = nodeTitle(this.selected, this.catalog);
238
+ this.drawer.heading = nodeTitle(this.selected, this.catalog, this.model);
196
239
  }
197
240
  this.drawer.open = this.narrow && Boolean(this.selected);
198
241
  }
@@ -1,6 +1,13 @@
1
- export interface FlowNode {
2
- id?: string;
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 {
3
6
  kind: string;
7
+ }
8
+ /** The document shape the default model reads. */
9
+ export interface FlowNode extends FlowNodeBase {
10
+ id?: string;
4
11
  title?: string;
5
12
  description?: string;
6
13
  status?: string;
@@ -11,23 +18,64 @@ export interface FlowBranch {
11
18
  label: string;
12
19
  body: FlowNode[];
13
20
  }
14
- export interface FlowKind {
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> {
15
50
  kind: string;
16
51
  label: string;
17
52
  description: string;
18
53
  group?: string;
19
54
  /** Supply an authored SVG node (for example from a named glyph import). */
20
55
  icon?: () => Node;
21
- create(): FlowNode;
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;
22
62
  }
23
63
  export interface FlowInsertDetail {
24
- list: FlowNode[];
64
+ list: FlowNodeBase[];
25
65
  index: number;
26
66
  label: string;
27
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
+ }
28
75
  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;
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;
32
80
  /** 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;
81
+ export declare function nodeAtPath(document: unknown, path: NodePath): FlowNodeBase | null;
@@ -1,17 +1,29 @@
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) {
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) {
3
14
  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(". ");
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(". ");
6
17
  }
7
- export function insertLabel(list, index, where = "in the flow", catalog = []) {
18
+ export function insertLabel(list, index, where = "in the flow", catalog = [], model = defaultFlowModel) {
19
+ const title = (node) => nodeTitle(node, catalog, model);
8
20
  if (!list.length)
9
21
  return `Add the first step ${where}`;
10
22
  if (index === 0)
11
- return `Insert before ${nodeTitle(list[0], catalog)} ${where}`;
23
+ return `Insert before ${title(list[0])} ${where}`;
12
24
  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}`;
25
+ return `Insert after ${title(list[list.length - 1])} ${where}`;
26
+ return `Insert between ${title(list[index - 1])} and ${title(list[index])} ${where}`;
15
27
  }
16
28
  /** Return the deepest node on a validation path, including paths ending in a field. */
17
29
  export function nodeAtPath(document, path) {
@@ -1,9 +1,16 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
- import { type FlowInsertDetail, type FlowKind, type FlowNode } from "./model.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"];
3
9
  export declare class FlowCard extends BaseElement {
4
10
  static readonly tagName = "box-flow-card";
5
- node: FlowNode;
11
+ node: FlowNodeBase;
6
12
  catalog: readonly FlowKind[];
13
+ model: FlowModel<FlowNodeBase>;
7
14
  selected: boolean;
8
15
  invalid: boolean;
9
16
  protected renderTemplate(): void;
@@ -32,16 +39,19 @@ export declare class KindPicker extends BaseElement {
32
39
  }
33
40
  export declare class FlowSpine extends BaseElement {
34
41
  static readonly tagName = "box-flow-spine";
35
- nodes: FlowNode[];
42
+ nodes: FlowNodeBase[];
36
43
  catalog: readonly FlowKind[];
37
- selected: FlowNode | null;
38
- invalid: FlowNode | null;
44
+ model: FlowModel<FlowNodeBase>;
45
+ selected: FlowNodeBase | null;
46
+ invalid: FlowNodeBase | null;
39
47
  startLabel: string;
40
48
  endLabel: string;
41
49
  private cards;
42
50
  protected renderTemplate(): void;
43
51
  refresh(): void;
44
- focusNode(node: FlowNode): void;
45
- select(node: FlowNode | null, invalid?: FlowNode | null): 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;
46
56
  protected update(): void;
47
57
  }
@@ -1,6 +1,6 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
- import { boeFocusVisibleStyles } from "../../foundations/tokens/index.js";
3
- import { cardLabel, insertLabel, nodeTitle } from "./model.js";
2
+ import { boeFocusVisibleStyles } from "../../foundations/tokens/interaction.js";
3
+ import { cardLabel, defaultFlowModel, insertLabel, nodeDescription, nodeTitle } from "./model.js";
4
4
  const styles = `
5
5
  :host { display:block; min-width:0; color:var(--boe-token-text-text,#222); font:inherit; }
6
6
  :host([hidden]) { display:none!important; }
@@ -8,10 +8,17 @@ const styles = `
8
8
  ${boeFocusVisibleStyles("button")}
9
9
  `;
10
10
  function emit(host, name, detail) { host.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })); }
11
+ /** Parts a card exposes; the spine and builder forward them unchanged. */
12
+ export const FLOW_CARD_PARTS = ["card", "icon", "title", "kind", "description", "status", "error"];
13
+ /** Parts the spine exposes (its own plus forwarded card and insert-point parts). */
14
+ export const FLOW_SPINE_PARTS = ["spine", "endpoint", "body", "branches", "branch", "branch-header", "branch-label", "branch-remove", "branch-add", "insert", ...FLOW_CARD_PARTS];
15
+ /** Parts a kind picker exposes. */
16
+ export const KIND_PICKER_PARTS = ["choices", "group-heading", "choice", "choice-icon", "choice-label", "choice-description"];
11
17
  export class FlowCard extends BaseElement {
12
18
  static tagName = "box-flow-card";
13
- node = { id: "", kind: "" };
19
+ node = { kind: "" };
14
20
  catalog = [];
21
+ model = defaultFlowModel;
15
22
  selected = false;
16
23
  invalid = false;
17
24
  renderTemplate() {
@@ -20,7 +27,7 @@ export class FlowCard extends BaseElement {
20
27
  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
28
  button[data-invalid=true] { border-color:var(--boe-token-text-status-text-error,#b92340); }
22
29
  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); }
30
+ [part=icon]{color:var(--boe-token-surface-surface-brand,#0061d5);width:20px;height:20px} [part=icon][data-tone=neutral]{color:var(--boe-token-text-text-secondary,#666)} [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
31
  [hidden] { display:none!important; }
25
32
  </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
33
  }
@@ -30,16 +37,17 @@ export class FlowCard extends BaseElement {
30
37
  focus(options) { this.shadowRoot?.querySelector("button")?.focus(options); }
31
38
  update() {
32
39
  const kind = this.catalog.find(kind => kind.kind === this.node.kind);
33
- const title = nodeTitle(this.node, this.catalog);
40
+ const title = nodeTitle(this.node, this.catalog, this.model);
34
41
  const button = this.shadowRoot.querySelector("button");
35
42
  button.setAttribute("aria-pressed", String(this.selected));
36
43
  button.classList.toggle("has-icon", Boolean(kind?.icon));
37
- button.setAttribute("aria-label", cardLabel(this.node, this.catalog, this.invalid));
44
+ button.setAttribute("aria-label", cardLabel(this.node, this.catalog, this.invalid, this.model));
38
45
  button.dataset.invalid = String(this.invalid);
39
46
  const iconEl = this.shadowRoot.querySelector("[part=icon]");
40
47
  iconEl.replaceChildren(kind?.icon?.() ?? "");
41
48
  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 || "" })) {
49
+ iconEl.dataset.tone = kind?.tone ?? "accent";
50
+ for (const [part, text] of Object.entries({ title, kind: kind?.label !== title ? kind?.label ?? "" : "", description: nodeDescription(this.node, this.catalog, this.model), status: this.model.status?.(this.node) || "" })) {
43
51
  const element = this.shadowRoot.querySelector(`[part=${part}]`);
44
52
  element.textContent = text;
45
53
  element.hidden = !text;
@@ -59,7 +67,7 @@ export class KindPicker extends BaseElement {
59
67
  static tagName = "box-kind-picker";
60
68
  catalog = [];
61
69
  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>`; }
70
+ 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)} button.has-icon{display:grid;grid-template-columns:20px minmax(0,1fr);column-gap:10px;align-items:start} [part=choice-icon]{grid-row:1/span 2;width:20px;height:20px;color:var(--boe-token-surface-surface-brand,#0061d5)} [part=choice-icon][data-tone=neutral]{color:var(--boe-token-text-text-secondary,#666)} [part=choice-icon] svg{display:block;width:20px;height:20px} [part=choice-label]{display:block} 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
71
  setupListeners() {
64
72
  this.shadowRoot.addEventListener("keydown", event => {
65
73
  const e = event;
@@ -96,17 +104,32 @@ export class KindPicker extends BaseElement {
96
104
  const heading = document.createElement("h3");
97
105
  heading.textContent = kind.group;
98
106
  heading.setAttribute("role", "presentation");
107
+ heading.setAttribute("part", "group-heading");
99
108
  choices.append(heading);
100
109
  group = kind.group;
101
110
  }
102
111
  const button = document.createElement("button");
103
112
  button.type = "button";
104
- button.textContent = kind.label;
113
+ button.setAttribute("part", "choice");
114
+ if (kind.icon) {
115
+ const icon = document.createElement("span");
116
+ icon.setAttribute("part", "choice-icon");
117
+ icon.setAttribute("aria-hidden", "true");
118
+ icon.dataset.tone = kind.tone ?? "accent";
119
+ icon.append(kind.icon());
120
+ button.classList.add("has-icon");
121
+ button.append(icon);
122
+ }
123
+ const label = document.createElement("span");
124
+ label.setAttribute("part", "choice-label");
125
+ label.textContent = kind.label;
126
+ button.append(label);
105
127
  if (this.variant === "menu") {
106
128
  button.setAttribute("role", "menuitem");
107
129
  button.tabIndex = index === 0 ? 0 : -1;
108
130
  }
109
131
  const description = document.createElement("small");
132
+ description.setAttribute("part", "choice-description");
110
133
  description.textContent = kind.description;
111
134
  button.append(description);
112
135
  button.addEventListener("click", () => emit(this, "kind-pick", { kind }));
@@ -120,14 +143,17 @@ export class FlowSpine extends BaseElement {
120
143
  static tagName = "box-flow-spine";
121
144
  nodes = [];
122
145
  catalog = [];
146
+ model = defaultFlowModel;
123
147
  selected = null;
124
148
  invalid = null;
125
149
  startLabel = "Flow starts";
126
150
  endLabel = "Flow ends";
127
151
  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>`; }
152
+ 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} [part=branch-header]{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:1em 0} h3{font:inherit;font-weight:600;margin:0} [part=branch-remove],[part=branch-add]{border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:16px;background:var(--boe-token-surface-surface,#fff);padding:4px 10px} [part=branch-add]{justify-self:center;margin:4px 0}</style><div part="spine"></div>`; }
129
153
  refresh() { if (this.isRendered)
130
154
  this.update(); }
155
+ /** Re-render one card after its fields change, without rebuilding the spine. */
156
+ refreshNode(node) { this.cards.get(node)?.refresh(); }
131
157
  focusNode(node) { this.cards.get(node)?.focus(); }
132
158
  select(node, invalid = this.invalid) { this.selected = node; this.invalid = invalid; for (const [item, card] of this.cards) {
133
159
  card.selected = item === node;
@@ -137,11 +163,13 @@ export class FlowSpine extends BaseElement {
137
163
  update() {
138
164
  const root = this.shadowRoot.querySelector("[part=spine]");
139
165
  const seen = new Set();
166
+ const title = (node) => nodeTitle(node, this.catalog, this.model);
140
167
  const listView = (list, where, ancestors) => {
141
168
  const fragment = document.createDocumentFragment();
142
169
  for (let index = 0; index <= list.length; index++) {
143
170
  const point = document.createElement(InsertPoint.tagName);
144
- point.detail = { list, index, label: insertLabel(list, index, where, this.catalog) };
171
+ point.setAttribute("exportparts", "button: insert");
172
+ point.detail = { list, index, label: insertLabel(list, index, where, this.catalog, this.model) };
145
173
  fragment.append(point);
146
174
  if (index === list.length)
147
175
  break;
@@ -150,26 +178,62 @@ export class FlowSpine extends BaseElement {
150
178
  continue;
151
179
  seen.add(node);
152
180
  const card = this.cards.get(node) ?? document.createElement(FlowCard.tagName);
181
+ card.setAttribute("exportparts", FLOW_CARD_PARTS.join(", "));
153
182
  this.cards.set(node, card);
154
183
  card.node = node;
155
184
  card.catalog = this.catalog;
185
+ card.model = this.model;
156
186
  card.selected = node === this.selected;
157
187
  card.invalid = node === this.invalid;
158
188
  card.refresh();
159
189
  fragment.append(card);
160
190
  const nextAncestors = new Set([...ancestors, node]);
161
- if (node.body) {
191
+ const children = this.model.children(node);
192
+ for (const child of children.filter(child => child.label === undefined)) {
162
193
  const body = document.createElement("section");
163
194
  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));
195
+ body.setAttribute("aria-label", `${title(node)} steps`);
196
+ body.append(listView(child.list, `inside ${title(node)}`, nextAncestors));
166
197
  fragment.append(body);
167
198
  }
168
- if (node.branches) {
199
+ const branchLists = children.filter(child => child.label !== undefined);
200
+ const addLabel = this.model.addBranchLabel?.(node);
201
+ if (branchLists.length || addLabel) {
169
202
  const branches = document.createElement("div");
170
203
  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); });
204
+ branchLists.forEach((branch, branchIndex) => {
205
+ const label = branch.label;
206
+ const column = document.createElement("section");
207
+ column.setAttribute("part", "branch");
208
+ column.setAttribute("aria-label", label);
209
+ const header = document.createElement("div");
210
+ header.setAttribute("part", "branch-header");
211
+ const heading = document.createElement("h3");
212
+ heading.setAttribute("part", "branch-label");
213
+ heading.textContent = label;
214
+ header.append(heading);
215
+ if (branch.removable) {
216
+ const remove = document.createElement("button");
217
+ remove.type = "button";
218
+ remove.setAttribute("part", "branch-remove");
219
+ remove.textContent = "Remove";
220
+ remove.setAttribute("aria-label", `Remove ${label} from ${title(node)}`);
221
+ remove.addEventListener("click", () => emit(this, "branch-remove-request", { node, index: branchIndex, list: branch.list }));
222
+ header.append(remove);
223
+ }
224
+ column.append(header, listView(branch.list, `in ${label}`, nextAncestors));
225
+ branches.append(column);
226
+ });
172
227
  fragment.append(branches);
228
+ if (addLabel) {
229
+ const add = document.createElement("button");
230
+ add.type = "button";
231
+ add.setAttribute("part", "branch-add");
232
+ add.textContent = addLabel;
233
+ add.setAttribute("aria-label", `${addLabel} to ${title(node)}`);
234
+ add.addEventListener("click", () => emit(this, "branch-add-request", { node }));
235
+ fragment.append(add);
236
+ }
173
237
  }
174
238
  }
175
239
  return fragment;
@@ -1,6 +1,6 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
2
  import { boePanel } from "../../foundations/geometry/index.js";
3
- import { boeBrandInteractiveStyles, boeNeutralInteractiveStyles, } from "../../foundations/tokens/index.js";
3
+ import { boeBrandInteractiveStyles, boeNeutralInteractiveStyles, } from "../../foundations/tokens/interaction.js";
4
4
  const DEFAULT_TAG_NAME = "box-governance-panel";
5
5
  const escapeHtml = (value) => value
6
6
  .replaceAll("&", "&amp;")
@@ -1,6 +1,6 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
2
  import { boePanel } from "../../foundations/geometry/index.js";
3
- import { boeBrandInteractiveStyles, boeNeutralInteractiveStyles, } from "../../foundations/tokens/index.js";
3
+ import { boeBrandInteractiveStyles, boeNeutralInteractiveStyles, } from "../../foundations/tokens/interaction.js";
4
4
  const DEFAULT_TAG_NAME = "box-chart-panel";
5
5
  const escapeHtml = (value) => value
6
6
  .replaceAll("&", "&amp;")
@@ -1,6 +1,6 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
2
  import { boePanel } from "../../foundations/geometry/index.js";
3
- import { boeBrandInteractiveStyles, boeNeutralInteractiveStyles, } from "../../foundations/tokens/index.js";
3
+ import { boeBrandInteractiveStyles, boeNeutralInteractiveStyles, } from "../../foundations/tokens/interaction.js";
4
4
  const DEFAULT_TAG_NAME = "box-metric-card";
5
5
  const escapeHtml = (value) => value
6
6
  .replaceAll("&", "&amp;")