@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
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  Browse the full catalog on the **[live docs site](https://unofficialbox.github.io/box-open-elements)** — live previews, an events/properties inspector, per-framework code snippets (React / Angular / Vue / Svelte / HTML), and the foundations pages.
14
14
 
15
- The library is organized around three layers (see [taxonomy](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/taxonomy.md) for the canonical model):
15
+ The library is organized around three layers (see [taxonomy](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/taxonomy.md) for the canonical model):
16
16
 
17
17
  - **Foundations** — design decisions as data: tokens, color, typography, iconography, accessibility, theming
18
18
  - **Components** — accessible Web Components for single controls, organized by category
@@ -77,7 +77,7 @@ selection.toggleSelection("1");
77
77
  - optimized component entrypoints: `@unofficialbox/box-open-elements/<name>`
78
78
  - patterns: `@unofficialbox/box-open-elements/patterns/<area>` and `@unofficialbox/box-open-elements/patterns/<area>/<module>`
79
79
 
80
- See the versioned [API guidelines](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/api-guidelines.md).
80
+ See the versioned [API guidelines](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/api-guidelines.md).
81
81
 
82
82
  ## Developing
83
83
 
@@ -91,17 +91,17 @@ bun run docs # build + serve the docs site at http://localhost:4600
91
91
 
92
92
  ## Documentation
93
93
 
94
- The versioned docs index is at [docs/README.md](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/README.md). The most important entries:
95
-
96
- - [Taxonomy](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/taxonomy.md)
97
- - [Architecture](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/architecture.md)
98
- - [API Guidelines](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/api-guidelines.md)
99
- - [Components catalog](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/components/catalog.md) and [patterns catalog](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/patterns/catalog.md)
100
- - [Using with React, Angular, Vue, and Svelte](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/integration/frameworks.md)
101
- - [Design Tokens](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/foundations/tokens.md)
102
- - [Content Explorer](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/patterns/content-explorer.md)
103
- - [Box Server Integration](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/integration/box-server.md)
104
- - [Agent UI guidelines](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/patterns/agent-ui-guidelines.md), [building blocks](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/patterns/agent-ui.md), and [motion](https://github.com/unofficialbox/box-open-elements/blob/v0.26.0/docs/foundations/motion.md)
94
+ The versioned docs index is at [docs/README.md](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/README.md). The most important entries:
95
+
96
+ - [Taxonomy](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/taxonomy.md)
97
+ - [Architecture](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/architecture.md)
98
+ - [API Guidelines](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/api-guidelines.md)
99
+ - [Components catalog](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/components/catalog.md) and [patterns catalog](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/patterns/catalog.md)
100
+ - [Using with React, Angular, Vue, and Svelte](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/integration/frameworks.md)
101
+ - [Design Tokens](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/foundations/tokens.md)
102
+ - [Content Explorer](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/patterns/content-explorer.md)
103
+ - [Box Server Integration](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/integration/box-server.md)
104
+ - [Agent UI guidelines](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/patterns/agent-ui-guidelines.md), [building blocks](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/patterns/agent-ui.md), and [motion](https://github.com/unofficialbox/box-open-elements/blob/v0.28.0/docs/foundations/motion.md)
105
105
 
106
106
  ## Contributing
107
107
 
@@ -21,6 +21,9 @@ export declare class Button extends BaseElement {
21
21
  set disabled(value: boolean);
22
22
  get label(): string;
23
23
  set label(value: string);
24
+ /** Optional spoken name for repeated actions whose visible label is brief. */
25
+ get accessibleLabel(): string;
26
+ set accessibleLabel(value: string);
24
27
  get size(): string;
25
28
  set size(value: string);
26
29
  get tone(): string;
@@ -2,6 +2,10 @@ import { BaseElement } from "../../core/index.js";
2
2
  import { boeControl, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
3
3
  import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
4
4
  const DEFAULT_TAG_NAME = "box-button";
5
+ // The status red is appropriate for accents, but white button text on it
6
+ // misses 4.5:1 in the light theme. Mix toward the theme's primary text color
7
+ // (dark in light mode, light in dark mode) without changing shared error tokens.
8
+ const dangerFill = "var(--boe-button-danger-background, color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 80%, var(--boe-token-text-text, #222222) 20%))";
5
9
  /**
6
10
  * Visual language tracks box-ui-elements `.btn` / `.btn-primary`
7
11
  * (`src/styles/common/_buttons.scss` + constants/_buttons.scss).
@@ -101,19 +105,19 @@ const buttonStyles = `
101
105
  }
102
106
 
103
107
  button[data-tone="danger"] {
104
- background: var(--boe-token-surface-status-surface-error, #ed3757);
105
- border-color: var(--boe-token-surface-status-surface-error, #ed3757);
108
+ background: ${dangerFill};
109
+ border-color: ${dangerFill};
106
110
  color: var(--boe-token-text-text-on-brand, #ffffff);
107
111
  }
108
112
 
109
113
  button[data-tone="danger"]:hover:not(:disabled) {
110
- background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 92%, white 8%);
111
- border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 92%, white 8%);
114
+ background: color-mix(in srgb, ${dangerFill} 92%, white 8%);
115
+ border-color: color-mix(in srgb, ${dangerFill} 92%, white 8%);
112
116
  }
113
117
 
114
118
  button[data-tone="danger"]:active:not(:disabled) {
115
- background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 88%, black 12%);
116
- border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 88%, black 12%);
119
+ background: color-mix(in srgb, ${dangerFill} 88%, black 12%);
120
+ border-color: color-mix(in srgb, ${dangerFill} 88%, black 12%);
117
121
  }
118
122
 
119
123
  button[data-tone="danger"]:focus-visible {
@@ -184,7 +188,7 @@ export class Button extends BaseElement {
184
188
  static tagName = DEFAULT_TAG_NAME;
185
189
  static formAssociated = true;
186
190
  static get observedAttributes() {
187
- return ["disabled", "label", "size", "tone", "is-loading", "type"];
191
+ return ["accessible-label", "aria-label", "disabled", "label", "size", "tone", "is-loading", "type"];
188
192
  }
189
193
  buttonEl;
190
194
  focus(options) { if (!this.disabled)
@@ -230,6 +234,16 @@ export class Button extends BaseElement {
230
234
  set label(value) {
231
235
  this.setAttribute("label", value);
232
236
  }
237
+ /** Optional spoken name for repeated actions whose visible label is brief. */
238
+ get accessibleLabel() {
239
+ return this.getAttribute("accessible-label") ?? "";
240
+ }
241
+ set accessibleLabel(value) {
242
+ if (value)
243
+ this.setAttribute("accessible-label", value);
244
+ else
245
+ this.removeAttribute("accessible-label");
246
+ }
233
247
  get size() {
234
248
  return this.getAttribute("size") ?? "medium";
235
249
  }
@@ -290,6 +304,11 @@ export class Button extends BaseElement {
290
304
  this.buttonEl.dataset.tone = this.tone;
291
305
  this.buttonEl.dataset.size = this.size;
292
306
  this.labelEl.textContent = this.label;
307
+ const spokenLabel = this.accessibleLabel || this.getAttribute("aria-label");
308
+ if (spokenLabel)
309
+ this.buttonEl.setAttribute("aria-label", spokenLabel);
310
+ else
311
+ this.buttonEl.removeAttribute("aria-label");
293
312
  const busy = this.isLoading;
294
313
  this.spinnerEl.hidden = !busy;
295
314
  this.buttonEl.setAttribute("aria-busy", String(busy));
@@ -0,0 +1,28 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ /** Selectable resource information with independent slotted actions. Not a listbox option. */
3
+ export declare class ResourceRow extends BaseElement {
4
+ static readonly tagName = "box-resource-row";
5
+ static get observedAttributes(): string[];
6
+ private selectEl;
7
+ private labelEl;
8
+ private metaEl;
9
+ private statusEl;
10
+ private statusSlot;
11
+ get label(): string;
12
+ set label(value: string);
13
+ get meta(): string;
14
+ set meta(value: string);
15
+ get status(): string;
16
+ set status(value: string);
17
+ get value(): string;
18
+ set value(value: string);
19
+ get selected(): boolean;
20
+ set selected(value: boolean);
21
+ get active(): boolean;
22
+ set active(value: boolean);
23
+ get disabled(): boolean;
24
+ set disabled(value: boolean);
25
+ protected renderTemplate(): void;
26
+ protected setupListeners(): void;
27
+ protected update(): void;
28
+ }
@@ -0,0 +1,75 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ import { boeRadius } from "../../foundations/geometry/index.js";
3
+ const styles = `
4
+ :host { display:block; container-type:inline-size; font:inherit; color:var(--boe-token-text-text,#222); }
5
+ :host([hidden]) { display:none!important; }
6
+ [part="row"] { display:flex; align-items:center; gap:.75rem; min-inline-size:0; padding:.65rem; border:1px solid var(--boe-token-stroke-stroke,#dedede); border-radius:${boeRadius.med}; background:var(--boe-token-surface-surface,#fff); }
7
+ :host([selected]) [part="row"] { border-color:var(--boe-token-surface-surface-brand,#0061d5); background:color-mix(in srgb,var(--boe-token-surface-surface-brand,#0061d5) 7%,var(--boe-token-surface-surface,#fff)); }
8
+ :host([active]) [part="row"] { box-shadow:inset 0 0 0 1px var(--boe-token-surface-surface-brand,#0061d5); }
9
+ [part="select"] { flex:1 1 auto; min-inline-size:0; display:flex; align-items:center; gap:.65rem; padding:.25rem; border:0; border-radius:4px; background:none; color:inherit; font:inherit; text-align:start; cursor:pointer; }
10
+ [part="select"]:focus-visible { outline:2px solid var(--boe-token-surface-surface-brand,#0061d5); outline-offset:2px; }
11
+ [part="select"]:disabled { cursor:not-allowed; opacity:.55; }
12
+ [part="body"] { display:grid; gap:.15rem; min-inline-size:0; }
13
+ [part="label"] { font-weight:600; overflow-wrap:anywhere; }
14
+ [part="meta"] { color:var(--boe-token-text-text-secondary,#6f6f6f); font-size:.8em; white-space:pre-line; overflow-wrap:anywhere; }
15
+ [part="meta"]:empty, [part="status"]:empty { display:none; }
16
+ [part="trailing"] { flex:0 1 auto; display:flex; align-items:center; gap:.5rem; min-inline-size:0; }
17
+ [part="status"] { color:var(--boe-token-text-text-secondary,#6f6f6f); font-size:.8em; }
18
+ ::slotted([slot="actions"]) { flex:none; }
19
+ @container (max-width: 320px) { [part="row"] { align-items:stretch; flex-wrap:wrap; } [part="trailing"] { inline-size:100%; justify-content:space-between; padding-inline-start:2.5rem; } }
20
+ @media (prefers-reduced-motion:reduce) { *,*::before,*::after { transition:none!important; } }
21
+ `;
22
+ /** Selectable resource information with independent slotted actions. Not a listbox option. */
23
+ export class ResourceRow extends BaseElement {
24
+ static tagName = "box-resource-row";
25
+ static get observedAttributes() { return ["label", "meta", "status", "value", "selected", "active", "disabled"]; }
26
+ selectEl;
27
+ labelEl;
28
+ metaEl;
29
+ statusEl;
30
+ statusSlot;
31
+ get label() { return this.getAttribute("label") ?? ""; }
32
+ set label(value) { this.setAttribute("label", value); }
33
+ get meta() { return this.getAttribute("meta") ?? ""; }
34
+ set meta(value) { this.setAttribute("meta", value); }
35
+ get status() { return this.getAttribute("status") ?? ""; }
36
+ set status(value) { this.setAttribute("status", value); }
37
+ get value() { return this.getAttribute("value") ?? this.label; }
38
+ set value(value) { this.setAttribute("value", value); }
39
+ get selected() { return this.hasAttribute("selected"); }
40
+ set selected(value) { this.toggleAttribute("selected", value); }
41
+ get active() { return this.hasAttribute("active"); }
42
+ set active(value) { this.toggleAttribute("active", value); }
43
+ get disabled() { return this.hasAttribute("disabled"); }
44
+ set disabled(value) { this.toggleAttribute("disabled", value); }
45
+ renderTemplate() {
46
+ this.shadowRoot.innerHTML = `<style>${styles}</style><div part="row"><button part="select" type="button"><slot name="icon"></slot><span part="body"><span part="label"></span><span part="meta"></span></span></button><span part="trailing"><span part="status"></span><slot name="status"></slot><slot name="actions"></slot></span></div>`;
47
+ this.selectEl = this.shadowRoot.querySelector('[part="select"]');
48
+ this.labelEl = this.shadowRoot.querySelector('[part="label"]');
49
+ this.metaEl = this.shadowRoot.querySelector('[part="meta"]');
50
+ this.statusEl = this.shadowRoot.querySelector('[part="status"]');
51
+ this.statusSlot = this.shadowRoot.querySelector('slot[name="status"]');
52
+ }
53
+ setupListeners() {
54
+ this.selectEl.addEventListener("click", () => this.dispatchEvent(new CustomEvent("select", { bubbles: true, composed: true, detail: { value: this.value } })));
55
+ this.statusSlot.addEventListener("slotchange", () => this.update());
56
+ }
57
+ update() {
58
+ this.labelEl.textContent = this.label;
59
+ this.metaEl.textContent = this.meta;
60
+ this.statusEl.textContent = this.status;
61
+ this.statusEl.hidden = this.statusSlot.assignedElements().length > 0;
62
+ this.selectEl.disabled = this.disabled;
63
+ this.selectEl.setAttribute("aria-pressed", String(this.selected));
64
+ this.selectEl.setAttribute("aria-label", this.label);
65
+ const slottedStatus = this.statusSlot.assignedNodes({ flatten: true }).map(node => node instanceof Element
66
+ ? node.getAttribute("aria-label") ?? node.getAttribute("label") ?? node.textContent ?? ""
67
+ : node.textContent ?? "").map(value => value.trim()).filter(Boolean).join(" ");
68
+ const spokenStatus = this.status || slottedStatus;
69
+ if (this.meta || spokenStatus)
70
+ this.selectEl.setAttribute("aria-description", [this.meta.replaceAll("\n", ", "), spokenStatus].filter(Boolean).join("; "));
71
+ else
72
+ this.selectEl.removeAttribute("aria-description");
73
+ }
74
+ }
75
+ ResourceRow.register();
@@ -0,0 +1,21 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ /** Host-controlled dry/live indicator. The host owns the mode-change destination. */
3
+ export declare class ModeIndicator extends BaseElement {
4
+ static readonly tagName = "box-mode-indicator";
5
+ static get observedAttributes(): string[];
6
+ private indicatorEl;
7
+ private stateEl;
8
+ private detailEl;
9
+ private readonly announcement;
10
+ private previousMode;
11
+ get mode(): "dry" | "live";
12
+ set mode(value: "dry" | "live");
13
+ get detail(): string;
14
+ set detail(value: string);
15
+ get namePrefix(): string;
16
+ set namePrefix(value: string);
17
+ get interactive(): boolean;
18
+ set interactive(value: boolean);
19
+ protected renderTemplate(): void;
20
+ protected update(): void;
21
+ }
@@ -0,0 +1,67 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ import { FeedbackAnnouncement } from "../../foundations/a11y/announcer.js";
3
+ const styles = `
4
+ :host { display:inline-flex; max-inline-size:100%; font:inherit; color:var(--boe-token-text-text,#222); }
5
+ :host([hidden]) { display:none!important; }
6
+ [part="indicator"] { display:inline-flex; align-items:center; gap:.4rem; min-block-size:24px; min-inline-size:0; padding:.2rem .55rem; border-radius:999px; border:1px solid var(--boe-token-stroke-stroke,#dedede); background:var(--boe-token-surface-surface-secondary,#f4f4f4); color:inherit; font:inherit; text-align:start; }
7
+ button[part="indicator"] { cursor:pointer; }
8
+ button[part="indicator"]:focus-visible { outline:2px solid var(--boe-token-surface-surface-brand,#0061d5); outline-offset:2px; }
9
+ :host([mode="live"]) [part="indicator"] { border-color:var(--boe-token-text-status-text-error,#b92340); background:color-mix(in srgb,var(--boe-token-surface-status-surface-error,#ed3757) 9%,var(--boe-token-surface-surface,#fff)); }
10
+ [part="dot"] { flex:none; inline-size:.45rem; block-size:.45rem; border-radius:50%; background:var(--boe-token-text-text-secondary,#6f6f6f); }
11
+ :host([mode="live"]) [part="dot"] { background:var(--boe-token-text-status-text-error,#b92340); }
12
+ [part="state"] { font-weight:700; white-space:nowrap; }
13
+ [part="detail"] { min-inline-size:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
14
+ [part="detail"]:empty { display:none; }
15
+ :host([live-stripe][mode="live"]) { border-block-start:3px solid var(--boe-token-text-status-text-error,#b92340); }
16
+ @media (max-width:390px) { [part="detail"] { display:none; } }
17
+ `;
18
+ /** Host-controlled dry/live indicator. The host owns the mode-change destination. */
19
+ export class ModeIndicator extends BaseElement {
20
+ static tagName = "box-mode-indicator";
21
+ static get observedAttributes() { return ["mode", "detail", "name-prefix", "interactive", "live-stripe"]; }
22
+ indicatorEl;
23
+ stateEl;
24
+ detailEl;
25
+ announcement = new FeedbackAnnouncement();
26
+ previousMode = null;
27
+ get mode() { return this.getAttribute("mode") === "live" ? "live" : "dry"; }
28
+ set mode(value) { this.setAttribute("mode", value); }
29
+ get detail() { return this.getAttribute("detail") ?? ""; }
30
+ set detail(value) { this.setAttribute("detail", value); }
31
+ get namePrefix() { return this.getAttribute("name-prefix") ?? "Next run"; }
32
+ set namePrefix(value) { this.setAttribute("name-prefix", value); }
33
+ get interactive() { return this.hasAttribute("interactive"); }
34
+ set interactive(value) { this.toggleAttribute("interactive", value); }
35
+ renderTemplate() {
36
+ this.shadowRoot.innerHTML = `<style>${styles}</style><span part="indicator"><span part="dot" aria-hidden="true"></span><span part="state"></span><span part="detail"></span></span>`;
37
+ this.indicatorEl = this.shadowRoot.querySelector('[part="indicator"]');
38
+ this.stateEl = this.shadowRoot.querySelector('[part="state"]');
39
+ this.detailEl = this.shadowRoot.querySelector('[part="detail"]');
40
+ }
41
+ update() {
42
+ const mode = this.mode;
43
+ if (this.interactive !== (this.indicatorEl.tagName === "BUTTON")) {
44
+ const replacement = document.createElement(this.interactive ? "button" : "span");
45
+ replacement.setAttribute("part", "indicator");
46
+ if (!this.interactive)
47
+ replacement.setAttribute("role", "group");
48
+ if (replacement instanceof HTMLButtonElement)
49
+ replacement.type = "button";
50
+ replacement.append(...Array.from(this.indicatorEl.childNodes));
51
+ this.indicatorEl.replaceWith(replacement);
52
+ this.indicatorEl = replacement;
53
+ this.indicatorEl.addEventListener("click", () => { if (this.interactive)
54
+ this.dispatchEvent(new CustomEvent("activate", { bubbles: true, composed: true })); });
55
+ }
56
+ const state = mode === "live" ? "Live" : "Dry run";
57
+ if (!this.interactive)
58
+ this.indicatorEl.setAttribute("role", "group");
59
+ this.stateEl.textContent = state;
60
+ this.detailEl.textContent = this.detail ? `· ${this.detail}` : "";
61
+ this.indicatorEl.setAttribute("aria-label", `${this.namePrefix}: ${state}${this.detail ? `, ${this.detail}` : ""}`);
62
+ if (this.previousMode !== null && this.previousMode !== mode)
63
+ this.announcement.update(this, this.indicatorEl.getAttribute("aria-label") ?? "", "polite");
64
+ this.previousMode = mode;
65
+ }
66
+ }
67
+ ModeIndicator.register();
@@ -46,7 +46,11 @@ export declare class ProgressSteps extends BaseElement {
46
46
  private valueInternal;
47
47
  private stepsEl;
48
48
  private liveEl;
49
+ private compactToggleEl;
50
+ private compactExpanded;
49
51
  private itemsSignature;
52
+ get compact(): boolean;
53
+ set compact(value: boolean);
50
54
  get label(): string;
51
55
  set label(value: string);
52
56
  get items(): ProgressStepItem[];
@@ -59,6 +59,23 @@ const progressStepsStyles = `
59
59
  font: inherit;
60
60
  }
61
61
 
62
+ [part="compact-toggle"] { display: none; }
63
+ @media (max-width: 600px) {
64
+ [part="compact-toggle"][data-enabled="true"] {
65
+ display: block;
66
+ width: 100%;
67
+ min-height: 36px;
68
+ text-align: left;
69
+ border: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
70
+ border-radius: ${boeRadius.med};
71
+ background: var(--boe-token-surface-surface, #ffffff);
72
+ color: var(--boe-token-text-text, #222222);
73
+ font: inherit;
74
+ padding: 0.45rem 0.65rem;
75
+ }
76
+ [part="steps"][data-compact="true"][data-expanded="false"] { display: none; }
77
+ }
78
+
62
79
  /* The host's own display would otherwise beat the UA rule for [hidden],
63
80
  leaving the element on screen when a host hides it. */
64
81
  :host([hidden]) {
@@ -214,12 +231,16 @@ const progressStepsStyles = `
214
231
  export class ProgressSteps extends BaseElement {
215
232
  static tagName = DEFAULT_TAG_NAME;
216
233
  static get observedAttributes() {
217
- return ["items", "label", "value"];
234
+ return ["compact", "items", "label", "value"];
218
235
  }
219
236
  valueInternal = "";
220
237
  stepsEl;
221
238
  liveEl;
239
+ compactToggleEl;
240
+ compactExpanded = false;
222
241
  itemsSignature = "";
242
+ get compact() { return this.hasAttribute("compact"); }
243
+ set compact(value) { this.toggleAttribute("compact", Boolean(value)); }
223
244
  get label() {
224
245
  return this.getAttribute("label") ?? "Progress Steps";
225
246
  }
@@ -270,13 +291,19 @@ export class ProgressSteps extends BaseElement {
270
291
  }
271
292
  this.shadowRoot.innerHTML = `
272
293
  <style>${progressStepsStyles}</style>
273
- <div part="steps" role="group"></div>
294
+ <button type="button" part="compact-toggle" aria-controls="progress-steps-list" aria-expanded="false"></button>
295
+ <div id="progress-steps-list" part="steps" role="group"></div>
274
296
  <div part="live" class="boe-sr-only" aria-live="polite"></div>
275
297
  `;
276
298
  this.stepsEl = this.shadowRoot.querySelector('[part="steps"]');
299
+ this.compactToggleEl = this.shadowRoot.querySelector('[part="compact-toggle"]');
277
300
  this.liveEl = this.shadowRoot.querySelector('[part="live"]');
278
301
  }
279
302
  setupListeners() {
303
+ this.compactToggleEl.addEventListener("click", () => {
304
+ this.compactExpanded = !this.compactExpanded;
305
+ this.update();
306
+ });
280
307
  this.stepsEl.addEventListener("click", event => {
281
308
  const step = event.target.closest('[part="step"]');
282
309
  if (!step || !this.stepsEl.contains(step)) {
@@ -397,6 +424,11 @@ export class ProgressSteps extends BaseElement {
397
424
  }
398
425
  this.stepsEl.setAttribute("aria-label", this.label);
399
426
  const resolved = resolveStepStates(items, this.valueInternal);
427
+ this.compactToggleEl.dataset.enabled = String(this.compact);
428
+ this.compactToggleEl.setAttribute("aria-expanded", String(this.compactExpanded));
429
+ this.compactToggleEl.textContent = `Step ${activeIndex + 1} of ${items.length}: ${items[activeIndex]?.label ?? ""} · ${this.compactExpanded ? "Hide steps" : "Show steps"}`;
430
+ this.stepsEl.dataset.compact = String(this.compact);
431
+ this.stepsEl.dataset.expanded = String(this.compactExpanded);
400
432
  const tabbable = this.tabbableIndex(resolved);
401
433
  const signature = JSON.stringify(items);
402
434
  if (signature !== this.itemsSignature) {
@@ -0,0 +1,23 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ /** Finished-run verdict with a single action and one-time polite announcement. */
3
+ export declare class VerdictBanner extends BaseElement {
4
+ static readonly tagName = "box-verdict-banner";
5
+ static get observedAttributes(): string[];
6
+ private readonly headingId;
7
+ private readonly announcement;
8
+ private headingEl;
9
+ private reasonsEl;
10
+ private iconEl;
11
+ private reasonsValue;
12
+ private announced;
13
+ get tone(): "passed" | "missed" | "none";
14
+ set tone(value: "passed" | "missed" | "none");
15
+ get heading(): string;
16
+ set heading(value: string);
17
+ get headingLevel(): number;
18
+ set headingLevel(value: number);
19
+ get reasons(): string[];
20
+ set reasons(value: string[]);
21
+ protected renderTemplate(): void;
22
+ protected update(): void;
23
+ }
@@ -0,0 +1,59 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ import { FeedbackAnnouncement } from "../../foundations/a11y/announcer.js";
3
+ import { boeRadius } from "../../foundations/geometry/index.js";
4
+ import { toneIcon } from "./tone.js";
5
+ const styles = `
6
+ :host { display:block; font:inherit; color:var(--boe-token-text-text,#222); container-type:inline-size; }
7
+ :host([hidden]) { display:none!important; }
8
+ [part="banner"] { display:grid; grid-template-columns:20px minmax(0,1fr) auto; align-items:start; gap:.7rem; padding:1rem; border-radius:${boeRadius.med}; border:1px solid var(--boe-token-stroke-stroke,#dedede); background:var(--boe-token-surface-surface-secondary,#f4f4f4); }
9
+ :host([tone="passed"]) [part="banner"] { border-color:var(--boe-token-text-status-text-success,#187657); background:color-mix(in srgb,var(--boe-token-surface-status-surface-success,#26c281) 9%,var(--boe-token-surface-surface,#fff)); }
10
+ :host([tone="missed"]) [part="banner"] { border-color:var(--boe-token-text-status-text-error,#b92340); background:color-mix(in srgb,var(--boe-token-surface-status-surface-error,#ed3757) 9%,var(--boe-token-surface-surface,#fff)); }
11
+ [part="icon"] { inline-size:20px; block-size:20px; color:var(--boe-token-text-text-secondary,#6f6f6f); }
12
+ :host([tone="passed"]) [part="icon"] { color:var(--boe-token-text-status-text-success,#187657); }
13
+ :host([tone="missed"]) [part="icon"] { color:var(--boe-token-text-status-text-error,#b92340); }
14
+ [part="icon"] svg { inline-size:100%; block-size:100%; }
15
+ [part="body"] { min-inline-size:0; }
16
+ [part="heading"] { margin:0; font:inherit; font-weight:700; overflow-wrap:anywhere; }
17
+ [part="reasons"] { margin:.45rem 0 0; padding-inline-start:1.2rem; overflow-wrap:anywhere; }
18
+ [part="reasons"]:empty { display:none; }
19
+ @container (max-width:320px) { [part="banner"] { grid-template-columns:20px minmax(0,1fr); } [part="action"] { grid-column:2; } }
20
+ `;
21
+ let nextHeadingId = 0;
22
+ /** Finished-run verdict with a single action and one-time polite announcement. */
23
+ export class VerdictBanner extends BaseElement {
24
+ static tagName = "box-verdict-banner";
25
+ static get observedAttributes() { return ["tone", "heading", "heading-level"]; }
26
+ headingId = `boe-verdict-${++nextHeadingId}`;
27
+ announcement = new FeedbackAnnouncement();
28
+ headingEl;
29
+ reasonsEl;
30
+ iconEl;
31
+ reasonsValue = [];
32
+ announced = false;
33
+ get tone() { const value = this.getAttribute("tone"); return value === "passed" || value === "missed" ? value : "none"; }
34
+ set tone(value) { this.setAttribute("tone", value); }
35
+ get heading() { return this.getAttribute("heading") ?? ""; }
36
+ set heading(value) { this.setAttribute("heading", value); }
37
+ get headingLevel() { const value = Number(this.getAttribute("heading-level") ?? 2); return Number.isInteger(value) && value >= 1 && value <= 6 ? value : 2; }
38
+ set headingLevel(value) { this.setAttribute("heading-level", String(value)); }
39
+ get reasons() { return [...this.reasonsValue]; }
40
+ set reasons(value) { this.reasonsValue = [...value]; if (this.isRendered)
41
+ this.update(); }
42
+ renderTemplate() {
43
+ this.shadowRoot.innerHTML = `<style>${styles}</style><section part="banner" role="group" aria-labelledby="${this.headingId}"><span part="icon" aria-hidden="true"></span><div part="body"><div part="heading" id="${this.headingId}" role="heading"></div><ul part="reasons"></ul></div><span part="action"><slot name="action"></slot></span></section>`;
44
+ this.headingEl = this.shadowRoot.querySelector('[part="heading"]');
45
+ this.reasonsEl = this.shadowRoot.querySelector('[part="reasons"]');
46
+ this.iconEl = this.shadowRoot.querySelector('[part="icon"]');
47
+ }
48
+ update() {
49
+ this.headingEl.textContent = this.heading;
50
+ this.headingEl.setAttribute("aria-level", String(this.headingLevel));
51
+ this.iconEl.innerHTML = toneIcon(this.tone === "passed" ? "success" : this.tone === "missed" ? "error" : "info");
52
+ this.reasonsEl.replaceChildren(...this.reasonsValue.map(reason => { const item = document.createElement("li"); item.textContent = reason; return item; }));
53
+ if (!this.announced && this.heading && this.tone !== "none") {
54
+ this.announced = true;
55
+ this.announcement.update(this, this.heading, "polite");
56
+ }
57
+ }
58
+ }
59
+ VerdictBanner.register();
@@ -21,6 +21,11 @@ export interface CodeSelection {
21
21
  anchor: number;
22
22
  head: number;
23
23
  }
24
+ export interface CodeLine {
25
+ number: number;
26
+ from: number;
27
+ to: number;
28
+ }
24
29
  /** Optional CodeMirror entrypoint. Import `code-editor`, not the root catalog. */
25
30
  export declare class CodeEditor extends BaseElement {
26
31
  static readonly tagName = "box-code-editor";
@@ -33,6 +38,7 @@ export declare class CodeEditor extends BaseElement {
33
38
  private readonlyConfig;
34
39
  private completionsConfig;
35
40
  private wrapConfig;
41
+ private wrapIndentConfig;
36
42
  private highlightsConfig;
37
43
  private highlightsInternal;
38
44
  private source?;
@@ -50,10 +56,19 @@ export declare class CodeEditor extends BaseElement {
50
56
  }): void;
51
57
  get wrap(): boolean;
52
58
  set wrap(value: boolean);
59
+ get currentLineStyle(): "fill" | "border" | "none";
60
+ set currentLineStyle(value: "fill" | "border" | "none");
61
+ get hideHelp(): boolean;
62
+ set hideHelp(value: boolean);
63
+ get hideProblems(): boolean;
64
+ set hideProblems(value: boolean);
65
+ get fillHeight(): boolean;
66
+ set fillHeight(value: boolean);
53
67
  get completionSource(): CodeCompletionSource | undefined;
54
68
  set completionSource(value: CodeCompletionSource | undefined);
55
69
  get highlights(): readonly CodeHighlight[];
56
70
  set highlights(value: readonly CodeHighlight[]);
71
+ private wrapIndentExtension;
57
72
  private highlightExtension;
58
73
  get language(): string;
59
74
  set language(value: string);
@@ -67,6 +82,10 @@ export declare class CodeEditor extends BaseElement {
67
82
  set completions(value: readonly CodeCompletion[]);
68
83
  get selection(): CodeSelection;
69
84
  set selection(value: CodeSelection);
85
+ /** One-based line containing a UTF-16 document offset, including before mount. */
86
+ lineAt(position: number): CodeLine;
87
+ /** Start offset of a one-based line; out-of-range lines clamp to the document. */
88
+ lineStart(number: number): number;
70
89
  revealLine(line: number, options?: {
71
90
  center?: boolean;
72
91
  }): void;