@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
@@ -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;
@@ -138,6 +175,19 @@ export class CodeEditor extends BaseElement {
138
175
  scrollIntoView: true,
139
176
  });
140
177
  }
178
+ /** One-based line containing a UTF-16 document offset, including before mount. */
179
+ lineAt(position) {
180
+ const doc = this.view?.state.doc ?? EditorState.create({ doc: this.value }).doc;
181
+ const offset = Math.max(0, Math.min(doc.length, Number.isFinite(position) ? Math.floor(position) : 0));
182
+ const { number, from, to } = doc.lineAt(offset);
183
+ return { number, from, to };
184
+ }
185
+ /** Start offset of a one-based line; out-of-range lines clamp to the document. */
186
+ lineStart(number) {
187
+ const doc = this.view?.state.doc ?? EditorState.create({ doc: this.value }).doc;
188
+ const line = Math.max(1, Math.min(doc.lines, Number.isFinite(number) ? Math.floor(number) : 1));
189
+ return doc.line(line).from;
190
+ }
141
191
  revealLine(line, options = {}) {
142
192
  if (!this.view)
143
193
  return;
@@ -166,16 +216,24 @@ export class CodeEditor extends BaseElement {
166
216
  }
167
217
  renderTemplate() {
168
218
  this.shadowRoot.innerHTML = `<style>
169
- :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}
170
220
  :host([hidden]){display:none!important}
171
- [part=editor]{border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:8px;overflow:hidden}
172
- .cm-editor{background:var(--boe-token-surface-surface,#fff);color:var(--boe-token-text-text,#222)}
221
+ [part=editor]{border:1px solid var(--boe-token-stroke-stroke,#ddd);border-radius:8px;overflow:hidden;min-height:0;flex:1 1 auto}
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))}
173
223
  [part=editor]:focus-within{box-shadow:0 0 0 3px var(--boe-token-surface-surface-brand,#0061d5)}
174
224
  .cm-editor.cm-focused{outline:none}
175
- .cm-scroller{max-height:var(--boe-code-editor-height,420px);min-height:160px;overflow:auto;font-family:monospace;font-size:14px;line-height:1.6}
176
- .cm-gutters,.cm-panels,.cm-tooltip{background:var(--boe-token-surface-surface-secondary,#fbfbfb)!important;color:var(--boe-token-text-text,#222)!important;border-color:var(--boe-token-stroke-stroke,#ddd)!important}
177
- .cm-cursor{border-left-color:var(--boe-token-text-text,#222)}
178
- .cm-activeLine,.cm-activeLineGutter,.cm-selectionBackground{background:color-mix(in srgb,var(--boe-token-surface-surface-brand,#0061d5) 12%,transparent)!important}
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}
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}
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}
230
+ .cm-cursor{border-left-color:var(--boe-code-caret,var(--boe-token-text-text,#222))}
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}
235
+ .cm-activeLineGutter{color:var(--boe-code-gutter-active,var(--boe-code-gutter,var(--boe-token-text-text,#222)))!important}
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}
179
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)}
180
238
  .cm-matchingBracket,.cm-selectionMatch{background:color-mix(in srgb,var(--boe-token-surface-surface-brand,#0061d5) 15%,transparent)!important;color:inherit!important}
181
239
  .cm-nonmatchingBracket{color:var(--boe-token-text-status-text-error,#b92340)!important}
@@ -190,6 +248,7 @@ export class CodeEditor extends BaseElement {
190
248
  .cm-lint-marker-warning{background-image:radial-gradient(circle,var(--boe-token-text-status-text-warning,#9a6500) 45%,transparent 50%)!important}
191
249
  .cm-lint-marker-info{background-image:radial-gradient(circle,var(--boe-token-surface-surface-brand,#0061d5) 45%,transparent 50%)!important}
192
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}
193
252
  [part=toolbar]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
194
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}
195
254
  button:focus-visible{outline:3px solid var(--boe-token-surface-surface-brand,#0061d5);outline-offset:2px}
@@ -281,45 +340,49 @@ export class CodeEditor extends BaseElement {
281
340
  ]),
282
341
  this.completionsConfig.of(this.completionExtension()),
283
342
  this.wrapConfig.of(this.wrap ? EditorView.lineWrapping : []),
343
+ this.wrapIndentConfig.of(this.wrapIndentExtension()),
284
344
  this.highlightsConfig.of(this.highlightExtension()),
285
345
  syntaxHighlighting(HighlightStyle.define([
286
346
  {
287
- tag: [
288
- tags.variableName,
289
- tags.definition(tags.variableName),
290
- tags.propertyName,
291
- tags.operator,
292
- tags.punctuation,
293
- ],
294
- color: "var(--boe-token-text-text,#222)",
347
+ tag: tags.controlKeyword,
348
+ color: "var(--boe-code-control,var(--boe-code-keyword,var(--boe-token-surface-surface-brand,#0061d5)))",
295
349
  },
296
350
  {
297
- tag: tags.keyword,
298
- color: "var(--boe-token-surface-surface-brand,#0061d5)",
351
+ tag: [tags.keyword, tags.moduleKeyword, tags.definitionKeyword, tags.operatorKeyword],
352
+ color: "var(--boe-code-keyword,var(--boe-token-surface-surface-brand,#0061d5))",
299
353
  },
300
354
  {
301
- tag: [
302
- tags.typeName,
303
- tags.className,
304
- tags.function(tags.variableName),
305
- ],
306
- color: "var(--boe-token-surface-surface-brand,#0061d5)",
355
+ tag: [tags.function(tags.variableName), tags.function(tags.propertyName)],
356
+ color: "var(--boe-code-function,var(--boe-token-surface-surface-brand,#0061d5))",
307
357
  },
308
358
  {
309
- tag: tags.string,
310
- color: "var(--boe-token-text-status-text-success,#138a58)",
359
+ tag: [tags.typeName, tags.className, tags.namespace],
360
+ color: "var(--boe-code-type,var(--boe-token-surface-surface-brand,#0061d5))",
311
361
  },
312
362
  {
313
- tag: tags.comment,
314
- color: "var(--boe-token-text-text-secondary,#666)",
363
+ tag: tags.propertyName,
364
+ color: "var(--boe-code-property,var(--boe-token-text-text,#222))",
365
+ },
366
+ {
367
+ tag: [tags.variableName, tags.definition(tags.variableName)],
368
+ color: "var(--boe-code-variable,var(--boe-token-text-text,#222))",
369
+ },
370
+ {
371
+ tag: [tags.string, tags.special(tags.string)],
372
+ color: "var(--boe-code-string,var(--boe-token-text-status-text-success,#138a58))",
373
+ },
374
+ {
375
+ tag: [tags.number, tags.bool, tags.null, tags.atom],
376
+ color: "var(--boe-code-number,var(--boe-token-text-status-text-warning,#9a6500))",
315
377
  },
316
378
  {
317
- tag: tags.number,
318
- color: "var(--boe-token-text-status-text-warning,#9a6500)",
379
+ tag: tags.comment,
380
+ color: "var(--boe-code-comment,var(--boe-token-text-text-secondary,#666))",
381
+ fontStyle: "var(--boe-code-comment-style,normal)",
319
382
  },
320
383
  {
321
- tag: [tags.bool, tags.null, tags.atom],
322
- color: "var(--boe-token-text-status-text-warning,#9a6500)",
384
+ tag: [tags.punctuation, tags.bracket, tags.operator, tags.separator],
385
+ color: "var(--boe-code-punctuation,var(--boe-token-text-text,#222))",
323
386
  },
324
387
  ])),
325
388
  EditorView.updateListener.of((update) => {
@@ -369,6 +432,7 @@ export class CodeEditor extends BaseElement {
369
432
  ]),
370
433
  this.completionsConfig.reconfigure(this.completionExtension()),
371
434
  this.wrapConfig.reconfigure(this.wrap ? EditorView.lineWrapping : []),
435
+ this.wrapIndentConfig.reconfigure(this.wrapIndentExtension()),
372
436
  this.highlightsConfig.reconfigure(this.highlightExtension()),
373
437
  ],
374
438
  });
@@ -379,7 +443,12 @@ export class CodeEditor extends BaseElement {
379
443
  }
380
444
  this.view.contentDOM.setAttribute("aria-label", this.label);
381
445
  this.view.contentDOM.setAttribute("tabindex", "0");
382
- 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;
383
452
  this.syncProblems();
384
453
  }
385
454
  flushChange() {
@@ -20,6 +20,7 @@ export declare class Combobox extends FormAssociatedElement {
20
20
  private listboxId;
21
21
  private inputEl;
22
22
  private labelEl;
23
+ private descriptionEl;
23
24
  private controlEl;
24
25
  private listboxEl;
25
26
  private errorEl;
@@ -1,4 +1,4 @@
1
- import { FormAssociatedElement, boeFormFieldErrorStyles, formErrorMessageMarkup, } from "../../core/index.js";
1
+ import { FormAssociatedElement, FORM_LABEL_ID, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDescriptionMarkup, formErrorMessageMarkup, } from "../../core/index.js";
2
2
  import { boeNeutralInteractiveStyles } from "../../foundations/tokens/interaction.js";
3
3
  import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
4
4
  import { boeControl, boeOverlay, boeRadius } from "../../foundations/geometry/index.js";
@@ -137,12 +137,13 @@ const comboboxStyles = `
137
137
  }
138
138
 
139
139
  ${boeFormFieldErrorStyles}
140
+ ${boeFormFieldSupportStyles}
140
141
  `;
141
142
  export class Combobox extends FormAssociatedElement {
142
143
  static tagName = DEFAULT_TAG_NAME;
143
144
  static get observedAttributes() {
144
145
  return [
145
- ...FormAssociatedElement.formObservedAttributes,
146
+ ...FormAssociatedElement.fieldObservedAttributes,
146
147
  "disabled",
147
148
  "label",
148
149
  "options",
@@ -159,6 +160,7 @@ export class Combobox extends FormAssociatedElement {
159
160
  listboxId = `box-combobox-list-${Math.random().toString(36).slice(2, 10)}`;
160
161
  inputEl;
161
162
  labelEl;
163
+ descriptionEl;
162
164
  controlEl;
163
165
  listboxEl;
164
166
  errorEl;
@@ -372,7 +374,7 @@ export class Combobox extends FormAssociatedElement {
372
374
  this.shadowRoot.innerHTML = `
373
375
  <style>${comboboxStyles}</style>
374
376
  <div part="field">
375
- <span part="label" id="${this.listboxId}-label"></span>
377
+ <span part="label" id="${FORM_LABEL_ID}"></span>
376
378
  <div part="control">
377
379
  <input
378
380
  type="text"
@@ -384,11 +386,13 @@ export class Combobox extends FormAssociatedElement {
384
386
  aria-controls="${this.listboxId}"
385
387
  />
386
388
  </div>
387
- <ul part="listbox" id="${this.listboxId}" role="listbox" aria-labelledby="${this.listboxId}-label" hidden></ul>
389
+ ${formDescriptionMarkup()}
390
+ <ul part="listbox" id="${this.listboxId}" role="listbox" aria-labelledby="${FORM_LABEL_ID}" hidden></ul>
388
391
  ${formErrorMessageMarkup()}
389
392
  </div>
390
393
  `;
391
394
  this.labelEl = this.shadowRoot.querySelector('[part="label"]');
395
+ this.descriptionEl = this.shadowRoot.querySelector('[part="description"]');
392
396
  this.inputEl = this.shadowRoot.querySelector('[part="input"]');
393
397
  this.controlEl = this.shadowRoot.querySelector('[part="control"]');
394
398
  this.listboxEl = this.shadowRoot.querySelector('[part="listbox"]');
@@ -511,7 +515,7 @@ export class Combobox extends FormAssociatedElement {
511
515
  }
512
516
  this.labelEl.textContent = this.label;
513
517
  this.inputEl.placeholder = this.placeholder;
514
- this.inputEl.setAttribute("aria-label", this.label);
518
+ this.applyFieldSupport(this.labelEl, this.inputEl, this.descriptionEl);
515
519
  if (this.shadowRoot?.activeElement !== this.inputEl && this.filterText == null) {
516
520
  this.inputEl.value = this.getDisplayValue();
517
521
  }
@@ -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
  }
@@ -11,6 +11,9 @@ export declare class Accordion extends BaseElement {
11
11
  /** Flat variant with no outer card border/background. */
12
12
  get borderless(): boolean;
13
13
  set borderless(value: boolean);
14
+ /** Omit region landmarks when many accordions or panels share a label. */
15
+ get plainPanels(): boolean;
16
+ set plainPanels(value: boolean);
14
17
  private valueInternal;
15
18
  private valuesInternal;
16
19
  get multiple(): boolean;
@@ -107,7 +107,7 @@ const accordionStyles = `
107
107
  export class Accordion extends BaseElement {
108
108
  static tagName = DEFAULT_TAG_NAME;
109
109
  static get observedAttributes() {
110
- return ["items", "label", "value", "values", "multiple", "borderless"];
110
+ return ["items", "label", "value", "values", "multiple", "borderless", "plain-panels"];
111
111
  }
112
112
  /** Flat variant with no outer card border/background. */
113
113
  get borderless() {
@@ -116,6 +116,9 @@ export class Accordion extends BaseElement {
116
116
  set borderless(value) {
117
117
  this.toggleAttribute("borderless", Boolean(value));
118
118
  }
119
+ /** Omit region landmarks when many accordions or panels share a label. */
120
+ get plainPanels() { return this.hasAttribute("plain-panels"); }
121
+ set plainPanels(value) { this.toggleAttribute("plain-panels", Boolean(value)); }
119
122
  valueInternal = "";
120
123
  valuesInternal = [];
121
124
  get multiple() { return this.hasAttribute("multiple"); }
@@ -196,8 +199,7 @@ export class Accordion extends BaseElement {
196
199
  <div
197
200
  part="panel"
198
201
  id="${panelId}"
199
- role="region"
200
- aria-labelledby="${triggerId}"
202
+ ${this.plainPanels ? "" : `role="region" aria-labelledby="${triggerId}"`}
201
203
  ${isOpen ? "" : "hidden"}
202
204
  ><slot name="panel-${escapeHtml(item.value)}">${escapeHtml(item.content ?? "")}</slot></div>
203
205
  </section>
@@ -261,8 +263,14 @@ export class Accordion extends BaseElement {
261
263
  this.valueInternal = items[0].value;
262
264
  this.setAttribute("value", this.valueInternal);
263
265
  }
264
- this.accordionEl.setAttribute("role", "region");
265
- this.accordionEl.setAttribute("aria-label", this.label);
266
+ if (this.plainPanels) {
267
+ this.accordionEl.removeAttribute("role");
268
+ this.accordionEl.removeAttribute("aria-label");
269
+ }
270
+ else {
271
+ this.accordionEl.setAttribute("role", "region");
272
+ this.accordionEl.setAttribute("aria-label", this.label);
273
+ }
266
274
  if (itemsJson !== this.lastItemsJson) {
267
275
  this.accordionEl.innerHTML = this.renderItemsMarkup(items);
268
276
  this.lastItemsJson = itemsJson;
@@ -284,6 +292,14 @@ export class Accordion extends BaseElement {
284
292
  }
285
293
  if (panel) {
286
294
  panel.hidden = !isOpen;
295
+ if (this.plainPanels) {
296
+ panel.removeAttribute("role");
297
+ panel.removeAttribute("aria-labelledby");
298
+ }
299
+ else {
300
+ panel.setAttribute("role", "region");
301
+ panel.setAttribute("aria-labelledby", trigger?.id ?? "");
302
+ }
287
303
  }
288
304
  });
289
305
  }