@unofficialbox/box-open-elements 0.26.0 → 0.28.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 (54) hide show
  1. package/README.md +13 -13
  2. package/dist/components/actions/button.d.ts +3 -0
  3. package/dist/components/actions/button.js +26 -7
  4. package/dist/components/collections/resource-row.d.ts +28 -0
  5. package/dist/components/collections/resource-row.js +75 -0
  6. package/dist/components/feedback/mode-indicator.d.ts +21 -0
  7. package/dist/components/feedback/mode-indicator.js +67 -0
  8. package/dist/components/feedback/progress-steps.d.ts +4 -0
  9. package/dist/components/feedback/progress-steps.js +34 -2
  10. package/dist/components/feedback/verdict-banner.d.ts +23 -0
  11. package/dist/components/feedback/verdict-banner.js +59 -0
  12. package/dist/components/forms/code-editor.d.ts +19 -0
  13. package/dist/components/forms/code-editor.js +102 -33
  14. package/dist/components/forms/combobox.d.ts +1 -0
  15. package/dist/components/forms/combobox.js +9 -5
  16. package/dist/components/forms/number-input.d.ts +1 -0
  17. package/dist/components/forms/number-input.js +11 -0
  18. package/dist/components/forms/text-area.d.ts +1 -0
  19. package/dist/components/forms/text-area.js +2 -0
  20. package/dist/components/forms/text-field.d.ts +3 -0
  21. package/dist/components/forms/text-field.js +15 -1
  22. package/dist/components/forms/tile-group.d.ts +6 -0
  23. package/dist/components/forms/tile-group.js +29 -2
  24. package/dist/components/layout/app-shell.js +13 -3
  25. package/dist/components/layout/nav-sidebar.js +14 -2
  26. package/dist/components/navigation/accordion.d.ts +3 -0
  27. package/dist/components/navigation/accordion.js +21 -5
  28. package/dist/components/output/code-block.d.ts +3 -0
  29. package/dist/components/output/code-block.js +12 -2
  30. package/dist/components/overlays/command-palette.js +4 -0
  31. package/dist/components/overlays/dialog.d.ts +6 -0
  32. package/dist/components/overlays/dialog.js +18 -2
  33. package/dist/components/overlays/drawer.d.ts +3 -0
  34. package/dist/components/overlays/drawer.js +11 -3
  35. package/dist/core/index.d.ts +1 -1
  36. package/dist/core/index.js +1 -1
  37. package/dist/entries/mode-indicator.d.ts +1 -0
  38. package/dist/entries/mode-indicator.js +1 -0
  39. package/dist/entries/resource-row.d.ts +1 -0
  40. package/dist/entries/resource-row.js +1 -0
  41. package/dist/entries/verdict-banner.d.ts +1 -0
  42. package/dist/entries/verdict-banner.js +1 -0
  43. package/dist/index.d.ts +3 -0
  44. package/dist/index.js +3 -0
  45. package/dist/patterns/call-console/call-console.js +1 -1
  46. package/dist/patterns/flow-builder/flow-builder.d.ts +8 -0
  47. package/dist/patterns/flow-builder/flow-builder.js +34 -0
  48. package/dist/patterns/flow-builder/primitives.d.ts +14 -2
  49. package/dist/patterns/flow-builder/primitives.js +28 -5
  50. package/dist/patterns/form-wizard/form-wizard.js +18 -3
  51. package/dist/patterns/form-wizard/wizard-summary.js +11 -2
  52. package/dist/patterns/process-modeler/design.d.ts +1 -1
  53. package/dist/patterns/process-modeler/design.js +1 -1
  54. package/package.json +1 -1
@@ -40,6 +40,9 @@ export declare class CodeBlock extends BaseElement {
40
40
  /** Accessible name for the copy button. */
41
41
  get copyLabel(): string;
42
42
  set copyLabel(value: string);
43
+ /** Accessible name for the scrollable code region. */
44
+ get label(): string;
45
+ set label(value: string);
43
46
  private copy;
44
47
  private markCopied;
45
48
  disconnectedCallback(): void;
@@ -116,7 +116,7 @@ const codeStyles = `
116
116
  export class CodeBlock extends BaseElement {
117
117
  static tagName = DEFAULT_TAG_NAME;
118
118
  static get observedAttributes() {
119
- return ["code", "copy-label", "language"];
119
+ return ["code", "copy-label", "language", "label"];
120
120
  }
121
121
  preEl;
122
122
  codeEl;
@@ -147,6 +147,16 @@ export class CodeBlock extends BaseElement {
147
147
  set copyLabel(value) {
148
148
  this.setAttribute("copy-label", value);
149
149
  }
150
+ /** Accessible name for the scrollable code region. */
151
+ get label() {
152
+ return this.getAttribute("label") ?? "";
153
+ }
154
+ set label(value) {
155
+ if (value)
156
+ this.setAttribute("label", value);
157
+ else
158
+ this.removeAttribute("label");
159
+ }
150
160
  async copy() {
151
161
  const text = this.code || this.textContent?.trim() || "";
152
162
  if (!text) {
@@ -222,7 +232,7 @@ export class CodeBlock extends BaseElement {
222
232
  this.slotEl.textContent = "";
223
233
  }
224
234
  const language = this.language;
225
- this.preEl.setAttribute("aria-label", language ? `${language} code` : "Source code");
235
+ this.preEl.setAttribute("aria-label", this.label || (language ? `${language} code` : "Source code"));
226
236
  if (language) {
227
237
  this.codeEl.setAttribute("data-language", language);
228
238
  this.codeEl.setAttribute("class", `language-${language}`);
@@ -170,6 +170,10 @@ const elementStyles = `
170
170
  white-space: nowrap;
171
171
  }
