@unofficialbox/box-open-elements 0.27.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 (42) 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 +16 -1
  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 +10 -0
  13. package/dist/components/forms/code-editor.js +54 -4
  14. package/dist/components/forms/number-input.d.ts +1 -0
  15. package/dist/components/forms/number-input.js +11 -0
  16. package/dist/components/forms/text-area.d.ts +1 -0
  17. package/dist/components/forms/text-area.js +2 -0
  18. package/dist/components/forms/text-field.d.ts +3 -0
  19. package/dist/components/forms/text-field.js +15 -1
  20. package/dist/components/forms/tile-group.d.ts +6 -0
  21. package/dist/components/forms/tile-group.js +29 -2
  22. package/dist/components/layout/app-shell.js +13 -3
  23. package/dist/components/layout/nav-sidebar.js +14 -2
  24. package/dist/components/overlays/dialog.d.ts +6 -0
  25. package/dist/components/overlays/dialog.js +18 -2
  26. package/dist/components/overlays/drawer.d.ts +3 -0
  27. package/dist/components/overlays/drawer.js +11 -3
  28. package/dist/entries/mode-indicator.d.ts +1 -0
  29. package/dist/entries/mode-indicator.js +1 -0
  30. package/dist/entries/resource-row.d.ts +1 -0
  31. package/dist/entries/resource-row.js +1 -0
  32. package/dist/entries/verdict-banner.d.ts +1 -0
  33. package/dist/entries/verdict-banner.js +1 -0
  34. package/dist/index.d.ts +3 -0
  35. package/dist/index.js +3 -0
  36. package/dist/patterns/flow-builder/flow-builder.d.ts +8 -0
  37. package/dist/patterns/flow-builder/flow-builder.js +34 -0
  38. package/dist/patterns/flow-builder/primitives.d.ts +14 -2
  39. package/dist/patterns/flow-builder/primitives.js +28 -5
  40. package/dist/patterns/form-wizard/form-wizard.js +18 -3
  41. package/dist/patterns/form-wizard/wizard-summary.js +11 -2
  42. package/package.json +1 -1
