@unofficialbox/box-open-elements 0.27.0 → 0.28.1

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 +7 -2
  9. package/dist/components/feedback/progress-steps.js +42 -3
  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 +31 -8
  41. package/dist/patterns/form-wizard/wizard-summary.js +11 -2
  42. package/package.json +1 -1
@@ -14,7 +14,7 @@ import { tags } from "@lezer/highlight";
14
14
  export class CodeEditor extends BaseElement {
15
15
  static tagName = "box-code-editor";
16
16
  static get observedAttributes() {
17
- return ["language", "readonly", "label", "value", "wrap"];
17
+ return ["current-line-style", "fill-height", "hide-help", "hide-problems", "language", "readonly", "label", "value", "wrap"];
18
18
  }
19
19
  valueInternal = "";
20
20
  problemsInternal = [];
@@ -24,6 +24,7 @@ export class CodeEditor extends BaseElement {
24
24
  readonlyConfig = new Compartment();
25
25
  completionsConfig = new Compartment();
26
26
  wrapConfig = new Compartment();
27
+ wrapIndentConfig = new Compartment();
27
28
  highlightsConfig = new Compartment();
28
29
  highlightsInternal = [];
29
30
  source;
@@ -63,12 +64,48 @@ export class CodeEditor extends BaseElement {
63
64
  }
64
65
  get wrap() { return this.hasAttribute("wrap"); }
65
66
  set wrap(value) { this.toggleAttribute("wrap", value); }
67
+ get currentLineStyle() {
68
+ const value = this.getAttribute("current-line-style");
69
+ return value === "border" || value === "none" ? value : "fill";
70
+ }
71
+ set currentLineStyle(value) { this.setAttribute("current-line-style", value); }
72
+ get hideHelp() { return this.hasAttribute("hide-help"); }
73
+ set hideHelp(value) { this.toggleAttribute("hide-help", value); }
74
+ get hideProblems() { return this.hasAttribute("hide-problems"); }
75
+ set hideProblems(value) { this.toggleAttribute("hide-problems", value); }
76
+ get fillHeight() { return this.hasAttribute("fill-height"); }
77
+ set fillHeight(value) { this.toggleAttribute("fill-height", value); }
66
78
  get completionSource() { return this.source; }
67
79
  set completionSource(value) { this.source = value; if (this.isRendered)
68
80
  this.update(); }
69
81
  get highlights() { return this.highlightsInternal.map(range => ({ ...range })); }
70
82
  set highlights(value) { this.highlightsInternal = value.map(range => ({ ...range })); if (this.isRendered)
71
83
  this.update(); }
84
+ wrapIndentExtension() {
85
+ return EditorView.decorations.of(view => {
86
+ if (!this.wrap)
87
+ return Decoration.none;
88
+ const ranges = [];
89
+ const seen = new Set();
90
+ for (const visible of view.visibleRanges) {
91
+ const first = view.state.doc.lineAt(visible.from).number;
92
+ const last = view.state.doc.lineAt(visible.to).number;
93
+ for (let number = first; number <= last; number++) {
94
+ const line = view.state.doc.line(number);
95
+ if (seen.has(line.from))
96
+ continue;
97
+ seen.add(line.from);
98
+ const whitespace = line.text.match(/^[\t ]*/)?.[0] ?? "";
99
+ const columns = Math.min(24, whitespace.replaceAll("\t", " ").length);
100
+ if (columns)
101
+ ranges.push(Decoration.line({ attributes: {
102
+ style: `padding-left:${columns}ch;text-indent:-${columns}ch`,
103
+ } }).range(line.from));
104
+ }
105
+ }
106
+ return Decoration.set(ranges, true);
107
+ });
108
+ }
72
109
  highlightExtension() {
73
110
  return EditorView.decorations.of(view => {
74
111
  const doc = view.state.doc;
@@ -179,17 +216,22 @@ export class CodeEditor extends BaseElement {
179
216
  }
180
217
  renderTemplate() {
181
218
  this.shadowRoot.innerHTML = `<style>
182
- :host{display:block;min-width:0;color:var(--boe-token-text-text,#222);font:inherit}
219
+ :host{display:flex;flex-direction:column;min-width:0;min-height:0;color:var(--boe-token-text-text,#222);font:inherit}
183
220
  :host([hidden]){display:none!important}
184
- [part=editor]{border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:8px;overflow:hidden}
221
+ [part=editor]{border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:8px;overflow:hidden;min-height:0;flex:1 1 auto}
185
222
  .cm-editor{background:var(--boe-code-background,var(--boe-token-surface-surface,#fff));color:var(--boe-code-foreground,var(--boe-token-text-text,#222))}
186
223
  [part=editor]:focus-within{box-shadow:0 0 0 3px var(--boe-token-surface-surface-brand,#0061d5)}
187
224
  .cm-editor.cm-focused{outline:none}
188
225
  .cm-scroller{max-height:var(--boe-code-editor-height,420px);min-height:160px;overflow:auto;font-family:var(--boe-code-font-family,monospace);font-size:var(--boe-code-font-size,14px);line-height:var(--boe-code-line-height,1.6)}
226
+ :host([fill-height]){height:100%}
227
+ :host([fill-height]) [part=editor],:host([fill-height]) .cm-editor,:host([fill-height]) .cm-scroller{height:100%;max-height:none;min-height:0}
189
228
  .cm-gutters,.cm-panels,.cm-tooltip{background:var(--boe-token-surface-surface-secondary,#fbfbfb)!important;color:var(--boe-code-foreground,var(--boe-token-text-text,#222))!important;border-color:var(--boe-token-stroke-stroke,#ddd)!important}
190
229
  .cm-gutters{color:var(--boe-code-gutter,var(--boe-token-text-text,#222))!important;border-right:var(--boe-code-gutter-border,1px solid var(--boe-token-stroke-stroke,#ddd))!important}
191
230
  .cm-cursor{border-left-color:var(--boe-code-caret,var(--boe-token-text-text,#222))}
192
231
  .cm-activeLine,.cm-activeLineGutter{background:color-mix(in srgb,var(--boe-token-surface-surface-brand,#0061d5) 12%,transparent)!important}
232
+ :host([current-line-style=border]) .cm-activeLine{background:transparent!important;box-shadow:inset 0 1px var(--boe-code-current-line-border,var(--boe-token-stroke-stroke,#ddd)),inset 0 -1px var(--boe-code-current-line-border,var(--boe-token-stroke-stroke,#ddd))}
233
+ :host([current-line-style=none]) .cm-activeLine{background:transparent!important}
234
+ :host([current-line-style=border]) .cm-activeLineGutter,:host([current-line-style=none]) .cm-activeLineGutter{background:transparent!important}
193
235
  .cm-activeLineGutter{color:var(--boe-code-gutter-active,var(--boe-code-gutter,var(--boe-token-text-text,#222)))!important}
194
236
  .cm-selectionBackground,.cm-content ::selection{background:var(--boe-code-selection,color-mix(in srgb,var(--boe-token-surface-surface-brand,#0061d5) 12%,transparent))!important}
195
237
  .boe-code-highlight{background:var(--boe-code-highlight-background,color-mix(in srgb,var(--boe-token-surface-surface-brand,#0061d5) 10%,transparent))!important;box-shadow:inset 3px 0 var(--boe-token-surface-surface-brand,#0061d5)}
@@ -206,6 +248,7 @@ export class CodeEditor extends BaseElement {
206
248
  .cm-lint-marker-warning{background-image:radial-gradient(circle,var(--boe-token-text-status-text-warning,#9a6500) 45%,transparent 50%)!important}
207
249
  .cm-lint-marker-info{background-image:radial-gradient(circle,var(--boe-token-surface-surface-brand,#0061d5) 45%,transparent 50%)!important}
208
250
  [part=help],[part=problems]{font-size:13px;color:var(--boe-token-text-text-secondary,#666);margin:8px 0}
251
+ [part=help][hidden],[part=toolbar][hidden]{display:none}
209
252
  [part=toolbar]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
210
253
  button{font:inherit;color:inherit;background:var(--boe-token-surface-surface,#fff);border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:8px;padding:8px;min-height:36px}
211
254
  button:focus-visible{outline:3px solid var(--boe-token-surface-surface-brand,#0061d5);outline-offset:2px}
@@ -297,6 +340,7 @@ export class CodeEditor extends BaseElement {
297
340
  ]),
298
341
  this.completionsConfig.of(this.completionExtension()),
299
342
  this.wrapConfig.of(this.wrap ? EditorView.lineWrapping : []),
343
+ this.wrapIndentConfig.of(this.wrapIndentExtension()),
300
344
  this.highlightsConfig.of(this.highlightExtension()),
301
345
  syntaxHighlighting(HighlightStyle.define([
302
346
  {
@@ -388,6 +432,7 @@ export class CodeEditor extends BaseElement {
388
432
  ]),
389
433
  this.completionsConfig.reconfigure(this.completionExtension()),
390
434
  this.wrapConfig.reconfigure(this.wrap ? EditorView.lineWrapping : []),
435
+ this.wrapIndentConfig.reconfigure(this.wrapIndentExtension()),
391
436
  this.highlightsConfig.reconfigure(this.highlightExtension()),
392
437
  ],
393
438
  });
@@ -398,7 +443,12 @@ export class CodeEditor extends BaseElement {
398
443
  }
399
444
  this.view.contentDOM.setAttribute("aria-label", this.label);
400
445
  this.view.contentDOM.setAttribute("tabindex", "0");
401
- this.view.contentDOM.setAttribute("aria-describedby", "editor-help");
446
+ if (this.hideHelp)
447
+ this.view.contentDOM.removeAttribute("aria-describedby");
448
+ else
449
+ this.view.contentDOM.setAttribute("aria-describedby", "editor-help");
450
+ this.shadowRoot.querySelector('[part=help]').hidden = this.hideHelp;
451
+ this.shadowRoot.querySelector('[part=toolbar]').hidden = this.hideProblems;
402
452
  this.syncProblems();
403
453
  }
404
454
  flushChange() {
@@ -9,6 +9,7 @@ export declare class NumberInput extends FormAssociatedElement {
9
9
  private labelEl;
10
10
  private errorEl;
11
11
  private descriptionEl;
12
+ private clampStatusEl;
12
13
  get disabled(): boolean;
13
14
  set disabled(value: boolean);
14
15
  get label(): string;
@@ -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;