172
172
 
173
+ [part="option"][aria-selected="true"] [part="shortcut"] {
174
+ color: var(--boe-token-text-text, #1f1e1b);
175
+ }
176
+
173
177
  [part="empty"] {
174
178
  padding: 1.4rem 0.9rem;
175
179
  text-align: center;
@@ -20,6 +20,12 @@ export declare class Dialog extends BaseElement {
20
20
  set description(value: string);
21
21
  get confirmLabel(): string;
22
22
  set confirmLabel(value: string);
23
+ get confirmDisabled(): boolean;
24
+ set confirmDisabled(value: boolean);
25
+ get confirmBusy(): boolean;
26
+ set confirmBusy(value: boolean);
27
+ get confirmBusyLabel(): string;
28
+ set confirmBusyLabel(value: string);
23
29
  /** Modal size — box-ui-elements Modal sizes. Default `medium`. */
24
30
  get size(): "small" | "medium" | "large" | "fullscreen";
25
31
  set size(value: "small" | "medium" | "large" | "fullscreen");
@@ -144,7 +144,7 @@ const dialogStyles = `
144
144
  export class Dialog extends BaseElement {
145
145
  static tagName = DEFAULT_TAG_NAME;
146
146
  static get observedAttributes() {
147
- return ["confirm-label", "description", "heading", "open", "size"];
147
+ return ["confirm-busy", "confirm-busy-label", "confirm-disabled", "confirm-label", "description", "heading", "open", "size"];
148
148
  }
149
149
  openValue = false;
150
150
  wasOpen = false;
@@ -191,6 +191,17 @@ export class Dialog extends BaseElement {
191
191
  set confirmLabel(value) {
192
192
  this.setAttribute("confirm-label", value);
193
193
  }
194
+ get confirmDisabled() { return this.hasAttribute("confirm-disabled"); }
195
+ set confirmDisabled(value) { this.toggleAttribute("confirm-disabled", Boolean(value)); }
196
+ get confirmBusy() { return this.hasAttribute("confirm-busy"); }
197
+ set confirmBusy(value) { this.toggleAttribute("confirm-busy", Boolean(value)); }
198
+ get confirmBusyLabel() { return this.getAttribute("confirm-busy-label") ?? ""; }
199
+ set confirmBusyLabel(value) {
200
+ if (value)
201
+ this.setAttribute("confirm-busy-label", value);
202
+ else
203
+ this.removeAttribute("confirm-busy-label");
204
+ }
194
205
  /** Modal size — box-ui-elements Modal sizes. Default `medium`. */
195
206
  get size() {
196
207
  const size = this.getAttribute("size");
@@ -252,6 +263,8 @@ export class Dialog extends BaseElement {
252
263
  return;
253
264
  }
254
265
  if (target.closest('[part="confirm"]')) {
266
+ if (this.confirmDisabled || this.confirmBusy)
267
+ return;
255
268
  this.dispatchEvent(new CustomEvent("confirm", { bubbles: true, composed: true }));
256
269
  this.close();
257
270
  }
@@ -348,7 +361,10 @@ export class Dialog extends BaseElement {
348
361
  this.descriptionEl.hidden = !description;
349
362
  }
350
363
  if (this.confirmEl) {
351
- this.confirmEl.textContent = this.confirmLabel;
364
+ this.confirmEl.textContent = this.confirmBusy && this.confirmBusyLabel
365
+ ? this.confirmBusyLabel : this.confirmLabel;
366
+ this.confirmEl.disabled = this.confirmDisabled || this.confirmBusy;
367
+ this.confirmEl.setAttribute("aria-busy", String(this.confirmBusy));
352
368
  }
353
369
  if (justOpened) {
354
370
  queueMicrotask(() => {
@@ -26,6 +26,9 @@ export declare class Drawer extends BaseElement {
26
26
  /** Busy: a saving/loading veil over the body; Close stays reachable. */
27
27
  get busy(): boolean;
28
28
  set busy(value: boolean);
29
+ /** Hide only the built-in header control; Escape and backdrop still request dismissal. */
30
+ get hideCloseButton(): boolean;
31
+ set hideCloseButton(value: boolean);
29
32
  get heading(): string;
30
33
  set heading(value: string);
31
34
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
@@ -149,6 +149,8 @@ const drawerStyles = `
149
149
  box-shadow: none;
150
150
  }
151
151
 
152
+ [part="close"][hidden] { display: none; }
153
+
152
154
  ${boeNeutralInteractiveStyles('[part="close"]')}
153
155
 
154
156
  [part="body"] {
@@ -201,7 +203,7 @@ const DRAWER_SIZES = new Set(["small", "medium", "large", "full"]);
201
203
  export class Drawer extends BaseElement {
202
204
  static tagName = DEFAULT_TAG_NAME;
203
205
  static get observedAttributes() {
204
- return ["busy", "description", "heading", "open", "position", "size"];
206
+ return ["busy", "description", "heading", "hide-close-button", "open", "position", "size"];
205
207
  }
206
208
  openValue = false;
207
209
  wasOpen = false;
@@ -255,6 +257,9 @@ export class Drawer extends BaseElement {
255
257
  set busy(value) {
256
258
  this.toggleAttribute("busy", Boolean(value));
257
259
  }
260
+ /** Hide only the built-in header control; Escape and backdrop still request dismissal. */
261
+ get hideCloseButton() { return this.hasAttribute("hide-close-button"); }
262
+ set hideCloseButton(value) { this.toggleAttribute("hide-close-button", Boolean(value)); }
258
263
  get heading() {
259
264
  return this.getAttribute("heading") ?? "Drawer";
260
265
  }
@@ -381,7 +386,7 @@ export class Drawer extends BaseElement {
381
386
  this.hostEl.innerHTML = `
382
387
  <style>${drawerStyles}</style>
383
388
  <dialog part="backdrop" aria-labelledby="drawer-title">
384
- <div part="drawer">
389
+ <div part="drawer" tabindex="-1">
385
390
  <header part="header">
386
391
  <div part="meta">
387
392
  <h2 id="drawer-title"></h2>
@@ -421,6 +426,9 @@ export class Drawer extends BaseElement {
421
426
  this.drawerEl = this.hostEl.querySelector('[part="drawer"]');
422
427
  this.titleEl = this.hostEl.querySelector("#drawer-title");
423
428
  this.descriptionEl = this.hostEl.querySelector('[part="description"]');
429
+ const closeButton = this.hostEl.querySelector('[part="close"]');
430
+ if (closeButton)
431
+ closeButton.hidden = this.hideCloseButton;
424
432
  const isLeft = this.position === "left";
425
433
  const isBottom = this.position === "bottom";
426
434
  if (this.backdropEl) {
@@ -456,7 +464,7 @@ export class Drawer extends BaseElement {
456
464
  }
457
465
  if (justOpened) {
458
466
  queueMicrotask(() => {
459
- this.hostEl.querySelector('[part="close"]')?.focus();
467
+ (this.hideCloseButton ? this.drawerEl : closeButton)?.focus();
460
468
  });
461
469
  }
462
470
  }
@@ -2,7 +2,7 @@ export { Controller } from "./controller.js";
2
2
  export { TypedEventEmitter } from "./event-emitter.js";
3
3
  export type { EventMap, Unsubscribe } from "./event-emitter.js";
4
4
  export { BaseElement } from "./element.js";
5
- export { FORM_DESCRIPTION_ID, FORM_ERROR_MESSAGE_ID, FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDataFromNamedValues, formDataFromRange, formDescriptionMarkup, formErrorMessageMarkup, getMirroredFormValue, rangeFromFormValue, stringValuesFromFormValue, } from "./form-associated.js";
5
+ export { FORM_DESCRIPTION_ID, FORM_ERROR_MESSAGE_ID, FORM_LABEL_ID, FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDataFromNamedValues, formDataFromRange, formDescriptionMarkup, formErrorMessageMarkup, getMirroredFormValue, rangeFromFormValue, stringValuesFromFormValue, } from "./form-associated.js";
6
6
  export type { FormValue } from "./form-associated.js";
7
7
  export { resolveRowWindow, sameRowWindow } from "./virtualize.js";
8
8
  export type { RowWindow, WindowMetrics } from "./virtualize.js";
@@ -1,6 +1,6 @@
1
1
  export { Controller } from "./controller.js";
2
2
  export { TypedEventEmitter } from "./event-emitter.js";
3
3
  export { BaseElement } from "./element.js";
4
- export { FORM_DESCRIPTION_ID, FORM_ERROR_MESSAGE_ID, FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDataFromNamedValues, formDataFromRange, formDescriptionMarkup, formErrorMessageMarkup, getMirroredFormValue, rangeFromFormValue, stringValuesFromFormValue, } from "./form-associated.js";
4
+ export { FORM_DESCRIPTION_ID, FORM_ERROR_MESSAGE_ID, FORM_LABEL_ID, FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDataFromNamedValues, formDataFromRange, formDescriptionMarkup, formErrorMessageMarkup, getMirroredFormValue, rangeFromFormValue, stringValuesFromFormValue, } from "./form-associated.js";
5
5
  export { resolveRowWindow, sameRowWindow } from "./virtualize.js";
6
6
  export { buildOffsetIndex, offsetOfItem, resolveOffsetWindow } from "./offset-window.js";
@@ -0,0 +1 @@
1
+ export * from "../components/feedback/mode-indicator.js";
@@ -0,0 +1 @@
1
+ export * from "../components/feedback/mode-indicator.js";
@@ -0,0 +1 @@
1
+ export * from "../components/collections/resource-row.js";
@@ -0,0 +1 @@
1
+ export * from "../components/collections/resource-row.js";
@@ -0,0 +1 @@
1
+ export * from "../components/feedback/verdict-banner.js";
@@ -0,0 +1 @@
1
+ export * from "../components/feedback/verdict-banner.js";
package/dist/index.d.ts CHANGED
@@ -24,6 +24,7 @@ export * from "./components/actions/toolbar.js";
24
24
  export * from "./components/collections/card.js";
25
25
  export * from "./components/collections/carousel.js";
26
26
  export * from "./components/collections/datalist-item.js";
27
+ export * from "./components/collections/resource-row.js";
27
28
  export * from "./components/collections/draggable-list.js";
28
29
  export * from "./components/collections/grid-view.js";
29
30
  export * from "./components/collections/pagination.js";
@@ -41,6 +42,7 @@ export * from "./components/feedback/empty-state.js";
41
42
  export * from "./components/feedback/error-mask.js";
42
43
  export * from "./components/feedback/help-text.js";
43
44
  export * from "./components/feedback/indicator.js";
45
+ export * from "./components/feedback/mode-indicator.js";
44
46
  export * from "./components/feedback/nudge.js";
45
47
  export * from "./components/feedback/progress-bar.js";
46
48
  export * from "./components/feedback/progress-ring.js";
@@ -49,6 +51,7 @@ export * from "./components/feedback/skeleton.js";
49
51
  export * from "./components/feedback/spinner.js";
50
52
  export * from "./components/feedback/path.js";
51
53
  export * from "./components/feedback/toast.js";
54
+ export * from "./components/feedback/verdict-banner.js";
52
55
  export * from "./components/files/drop-zone.js";
53
56
  export * from "./components/identity/avatar.js";
54
57
  export * from "./components/identity/contact-datalist-item.js";
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ export * from "./components/actions/toolbar.js";
27
27
  export * from "./components/collections/card.js";
28
28
  export * from "./components/collections/carousel.js";
29
29
  export * from "./components/collections/datalist-item.js";
30
+ export * from "./components/collections/resource-row.js";
30
31
  export * from "./components/collections/draggable-list.js";
31
32
  export * from "./components/collections/grid-view.js";
32
33
  export * from "./components/collections/pagination.js";
@@ -45,6 +46,7 @@ export * from "./components/feedback/empty-state.js";
45
46
  export * from "./components/feedback/error-mask.js";
46
47
  export * from "./components/feedback/help-text.js";
47
48
  export * from "./components/feedback/indicator.js";
49
+ export * from "./components/feedback/mode-indicator.js";
48
50
  export * from "./components/feedback/nudge.js";
49
51
  export * from "./components/feedback/progress-bar.js";
50
52
  export * from "./components/feedback/progress-ring.js";
@@ -53,6 +55,7 @@ export * from "./components/feedback/skeleton.js";
53
55
  export * from "./components/feedback/spinner.js";
54
56
  export * from "./components/feedback/path.js";
55
57
  export * from "./components/feedback/toast.js";
58
+ export * from "./components/feedback/verdict-banner.js";
56
59
  // Components: files
57
60
  export * from "./components/files/drop-zone.js";
58
61
  // Components: identity
@@ -86,7 +86,7 @@ export class CallConsole extends BaseElement {
86
86
  <section part="console" aria-label="API call console">
87
87
  <header><div part="heading"><h2></h2><small>Inspect requests, responses, and tool outcomes</small></div><span id="connection" role="status"></span><box-button id="retry" label="Reconnect" tone="neutral" hidden></box-button></header>
88
88
  <div class="filters"><label class="search">Search <input id="search" type="search" placeholder="Method, URL, or summary"></label><label>Service <select id="service" aria-label="Service"><option value="">All services</option></select></label><label><input type="checkbox" id="errors"> Errors only</label><box-button id="clear" label="Clear calls" tone="neutral"></box-button></div>
89
- <box-split-view resizable ratio="0.42" label="Calls and request inspector"><section slot="primary" aria-label="Calls"><div class="pane-heading"><h3>Requests</h3><span id="count" class="muted" role="status"></span></div><div id="list"></div><p id="list-empty" role="status" hidden></p></section><section aria-label="Request inspector"><div class="pane-heading"><h3>Inspector</h3><box-button data-copy="both" label="Copy both" tone="neutral"></box-button><span id="copy-status" role="status"></span></div><p id="detail-empty">Select a request to inspect its details.</p><div id="inspector" hidden><h3 id="summary"></h3><p id="metadata" class="muted"></p><p id="outcome" role="status" hidden></p><h3 class="code-heading">Request</h3><box-code-block id="request" copy-label="Copy request" wrap></box-code-block><h3 class="code-heading">Response</h3><box-code-block id="response" copy-label="Copy response" wrap></box-code-block></div></section></box-split-view></section>`;
89
+ <box-split-view resizable ratio="0.42" label="Calls and request inspector"><section slot="primary" aria-label="Calls"><div class="pane-heading"><h3>Requests</h3><span id="count" class="muted" role="status"></span></div><div id="list"></div><p id="list-empty" role="status" hidden></p></section><section aria-label="Request inspector"><div class="pane-heading"><h3>Inspector</h3><box-button data-copy="both" label="Copy both" tone="neutral"></box-button><span id="copy-status" role="status"></span></div><p id="detail-empty">Select a request to inspect its details.</p><div id="inspector" hidden><h3 id="summary"></h3><p id="metadata" class="muted"></p><p id="outcome" role="status" hidden></p><h3 class="code-heading">Request</h3><box-code-block id="request" label="Request" copy-label="Copy request" wrap></box-code-block><h3 class="code-heading">Response</h3><box-code-block id="response" label="Response" copy-label="Copy response" wrap></box-code-block></div></section></box-split-view></section>`;
90
90
  }
91
91
  setupListeners() {
92
92
  this.shadowRoot.querySelector("#list").addEventListener("keydown", event => {
@@ -9,6 +9,10 @@ export interface ValidationOptions {
9
9
  */
10
10
  showMessage?: boolean;
11
11
  }
12
+ export interface FlowNodeFigure {
13
+ text: string;
14
+ tone?: "neutral" | "warning" | "error";
15
+ }
12
16
  /**
13
17
  * Host-owned document editing with accessible navigation and responsive inspection.
14
18
  * `N` is the host's node type; it defaults to {@link FlowNode}, which the default
@@ -17,6 +21,7 @@ export interface ValidationOptions {
17
21
  export declare class FlowBuilder<N extends FlowNodeBase = FlowNode> extends BaseElement {
18
22
  static readonly tagName = "box-flow-builder";
19
23
  private nodesValue;
24
+ private figuresValue;
20
25
  private modelValue;
21
26
  private catalogValue;
22
27
  private selectedValue;
@@ -32,6 +37,9 @@ export declare class FlowBuilder<N extends FlowNodeBase = FlowNode> extends Base
32
37
  get narrow(): boolean;
33
38
  /** Focus a rendered card without changing selection or opening the editor. */
34
39
  focusNode(node: N): void;
40
+ /** Update one live card figure without rebuilding the flow or moving focus. */
41
+ setNodeFigure(target: N | string | NodePath, figure: FlowNodeFigure | null): void;
42
+ private findNodeById;
35
43
  private insertion?;
36
44
  private invoker?;
37
45
  private spine;
@@ -12,6 +12,7 @@ import { FLOW_SPINE_PARTS, InsertPoint, KIND_PICKER_PARTS } from "./primitives.j
12
12
  export class FlowBuilder extends BaseElement {
13
13
  static tagName = "box-flow-builder";
14
14
  nodesValue = [];
15
+ figuresValue = new Map();
15
16
  modelValue = defaultFlowModel;
16
17
  catalogValue = [];
17
18
  selectedValue = null;
@@ -27,6 +28,38 @@ export class FlowBuilder extends BaseElement {
27
28
  get narrow() { return this.narrowValue; }
28
29
  /** Focus a rendered card without changing selection or opening the editor. */
29
30
  focusNode(node) { this.spine?.focusNode(node); }
31
+ /** Update one live card figure without rebuilding the flow or moving focus. */
32
+ setNodeFigure(target, figure) {
33
+ const node = typeof target === "string" ? this.findNodeById(target)
34
+ : Array.isArray(target) ? nodeAtPath({ body: this.nodes }, target)
35
+ : target;
36
+ if (!node)
37
+ return;
38
+ if (figure)
39
+ this.figuresValue.set(node, figure);
40
+ else
41
+ this.figuresValue.delete(node);
42
+ this.spine?.setNodeFigure(node, figure);
43
+ }
44
+ findNodeById(id) {
45
+ const seen = new Set();
46
+ const visit = (nodes) => {
47
+ for (const node of nodes) {
48
+ if (seen.has(node))
49
+ continue;
50
+ seen.add(node);
51
+ if (node.id === id)
52
+ return node;
53
+ for (const child of this.model.children(node)) {
54
+ const found = visit(child.list);
55
+ if (found)
56
+ return found;
57
+ }
58
+ }
59
+ return null;
60
+ };
61
+ return visit(this.nodes);
62
+ }
30
63
  insertion;
31
64
  invoker;
32
65
  spine;
@@ -106,6 +139,7 @@ export class FlowBuilder extends BaseElement {
106
139
  [hidden]{display:none!important} [part=inspector-heading]{font:inherit;font-weight:650;margin:0 0 12px}
107
140
  </style><div part="error" hidden></div><div part="layout"><box-flow-spine exportparts="${FLOW_SPINE_PARTS.join(", ")}"></box-flow-spine><div part="inspector" role="region" aria-label="Add a step"><box-kind-picker exportparts="${KIND_PICKER_PARTS.join(", ")}"></box-kind-picker><div part="editor"></div></div></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>`;
108
141
  this.spine = this.shadowRoot.querySelector("box-flow-spine");
142
+ this.spine.figures = this.figuresValue;
109
143
  this.aside = this.shadowRoot.querySelector("[part=inspector]");
110
144
  this.palette = this.aside.querySelector("box-kind-picker");
111
145
  this.inspector = this.aside.querySelector("[part=editor]");
@@ -1,9 +1,9 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
2
  import { type FlowInsertDetail, type FlowKind, type FlowModel, type FlowNodeBase } from "./model.js";
3
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"];
4
+ export declare const FLOW_CARD_PARTS: readonly ["card", "icon", "title", "kind", "description", "status", "figure", "error"];
5
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"];
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", "figure", "error"];
7
7
  /** Parts a kind picker exposes. */
8
8
  export declare const KIND_PICKER_PARTS: readonly ["choices", "group-heading", "choice", "choice-icon", "choice-label", "choice-description"];
9
9
  export declare class FlowCard extends BaseElement {
@@ -13,6 +13,10 @@ export declare class FlowCard extends BaseElement {
13
13
  model: FlowModel<FlowNodeBase>;
14
14
  selected: boolean;
15
15
  invalid: boolean;
16
+ figure: {
17
+ text: string;
18
+ tone?: "neutral" | "warning" | "error";
19
+ } | null;
16
20
  protected renderTemplate(): void;
17
21
  protected setupListeners(): void;
18
22
  refresh(): void;
@@ -49,10 +53,18 @@ export declare class FlowSpine extends BaseElement {
49
53
  endLabel: string;
50
54
  headingLevel: number;
51
55
  private cards;
56
+ figures: Map<FlowNodeBase, {
57
+ text: string;
58
+ tone?: "neutral" | "warning" | "error";
59
+ }>;
52
60
  protected renderTemplate(): void;
53
61
  refresh(): void;
54
62
  /** Re-render one card after its fields change, without rebuilding the spine. */
55
63
  refreshNode(node: FlowNodeBase): void;
64
+ setNodeFigure(node: FlowNodeBase, figure: {
65
+ text: string;
66
+ tone?: "neutral" | "warning" | "error";
67
+ } | null): void;
56
68
  focusNode(node: FlowNodeBase): void;
57
69
  select(node: FlowNodeBase | null, invalid?: FlowNodeBase | null): void;
58
70
  protected update(): void;
@@ -9,7 +9,7 @@ const styles = `
9
9
  `;
10
10
  function emit(host, name, detail) { host.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })); }
11
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"];
12
+ export const FLOW_CARD_PARTS = ["card", "icon", "title", "kind", "description", "status", "figure", "error"];
13
13
  /** Parts the spine exposes (its own plus forwarded card and insert-point parts). */
14
14
  export const FLOW_SPINE_PARTS = ["spine", "endpoint", "body", "branches", "branch", "branch-header", "branch-label", "branch-remove", "branch-add", "insert", ...FLOW_CARD_PARTS];
15
15
  /** Parts a kind picker exposes. */
@@ -21,15 +21,19 @@ export class FlowCard extends BaseElement {
21
21
  model = defaultFlowModel;
22
22
  selected = false;
23
23
  invalid = false;
24
+ figure = null;
24
25
  renderTemplate() {
25
26
  this.shadowRoot.innerHTML = `<style>${styles}
26
27
  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; }
27
28
  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) 5%,var(--boe-token-surface-surface,#fff)); }
28
29
  button[data-invalid=true] { border-color:var(--boe-token-text-status-text-error,#b92340); }
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}
30
+ 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 5} button.has-icon [part=title],button.has-icon [part=kind],button.has-icon [part=description],button.has-icon [part=status],button.has-icon [part=figure],button.has-icon [part=error]{grid-column:2}
30
31
  [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); }
32
+ [part=figure]{font-size:.78rem;font-variant-numeric:tabular-nums;color:var(--boe-token-text-text-secondary,#666);min-width:0;overflow-wrap:anywhere}
33
+ [part=figure][data-tone=warning]{color:var(--boe-token-text-status-text-warning,#805600)}
34
+ [part=figure][data-tone=error]{color:var(--boe-token-text-status-text-error,#b92340)}
31
35
  [hidden] { display:none!important; }
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>`;
36
+ </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="figure" hidden></span><span part="error">Needs attention</span></button>`;
33
37
  }
34
38
  setupListeners() { this.shadowRoot.querySelector("button").addEventListener("click", () => emit(this, "node-select", { node: this.node })); }
35
39
  refresh() { if (this.isRendered)
@@ -41,7 +45,7 @@ export class FlowCard extends BaseElement {
41
45
  const button = this.shadowRoot.querySelector("button");
42
46
  button.setAttribute("aria-pressed", String(this.selected));
43
47
  button.classList.toggle("has-icon", Boolean(kind?.icon));
44
- button.setAttribute("aria-label", cardLabel(this.node, this.catalog, this.invalid, this.model));
48
+ button.setAttribute("aria-label", [cardLabel(this.node, this.catalog, this.invalid, this.model), this.figure?.text].filter(Boolean).join(". "));
45
49
  button.dataset.invalid = String(this.invalid);
46
50
  const iconEl = this.shadowRoot.querySelector("[part=icon]");
47
51
  iconEl.replaceChildren(kind?.icon?.() ?? "");
@@ -53,6 +57,10 @@ export class FlowCard extends BaseElement {
53
57
  element.hidden = !text;
54
58
  }
55
59
  this.shadowRoot.querySelector("[part=error]").hidden = !this.invalid;
60
+ const figureEl = this.shadowRoot.querySelector("[part=figure]");
61
+ figureEl.textContent = this.figure?.text ?? "";
62
+ figureEl.hidden = !this.figure?.text;
63
+ figureEl.dataset.tone = this.figure?.tone ?? "neutral";
56
64
  }
57
65
  }
58
66
  export class InsertPoint extends BaseElement {
@@ -157,11 +165,23 @@ export class FlowSpine extends BaseElement {
157
165
  endLabel = "Flow ends";
158
166
  headingLevel = 3;
159
167
  cards = new Map();
168
+ figures = new Map();
160
169
  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} [part=branch-label]{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>`; }
161
170
  refresh() { if (this.isRendered)
162
171
  this.update(); }
163
172
  /** Re-render one card after its fields change, without rebuilding the spine. */
164
173
  refreshNode(node) { this.cards.get(node)?.refresh(); }
174
+ setNodeFigure(node, figure) {
175
+ if (figure)
176
+ this.figures.set(node, figure);
177
+ else
178
+ this.figures.delete(node);
179
+ const card = this.cards.get(node);
180
+ if (card) {
181
+ card.figure = figure;
182
+ card.refresh();
183
+ }
184
+ }
165
185
  focusNode(node) { this.cards.get(node)?.focus(); }
166
186
  select(node, invalid = this.invalid) { this.selected = node; this.invalid = invalid; for (const [item, card] of this.cards) {
167
187
  card.selected = item === node;
@@ -193,6 +213,7 @@ export class FlowSpine extends BaseElement {
193
213
  card.model = this.model;
194
214
  card.selected = node === this.selected;
195
215
  card.invalid = node === this.invalid;
216
+ card.figure = this.figures.get(node) ?? null;
196
217
  card.refresh();
197
218
  fragment.append(card);
198
219
  const nextAncestors = new Set([...ancestors, node]);
@@ -254,8 +275,10 @@ export class FlowSpine extends BaseElement {
254
275
  end.textContent = this.endLabel;
255
276
  root.replaceChildren(start, listView(this.nodes, "in the flow", new Set()), end);
256
277
  for (const node of this.cards.keys())
257
- if (!seen.has(node))
278
+ if (!seen.has(node)) {
258
279
  this.cards.delete(node);
280
+ this.figures.delete(node);
281
+ }
259
282
  }
260
283
  }
261
284
  FlowCard.register();
@@ -77,7 +77,7 @@ const elementStyles = `
77
77
  font-size: 0.9rem;
78
78
  background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 10%, var(--boe-token-surface-surface, #ffffff));
79
79
  border: 1px solid color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 34%, transparent);
80
- color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 72%, black 28%);
80
+ color: var(--boe-token-text-status-text-error, #b92340);
81
81
  }
82
82
 
83
83
  [part="error"][hidden] {
@@ -338,6 +338,11 @@ export class FormWizard extends BaseElement {
338
338
  }));
339
339
  if (this.isRendered) {
340
340
  this.update();
341
+ if (eventName === "stepChanged") {
342
+ queueMicrotask(() => {
343
+ this.panelsEl.querySelector('[part="panel"]:not([hidden])')?.focus();
344
+ });
345
+ }
341
346
  }
342
347
  }));
343
348
  }
@@ -357,7 +362,7 @@ export class FormWizard extends BaseElement {
357
362
  this.shadowRoot.innerHTML = `
358
363
  <style>${elementStyles}</style>
359
364
  <section part="wizard" aria-label="Form wizard">
360
- <h2 part="title" hidden></h2>
365
+ <h2 id="wizard-title" part="title" hidden></h2>
361
366
  <div part="layout">
362
367
  <box-progress-steps part="rail"></box-progress-steps>
363
368
  <div part="body">
@@ -376,6 +381,7 @@ export class FormWizard extends BaseElement {
376
381
  `;
377
382
  this.titleEl = this.shadowRoot.querySelector('[part="title"]');
378
383
  this.railEl = this.shadowRoot.querySelector('[part="rail"]');
384
+ this.railEl.compact = true;
379
385
  this.panelsEl = this.shadowRoot.querySelector('[part="panels"]');
380
386
  this.errorEl = this.shadowRoot.querySelector('[part="error"]');
381
387
  this.backEl = this.shadowRoot.querySelector('[part="back"]');
@@ -418,7 +424,7 @@ export class FormWizard extends BaseElement {
418
424
  rebuildPanels(steps) {
419
425
  this.panelsEl.innerHTML = steps
420
426
  .map(step => `
421
- <div part="panel" data-step-id="${escapeHtml(step.id)}" role="group" aria-label="${escapeHtml(step.label)}" hidden>
427
+ <div part="panel" data-step-id="${escapeHtml(step.id)}" role="group" aria-label="${escapeHtml(step.label)}" tabindex="-1" hidden>
422
428
  <slot name="${escapeHtml(step.id)}"></slot>
423
429
  </div>
424
430
  `)
@@ -432,6 +438,15 @@ export class FormWizard extends BaseElement {
432
438
  const steps = state?.steps ?? [];
433
439
  this.titleEl.hidden = !this.heading;
434
440
  this.titleEl.textContent = this.heading;
441
+ const wizard = this.shadowRoot?.querySelector('[part="wizard"]');
442
+ if (this.heading) {
443
+ wizard?.setAttribute("aria-labelledby", "wizard-title");
444
+ wizard?.removeAttribute("aria-label");
445
+ }
446
+ else {
447
+ wizard?.removeAttribute("aria-labelledby");
448
+ wizard?.setAttribute("aria-label", "Form wizard");
449
+ }
435
450
  const signature = JSON.stringify(steps.map(step => step.id));
436
451
  if (signature !== this.panelsSignature) {
437
452
  this.panelsSignature = signature;
@@ -11,6 +11,7 @@ const escapeHtml = (value) => value
11
11
  const elementStyles = `
12
12
  :host {
13
13
  display: block;
14
+ container-type: inline-size;
14
15
  }
15
16
 
16
17
  /* The host's own display would otherwise beat the UA rule for [hidden],
@@ -87,7 +88,7 @@ const elementStyles = `
87
88
  margin: 0;
88
89
  padding: 0 1rem 0.8rem;
89
90
  display: grid;
90
- grid-template-columns: minmax(8rem, 14rem) 1fr;
91
+ grid-template-columns: minmax(8rem, 14rem) minmax(0, 1fr);
91
92
  gap: 0.3rem 1rem;
92
93
  }
93
94
 
@@ -99,9 +100,17 @@ const elementStyles = `
99
100
 
100
101
  [part="row-value"] {
101
102
  margin: 0;
103
+ min-inline-size: 0;
104
+ max-inline-size: 100%;
105
+ overflow-x: auto;
102
106
  font-size: 0.85rem;
103
107
  color: var(--boe-token-text-text, #1f1e1b);
104
- overflow-wrap: anywhere;
108
+ overflow-wrap: normal;
109
+ word-break: normal;
110
+ }
111
+
112
+ @container (max-width: 32rem) {
113
+ [part="rows"] { grid-template-columns: minmax(0, 1fr); }
105
114
  }
106
115
 
107
116
  [part="row-value"][data-empty="true"] {