@@ -85,6 +85,7 @@ export class NumberInput extends FormAssociatedElement {
85
85
  labelEl;
86
86
  errorEl;
87
87
  descriptionEl;
88
+ clampStatusEl;
88
89
  get disabled() {
89
90
  return this.hasAttribute("disabled");
90
91
  }
@@ -183,6 +184,14 @@ export class NumberInput extends FormAssociatedElement {
183
184
  }
184
185
  syncValue(nextValue) {
185
186
  const normalizedValue = this.clamp(nextValue);
187
+ if (normalizedValue !== nextValue) {
188
+ this.inputEl.value = String(normalizedValue);
189
+ const boundary = nextValue > normalizedValue ? "most" : "least";
190
+ this.clampStatusEl.textContent = `Changed to ${normalizedValue}, the ${boundary} allowed`;
191
+ }
192
+ else {
193
+ this.clampStatusEl.textContent = "";
194
+ }
186
195
  this.valueInternal = normalizedValue;
187
196
  this.setAttribute("value", String(normalizedValue));
188
197
  this.syncFormAssociation();
@@ -202,6 +211,7 @@ export class NumberInput extends FormAssociatedElement {
202
211
  <span part="label"></span>
203
212
  ${formDescriptionMarkup()}
204
213
  <input type="number" part="input" />
214
+ <span part="clamp-status" role="status" style="position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)"></span>
205
215
  ${formErrorMessageMarkup()}
206
216
  </label>
207
217
  `;
@@ -209,6 +219,7 @@ export class NumberInput extends FormAssociatedElement {
209
219
  this.inputEl = this.shadowRoot.querySelector('[part="input"]');
210
220
  this.errorEl = this.shadowRoot.querySelector('[part="error-message"]');
211
221
  this.descriptionEl = this.shadowRoot.querySelector('[part="description"]');
222
+ this.clampStatusEl = this.shadowRoot.querySelector('[part="clamp-status"]');
212
223
  }
213
224
  setupListeners() {
214
225
  this.inputEl.addEventListener("input", event => {
@@ -5,6 +5,7 @@ export declare class TextArea extends FormAssociatedElement {
5
5
  static get observedAttributes(): string[];
6
6
  private valueInternal;
7
7
  private textareaEl;
8
+ focus(options?: FocusOptions): void;
8
9
  private labelEl;
9
10
  private errorEl;
10
11
  get value(): string;
@@ -77,6 +77,8 @@ export class TextArea extends FormAssociatedElement {
77
77
  }
78
78
  valueInternal = "";
79
79
  textareaEl;
80
+ focus(options) { if (!this.disabled)
81
+ this.textareaEl?.focus(options); }
80
82
  labelEl;
81
83
  errorEl;
82
84
  get value() {
@@ -36,6 +36,9 @@ export declare class TextField extends FormAssociatedElement {
36
36
  */
37
37
  get reveal(): boolean;
38
38
  set reveal(value: boolean);
39
+ /** Noun used in the Show/Hide control's accessible name. Defaults to the field label. */
40
+ get revealLabel(): string;
41
+ set revealLabel(value: string);
39
42
  /** Shows a trailing spinner (e.g. while validating/looking up asynchronously). */
40
43
  get loading(): boolean;
41
44
  set loading(value: boolean);
@@ -125,6 +125,8 @@ const textFieldStyles = `
125
125
  appearance: none;
126
126
  border: 0;
127
127
  padding: 0.15rem 0.4rem;
128
+ min-inline-size: 24px;
129
+ min-block-size: 24px;
128
130
  border-radius: 4px;
129
131
  background: transparent;
130
132
  font: inherit;
@@ -169,6 +171,7 @@ export class TextField extends FormAssociatedElement {
169
171
  "label",
170
172
  "placeholder",
171
173
  "reveal",
174
+ "reveal-label",
172
175
  "type",
173
176
  "loading",
174
177
  "valid",
@@ -223,6 +226,16 @@ export class TextField extends FormAssociatedElement {
223
226
  set reveal(value) {
224
227
  this.toggleAttribute("reveal", Boolean(value));
225
228
  }
229
+ /** Noun used in the Show/Hide control's accessible name. Defaults to the field label. */
230
+ get revealLabel() {
231
+ return this.getAttribute("reveal-label") ?? "";
232
+ }
233
+ set revealLabel(value) {
234
+ if (value)
235
+ this.setAttribute("reveal-label", value);
236
+ else
237
+ this.removeAttribute("reveal-label");
238
+ }
226
239
  /** Shows a trailing spinner (e.g. while validating/looking up asynchronously). */
227
240
  get loading() {
228
241
  return this.hasAttribute("loading");
@@ -357,7 +370,8 @@ export class TextField extends FormAssociatedElement {
357
370
  // The visible word and the accessible name agree; aria-pressed makes
358
371
  // the toggle state machine explicit to AT.
359
372
  this.revealEl.textContent = this.revealed ? "Hide" : "Show";
360
- this.revealEl.setAttribute("aria-label", this.revealed ? "Hide password" : "Show password");
373
+ const noun = this.revealLabel || (this.hasAttribute("label") ? this.label : "password");
374
+ this.revealEl.setAttribute("aria-label", `${this.revealed ? "Hide" : "Show"} ${noun}`);
361
375
  this.revealEl.setAttribute("aria-pressed", String(this.revealed));
362
376
  }
363
377
  else {
@@ -3,8 +3,14 @@ import { BaseElement } from "../../core/index.js";
3
3
  export interface TileOption {
4
4
  description?: string;
5
5
  disabled?: boolean;
6
+ disabledReason?: string;
6
7
  id: string;
7
8
  label: string;
9
+ meta?: string;
10
+ status?: {
11
+ label: string;
12
+ tone?: "neutral" | "success" | "warning" | "error";
13
+ };
8
14
  }
9
15
  /** Attribute payloads are author input — validate every record. */
10
16
  export declare const isTileOptionRecord: (value: unknown) => value is TileOption;
@@ -14,7 +14,14 @@ export const isTileOptionRecord = (value) => {
14
14
  return false;
15
15
  }
16
16
  const item = value;
17
- return typeof item.id === "string" && typeof item.label === "string";
17
+ return typeof item.id === "string" && typeof item.label === "string"
18
+ && (item.description === undefined || typeof item.description === "string")
19
+ && (item.meta === undefined || typeof item.meta === "string")
20
+ && (item.disabledReason === undefined || typeof item.disabledReason === "string")
21
+ && (item.status === undefined || (typeof item.status === "object" && item.status !== null
22
+ && typeof item.status.label === "string"
23
+ && [undefined, "neutral", "success", "warning", "error"]
24
+ .includes(item.status.tone)));
18
25
  };
19
26
  const tileStyles = `
20
27
  :host {
@@ -109,6 +116,17 @@ const tileStyles = `
109
116
  [part="tile-description"]:empty {
110
117
  display: none;
111
118
  }
119
+
120
+ [part="tile-meta"], [part="tile-status"], [part="tile-disabled-reason"] {
121
+ font-size: 0.76rem;
122
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
123
+ overflow-wrap: normal;
124
+ }
125
+ [part="tile-status"] { font-weight: 700; }
126
+ [part="tile-status"][data-tone="success"] { color: var(--boe-token-text-status-text-success, #176b45); }
127
+ [part="tile-status"][data-tone="warning"] { color: var(--boe-token-text-status-text-warning, #805500); }
128
+ [part="tile-status"][data-tone="error"] { color: var(--boe-token-text-status-text-error, #ad1b36); }
129
+ .boe-sr-only { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }
112
130
  `;
113
131
  /**
114
132
  * Choose one option, or several, as cards rather than radios.
@@ -199,8 +217,11 @@ export class TileGroup extends BaseElement {
199
217
  rebuild() {
200
218
  const type = this.multiple ? "checkbox" : "radio";
201
219
  this.groupEl.innerHTML = this.options
202
- .map(option => {
220
+ .map((option, index) => {
203
221
  const id = escapeHtml(option.id);
222
+ const detailsId = `tile-details-${index}`;
223
+ const details = [option.description, option.meta, option.status?.label, option.disabledReason]
224
+ .filter(Boolean).join(". ");
204
225
  return `
205
226
  <label part="tile" data-option-id="${id}" data-selected="false" data-disabled="${option.disabled ? "true" : "false"}">
206
227
  <input
@@ -208,10 +229,16 @@ export class TileGroup extends BaseElement {
208
229
  type="${type}"
209
230
  name="${escapeHtml(this.name)}"
210
231
  value="${id}"
232
+ aria-label="${escapeHtml(option.label)}"
233
+ ${details ? `aria-describedby="${detailsId}"` : ""}
211
234
  ${option.disabled ? "disabled" : ""}
212
235
  />
213
236
  <span part="tile-label">${escapeHtml(option.label)}</span>
214
237
  <span part="tile-description">${escapeHtml(option.description ?? "")}</span>
238
+ ${option.meta ? `<span part="tile-meta">${escapeHtml(option.meta)}</span>` : ""}
239
+ ${option.status ? `<span part="tile-status" data-tone="${escapeHtml(option.status.tone ?? "neutral")}">${escapeHtml(option.status.label)}</span>` : ""}
240
+ ${option.disabledReason ? `<span part="tile-disabled-reason">${escapeHtml(option.disabledReason)}</span>` : ""}
241
+ ${details ? `<span id="${detailsId}" class="boe-sr-only">${escapeHtml(details)}</span>` : ""}
215
242
  </label>
216
243
  `;
217
244
  })
@@ -189,9 +189,9 @@ export class AppShell extends BaseElement {
189
189
  </div>
190
190
  </header>
191
191
  <div part="frame">
192
- <nav part="nav" aria-label="Primary">
192
+ <div part="nav" role="navigation" aria-label="Primary">
193
193
  <slot name="nav"></slot>
194
- </nav>
194
+ </div>
195
195
  <main part="main">
196
196
  <slot></slot>
197
197
  </main>
@@ -240,6 +240,17 @@ export class AppShell extends BaseElement {
240
240
  return;
241
241
  }
242
242
  this.navEl.hidden = !this.hasNamedSlotContent("nav", this.navSlot);
243
+ // A slotted NavSidebar supplies its own labelled nav landmark. Keep this
244
+ // wrapper for layout, but never nest a second navigation landmark around it.
245
+ const childOwnsNavigation = Array.from(this.children).some(el => el.getAttribute("slot") === "nav" && el.localName === "box-nav-sidebar");
246
+ if (childOwnsNavigation) {
247
+ this.navEl.removeAttribute("role");
248
+ this.navEl.removeAttribute("aria-label");
249
+ }
250
+ else {
251
+ this.navEl.setAttribute("role", "navigation");
252
+ this.navEl.setAttribute("aria-label", this.navLabel);
253
+ }
243
254
  this.asideEl.hidden = !this.hasNamedSlotContent("aside", this.asideSlot);
244
255
  this.footerEl.hidden = !this.hasNamedSlotContent("footer", this.footerSlot);
245
256
  }
@@ -250,7 +261,6 @@ export class AppShell extends BaseElement {
250
261
  const heading = this.heading;
251
262
  this.shellEl.setAttribute("aria-label", heading);
252
263
  this.titleEl.textContent = heading;
253
- this.navEl.setAttribute("aria-label", this.navLabel);
254
264
  this.asideEl.setAttribute("aria-label", this.asideLabel);
255
265
  this.syncLandmarkVisibility();
256
266
  }
@@ -37,8 +37,8 @@ const navSidebarStyles = `
37
37
  transition: inline-size ${boeMotionDuration.medium} ${boeMotionEasing.standard};
38
38
  }
39
39
 
40
- /* Collapsed: shrink to an icon strip. The header and footer usually hold
41
- branding and labels, so they hide; the body (icon nav) stays. */
40
+ /* Collapsed: shrink to an icon strip. Keep the header when it owns the
41
+ companion toggle; otherwise branding-only headers can disappear. */
42
42
  [part="sidebar"][data-collapsed="true"] {
43
43
  inline-size: 3.75rem;
44
44
  padding-inline: 0.55rem;
@@ -55,6 +55,14 @@ const navSidebarStyles = `
55
55
  display: none;
56
56
  }
57
57
 
58
+ [part="sidebar"][data-collapsed="true"][data-header-toggle="true"] [part="header"] {
59
+ display: block;
60
+ }
61
+
62
+ [part="sidebar"][data-collapsed="true"] [part="header"] ::slotted(:not(box-sidebar-toggle-button)) {
63
+ display: none;
64
+ }
65
+
58
66
  [part="body"] {
59
67
  display: flex;
60
68
  flex-direction: column;
@@ -192,6 +200,9 @@ export class NavSidebar extends BaseElement {
192
200
  ?.addEventListener("slotchange", () => {
193
201
  this.update();
194
202
  });
203
+ this.shadowRoot
204
+ ?.querySelector('slot[name="header"]')
205
+ ?.addEventListener("slotchange", () => this.update());
195
206
  }
196
207
  /**
197
208
  * Collapsing hides `[data-nav-label]`, which is a row's only accessible
@@ -232,6 +243,7 @@ export class NavSidebar extends BaseElement {
232
243
  return;
233
244
  }
234
245
  this.sidebarEl.dataset.collapsed = this.collapsed ? "true" : "false";
246
+ this.sidebarEl.dataset.headerToggle = Array.from(this.children).some(el => el.getAttribute("slot") === "header" && el.localName === "box-sidebar-toggle-button") ? "true" : "false";
235
247
  this.sidebarEl.setAttribute("aria-label", this.label);
236
248
  this.syncCollapsedRowNames();
237
249
  }
@@ -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
  }
@@ -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
@@ -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;