@unofficialbox/box-open-elements 0.20.0 → 0.21.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 (57) hide show
  1. package/dist/components/actions/menu-item.d.ts +2 -0
  2. package/dist/components/actions/menu-item.js +4 -0
  3. package/dist/components/collections/card.js +6 -0
  4. package/dist/components/collections/result-blocks.js +4 -1
  5. package/dist/components/feedback/alert.d.ts +2 -0
  6. package/dist/components/feedback/alert.js +17 -13
  7. package/dist/components/feedback/toast.d.ts +10 -1
  8. package/dist/components/feedback/toast.js +69 -27
  9. package/dist/components/forms/number-input.d.ts +1 -0
  10. package/dist/components/forms/number-input.js +9 -6
  11. package/dist/components/forms/search-field.js +16 -0
  12. package/dist/components/overlays/context-menu.d.ts +2 -0
  13. package/dist/components/overlays/context-menu.js +7 -0
  14. package/dist/components/overlays/dialog.js +19 -3
  15. package/dist/components/overlays/popover.js +6 -1
  16. package/dist/element-maps.d.ts +11 -1
  17. package/dist/foundations/a11y/announcer.d.ts +12 -0
  18. package/dist/foundations/a11y/announcer.js +75 -0
  19. package/dist/foundations/a11y/focus.js +46 -25
  20. package/dist/foundations/a11y/index.d.ts +2 -0
  21. package/dist/foundations/a11y/index.js +1 -0
  22. package/dist/foundations/icons/box-iconography.d.ts +472 -472
  23. package/dist/foundations/icons/box-iconography.generated.d.ts +1151 -679
  24. package/dist/foundations/icons/box-iconography.generated.js +1006 -3588
  25. package/dist/foundations/icons/glyphs/index.d.ts +1 -0
  26. package/dist/foundations/icons/glyphs/index.js +2 -0
  27. package/dist/foundations/icons/glyphs/package.json +1 -0
  28. package/dist/foundations/overlay/positioning.js +7 -0
  29. package/dist/foundations/status/index.d.ts +1 -1
  30. package/dist/foundations/status/index.js +8 -6
  31. package/dist/foundations/tokens/box-dark.js +4 -1
  32. package/dist/foundations/tokens/box-defaults.js +3 -0
  33. package/dist/foundations/tokens/registry.d.ts +3 -0
  34. package/dist/foundations/tokens/registry.js +3 -0
  35. package/dist/foundations/tokens/types.d.ts +3 -0
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.js +2 -0
  38. package/dist/patterns/agent-chat/agent-chat.js +12 -4
  39. package/dist/patterns/call-console/call-console.d.ts +3 -0
  40. package/dist/patterns/call-console/call-console.js +114 -20
  41. package/dist/patterns/content-explorer/adapters/action-menu.js +7 -5
  42. package/dist/patterns/content-explorer/adapters/toolbar.js +6 -2
  43. package/dist/patterns/file-request/file-request-builder.js +3 -0
  44. package/dist/patterns/flow-builder/flow-builder.d.ts +48 -0
  45. package/dist/patterns/flow-builder/flow-builder.js +200 -0
  46. package/dist/patterns/flow-builder/index.d.ts +3 -0
  47. package/dist/patterns/flow-builder/index.js +3 -0
  48. package/dist/patterns/flow-builder/model.d.ts +33 -0
  49. package/dist/patterns/flow-builder/model.js +28 -0
  50. package/dist/patterns/flow-builder/primitives.d.ts +47 -0
  51. package/dist/patterns/flow-builder/primitives.js +192 -0
  52. package/dist/patterns/lineage/lineage-graph.js +26 -6
  53. package/dist/patterns/notifications/notification-inbox.js +19 -3
  54. package/dist/patterns/preview/annotation-inspector.js +2 -2
  55. package/dist/patterns/undo/index.d.ts +19 -0
  56. package/dist/patterns/undo/index.js +106 -0
  57. package/package.json +11 -2
@@ -3,6 +3,8 @@ export declare class MenuItem extends BaseElement {
3
3
  static readonly tagName: string;
4
4
  static get observedAttributes(): string[];
5
5
  private itemEl;
6
+ /** Let composed menus move focus without reaching into the shadow root. */
7
+ focus(options?: FocusOptions): void;
6
8
  get disabled(): boolean;
7
9
  set disabled(value: boolean);
8
10
  get label(): string;
@@ -67,6 +67,10 @@ export class MenuItem extends BaseElement {
67
67
  return ["disabled", "label", "selected", "value"];
68
68
  }
69
69
  itemEl;
70
+ /** Let composed menus move focus without reaching into the shadow root. */
71
+ focus(options) {
72
+ this.itemEl?.focus(options);
73
+ }
70
74
  get disabled() {
71
75
  return this.hasAttribute("disabled");
72
76
  }
@@ -25,6 +25,11 @@ const cardStyles = `
25
25
  0 14px 28px rgba(15, 23, 42, 0.04);
26
26
  }
27
27
 
28
+ [part="header"],
29
+ [part="body"] {
30
+ min-width: 0;
31
+ }
32
+
28
33
  [part="header"] {
29
34
  display: grid;
30
35
  gap: 0.35rem;
@@ -48,6 +53,7 @@ const cardStyles = `
48
53
  }
49
54
 
50
55
  [part="body"] {
56
+ overflow-x: auto;
51
57
  color: var(--boe-token-text-text-secondary, #6f6f6f);
52
58
  line-height: 1.55;
53
59
  }
@@ -25,12 +25,15 @@ export const resultBlockStyles = `
25
25
  :host([hidden]){display:none !important} *{box-sizing:border-box} h3{font-size:1em;margin:0 0 .5rem} dl,ul{padding:0;margin:0;list-style:none}
26
26
  [part="row"]{border-bottom:1px solid var(--boe-token-stroke-stroke,#ddd);padding:.7rem 0;display:flex;gap:.6rem;align-items:baseline}
27
27
  dt{color:var(--boe-token-text-text-secondary,#666)} dd{margin:0 0 0 auto;font-variant-numeric:tabular-nums}
28
+ [part="facts"] [part="row"]{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);overflow-wrap:anywhere}
29
+ [part="facts"] dd{margin:0;text-align:end}
30
+ @container boe-result-block (max-width:28rem){[part="facts"] [part="row"]{grid-template-columns:minmax(0,1fr);gap:.25rem}[part="facts"] dd{text-align:start}}
28
31
  p{margin:.25rem 0;color:var(--boe-token-text-text-secondary,#666)} small{display:block;font-weight:normal;color:var(--boe-token-text-text-secondary,#666)}
29
32
  [part="verdict"]{margin-left:auto;text-align:right} [data-status="warn"]{color:var(--boe-token-text-status-text-warning,#946400)} [data-status="fail"]{color:var(--boe-token-text-status-text-error,#c52a46)}
30
33
  [part="document"]{display:flex;gap:.6rem;align-items:center;width:100%;text-align:left;color:inherit;font:inherit;background:none;border:0;text-decoration:none;cursor:pointer}
31
34
  [part="table-frame"]{overflow:auto;max-width:100%} table{border-collapse:collapse;min-width:100%;font-variant-numeric:tabular-nums} td,th{padding:.7rem;text-align:left;border-bottom:1px solid var(--boe-token-stroke-stroke,#ddd)} td:last-child{font-weight:600;background:var(--boe-token-surface-surface-secondary,#f7f7f7)}
32
35
  .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
33
- [part="block"]{${boeEntrance()}${boeStaggerStyles};margin-block:1rem;min-width:0}
36
+ [part="block"]{${boeEntrance()}${boeStaggerStyles};margin-block:1rem;min-width:0;container:boe-result-block / inline-size}
34
37
  ${boeEntranceKeyframes}${boeStatusStyles}${boeReducedMotionPolicy}`;
35
38
  const DEFAULT_TAG_NAME = "box-result-blocks";
36
39
  export class ResultBlocks extends BaseElement {
@@ -22,6 +22,8 @@ export declare class Alert extends BaseElement {
22
22
  get tone(): string;
23
23
  set tone(value: string);
24
24
  connectedCallback(): void;
25
+ private announcement;
26
+ disconnectedCallback(): void;
25
27
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
26
28
  dismiss(): void;
27
29
  protected renderTemplate(): void;
@@ -1,4 +1,5 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
+ import { FeedbackAnnouncement } from "../../foundations/a11y/announcer.js";
2
3
  import { toneAccessibleLabel, toneIcon } from "./tone.js";
3
4
  import { boeRadius, boeSpace } from "../../foundations/geometry/index.js";
4
5
  import { boeNeutralInteractiveStyles } from "../../foundations/tokens/index.js";
@@ -15,10 +16,8 @@ const alertStyles = `
15
16
  display: none !important;
16
17
  }
17
18
 
18
- /* The accent colours the glyph only. The tinted backgrounds below are pinned
19
- by the colour conformance manifest against upstream box-ui-elements and are
20
- deliberately not derived from it; upstream has no glyph, so adding one
21
- costs no conformance. */
19
+ /* Preserve upstream light tints, but mix against the active theme surface.
20
+ Glyphs use readable status ink, not decorative status fill colours. */
22
21
  [part="alert"] {
23
22
  --alert-accent: var(--boe-token-text-text-secondary, #6f6f6f);
24
23
 
@@ -36,23 +35,23 @@ const alertStyles = `
36
35
 
37
36
  [part="alert"][data-tone="info"] {
38
37
  --alert-accent: var(--boe-token-surface-surface-brand, #0061d5);
39
- background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 10%, #fff);
38
+ background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 10%, var(--boe-token-surface-surface, #ffffff));
40
39
  }
41
40
 
42
41
  [part="alert"][data-tone="success"] {
43
- --alert-accent: var(--boe-token-surface-status-surface-success, #26c281);
44
- background: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26c281) 10%, #fff);
42
+ --alert-accent: var(--boe-token-text-status-text-success, #187657);
43
+ background: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26c281) 10%, var(--boe-token-surface-surface, #ffffff));
45
44
  }
46
45
 
47
46
  [part="alert"][data-tone="error"] {
48
- --alert-accent: var(--boe-token-surface-status-surface-error, #ed3757);
49
- background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 10%, #fff);
47
+ --alert-accent: var(--boe-token-text-status-text-error, #b92340);
48
+ background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 10%, var(--boe-token-surface-surface, #ffffff));
50
49
  }
51
50
 
52
51
  [part="alert"][data-tone="warning"],
53
52
  [part="alert"][data-tone="inprogress"] {
54
- --alert-accent: var(--boe-token-surface-status-surface-inprogress, #f5b31b);
55
- background: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 10%, #fff);
53
+ --alert-accent: var(--boe-token-text-status-text-warning, #805600);
54
+ background: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 10%, var(--boe-token-surface-surface, #ffffff));
56
55
  }
57
56
 
58
57
  [part="icon"] {
@@ -202,9 +201,12 @@ export class Alert extends BaseElement {
202
201
  this.setAttribute("tone", value);
203
202
  }
204
203
  connectedCallback() {
205
- this.openValue = this.hasAttribute("open") || !this.hasAttribute("open");
206
204
  super.connectedCallback();
207
205
  }
206
+ announcement = new FeedbackAnnouncement();
207
+ disconnectedCallback() {
208
+ this.announcement.reset();
209
+ }
208
210
  attributeChangedCallback(name, oldValue, newValue) {
209
211
  if (name === "open") {
210
212
  this.openValue = this.hasAttribute("open");
@@ -224,7 +226,7 @@ export class Alert extends BaseElement {
224
226
  }
225
227
  this.shadowRoot.innerHTML = `
226
228
  <style>${alertStyles}</style>
227
- <div part="alert" role="status" aria-live="polite">
229
+ <div part="alert" role="group">
228
230
  <span part="icon" aria-hidden="true"></span>
229
231
  <div part="content">
230
232
  <span part="tone-label" class="sr-only"></span>
@@ -263,6 +265,8 @@ export class Alert extends BaseElement {
263
265
  const hasRich = this.hasRichContent();
264
266
  const visible = this.openValue && Boolean(this.heading || this.message || hasRich);
265
267
  this.hidden = !visible;
268
+ const richText = this.richSlot.assignedNodes({ flatten: true }).map(node => node.textContent ?? "").join(" ").trim();
269
+ this.announcement.update(this, visible ? [toneAccessibleLabel(this.tone), this.heading, this.message, richText].filter(Boolean).join(": ") : "", this.tone === "error" ? "assertive" : "polite");
266
270
  this.richSlot.classList.toggle("has-content", hasRich);
267
271
  if (!visible) {
268
272
  return;
@@ -2,7 +2,8 @@ import { BaseElement } from "../../core/index.js";
2
2
  /**
3
3
  * Whether the toast clears itself.
4
4
  *
5
- * `dismissible` auto-dismisses after `duration` and can also be closed by hand;
5
+ * `dismissible` auto-dismisses non-error feedback after `duration`;
6
+ * errors always persist until explicitly dismissed or replaced.
6
7
  * `sticky` stays until the reader closes it, whatever `duration` says. Both
7
8
  * carry the close control — a toast the reader cannot get rid of is a trap.
8
9
  */
@@ -14,6 +15,10 @@ export declare class Toast extends BaseElement {
14
15
  static get observedAttributes(): string[];
15
16
  private openValue;
16
17
  private timeoutId;
18
+ private remaining;
19
+ private deadline;
20
+ private holds;
21
+ private announcement;
17
22
  private toastEl;
18
23
  private iconEl;
19
24
  private headingEl;
@@ -28,6 +33,9 @@ export declare class Toast extends BaseElement {
28
33
  get open(): boolean;
29
34
  set open(value: boolean);
30
35
  private scheduleAutoDismiss;
36
+ private resumeTimer;
37
+ private hold;
38
+ private dismiss;
31
39
  get message(): string;
32
40
  set message(value: string);
33
41
  /**
@@ -56,6 +64,7 @@ export declare class Toast extends BaseElement {
56
64
  get tone(): string;
57
65
  set tone(value: string);
58
66
  disconnectedCallback(): void;
67
+ connectedCallback(): void;
59
68
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
60
69
  show(message?: string, options?: {
61
70
  duration?: number;
@@ -1,4 +1,5 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
+ import { FeedbackAnnouncement } from "../../foundations/a11y/announcer.js";
2
3
  import { toneAccessibleLabel, toneIcon } from "./tone.js";
3
4
  import { boeRadius, boeSpace } from "../../foundations/geometry/index.js";
4
5
  import { boeFocusVisibleStyles } from "../../foundations/tokens/index.js";
@@ -21,18 +22,8 @@ const toastStyles = `
21
22
  display: none !important;
22
23
  }
23
24
 
24
- /* Fill, text colour and shadow track box-ui-elements' .notification and are
25
- pinned by the colour conformance manifest — they are deliberately not
26
- derived from the tone accent. The --toast-accent property exists only to
27
- colour the status glyph, which upstream has no equivalent for, so adding it
28
- costs no conformance.
29
-
30
- There is no border. Upstream .notification has one, and box-open-elements
31
- matched it until the outline was judged too heavy; the claim that pinned it
32
- was retired rather than left describing something no longer painted. Tone
33
- survives that in three places — the tinted fill, the full-strength glyph,
34
- and the visually-hidden tone label — so it was never carried by the border
35
- alone. */
25
+ /* Preserve upstream light tints while deriving dark fills from the active
26
+ surface. Status ink, tinted fill and a text label communicate the tone. */
36
27
  [part="toast"] {
37
28
  --toast-accent: var(--boe-token-text-text, #222222);
38
29
 
@@ -57,19 +48,19 @@ const toastStyles = `
57
48
  }
58
49
 
59
50
  [part="toast"][data-tone="success"] {
60
- --toast-accent: var(--boe-token-surface-status-surface-success, #26c281);
61
- background: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26c281) 20%, #fff);
51
+ --toast-accent: var(--boe-token-text-status-text-success, #187657);
52
+ background: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26c281) 20%, var(--boe-token-surface-surface, #ffffff));
62
53
  }
63
54
 
64
55
  [part="toast"][data-tone="error"] {
65
- --toast-accent: var(--boe-token-surface-status-surface-error, #ed3757);
66
- background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 20%, #fff);
56
+ --toast-accent: var(--boe-token-text-status-text-error, #b92340);
57
+ background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 20%, var(--boe-token-surface-surface, #ffffff));
67
58
  }
68
59
 
69
60
  [part="toast"][data-tone="warning"],
70
61
  [part="toast"][data-tone="inprogress"] {
71
- --toast-accent: var(--boe-token-surface-status-surface-inprogress, #f5b31b);
72
- background: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 20%, #fff);
62
+ --toast-accent: var(--boe-token-text-status-text-warning, #805600);
63
+ background: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 20%, var(--boe-token-surface-surface, #ffffff));
73
64
  }
74
65
 
75
66
  .sr-only {
@@ -88,8 +79,7 @@ const toastStyles = `
88
79
  flex: 0 0 auto;
89
80
  inline-size: 20px;
90
81
  block-size: 20px;
91
- /* The accent at full strength: the glyph is the one element that should
92
- read as the status colour rather than a darkened version of it. */
82
+ /* Readable status ink is separate from the decorative fill colour. */
93
83
  color: var(--toast-accent);
94
84
  }
95
85
 
@@ -205,6 +195,10 @@ export class Toast extends BaseElement {
205
195
  }
206
196
  openValue = false;
207
197
  timeoutId = null;
198
+ remaining = 0;
199
+ deadline = 0;
200
+ holds = new Set();
201
+ announcement = new FeedbackAnnouncement();
208
202
  toastEl;
209
203
  iconEl;
210
204
  headingEl;
@@ -255,11 +249,36 @@ export class Toast extends BaseElement {
255
249
  clearTimeout(this.timeoutId);
256
250
  this.timeoutId = null;
257
251
  }
258
- // Sticky wins over any duration, including one passed to show().
259
- if (duration > 0 && this.mode !== "sticky") {
260
- this.timeoutId = setTimeout(() => this.hide(), duration);
252
+ this.remaining = Number.isFinite(duration) ? Math.max(0, duration) : 0;
253
+ this.resumeTimer();
254
+ }
255
+ resumeTimer() {
256
+ if (!this.isConnected || !this.open || this.holds.size || this.remaining <= 0 || this.mode === "sticky" || this.tone === "error")
257
+ return;
258
+ this.deadline = Date.now() + this.remaining;
259
+ this.timeoutId = setTimeout(() => {
260
+ this.timeoutId = null;
261
+ this.remaining = 0;
262
+ this.dismiss("timeout");
263
+ }, this.remaining);
264
+ }
265
+ hold(reason, active) {
266
+ if (active) {
267
+ this.holds.add(reason);
268
+ if (this.timeoutId !== null) {
269
+ this.remaining = Math.max(0, this.deadline - Date.now());
270
+ clearTimeout(this.timeoutId);
271
+ this.timeoutId = null;
272
+ }
273
+ }
274
+ else if (this.holds.delete(reason) && !this.holds.size) {
275
+ this.resumeTimer();
261
276
  }
262
277
  }
278
+ dismiss(source) {
279
+ this.hide();
280
+ this.dispatchEvent(new CustomEvent("dismiss", { bubbles: true, composed: true, detail: { source } }));
281
+ }
263
282
  get message() {
264
283
  return this.getAttribute("message") ?? "";
265
284
  }
@@ -308,16 +327,26 @@ export class Toast extends BaseElement {
308
327
  this.setAttribute("tone", value);
309
328
  }
310
329
  disconnectedCallback() {
330
+ this.announcement.reset();
331
+ this.holds.clear();
311
332
  if (this.timeoutId) {
333
+ this.remaining = Math.max(0, this.deadline - Date.now());
312
334
  clearTimeout(this.timeoutId);
313
335
  this.timeoutId = null;
314
336
  }
315
337
  }
338
+ connectedCallback() {
339
+ super.connectedCallback();
340
+ this.resumeTimer();
341
+ }
316
342
  attributeChangedCallback(name, oldValue, newValue) {
317
343
  if (name === "open") {
318
344
  this.openValue = this.hasAttribute("open");
319
345
  }
320
346
  super.attributeChangedCallback(name, oldValue, newValue);
347
+ if (oldValue !== newValue && ["open", "duration", "mode", "tone"].includes(name)) {
348
+ this.scheduleAutoDismiss(this.open ? this.duration : 0);
349
+ }
321
350
  }
322
351
  show(message, options) {
323
352
  if (typeof message === "string") {
@@ -328,8 +357,12 @@ export class Toast extends BaseElement {
328
357
  }
329
358
  this.open = true;
330
359
  // options.duration wins; else a declarative `duration` attribute; else 2500.
331
- const duration = options?.duration ?? (this.duration || 2500);
360
+ const duration = options?.duration ?? (this.hasAttribute("duration") ? this.duration : 2500);
332
361
  this.scheduleAutoDismiss(duration);
362
+ // Re-showing the same text is a new feedback event, even when already open.
363
+ this.announcement.reset();
364
+ if (this.isRendered)
365
+ this.update();
333
366
  }
334
367
  hide() {
335
368
  this.open = false;
@@ -340,7 +373,7 @@ export class Toast extends BaseElement {
340
373
  }
341
374
  this.shadowRoot.innerHTML = `
342
375
  <style>${toastStyles}</style>
343
- <div part="toast" role="status" aria-live="polite">
376
+ <div part="toast" role="group" aria-label="Notification">
344
377
  <span part="icon" aria-hidden="true"></span>
345
378
  <div part="content">
346
379
  <span part="tone-label" class="sr-only"></span>
@@ -360,9 +393,17 @@ export class Toast extends BaseElement {
360
393
  this.dismissEl = this.shadowRoot.querySelector('[part="dismiss"]');
361
394
  }
362
395
  setupListeners() {
396
+ this.addEventListener("pointerenter", () => this.hold("pointer", true));
397
+ this.addEventListener("pointerleave", () => this.hold("pointer", false));
398
+ this.addEventListener("focusin", () => this.hold("focus", true));
399
+ this.addEventListener("focusout", event => {
400
+ const target = event.relatedTarget;
401
+ if (target instanceof Node && (this.contains(target) || this.shadowRoot?.contains(target)))
402
+ return;
403
+ this.hold("focus", false);
404
+ });
363
405
  this.dismissEl.addEventListener("click", () => {
364
- this.dispatchEvent(new CustomEvent("dismiss", { bubbles: true, composed: true }));
365
- this.hide();
406
+ this.dismiss("close-button");
366
407
  });
367
408
  this.actionSlot.addEventListener("slotchange", () => {
368
409
  const hasContent = this.actionSlot.assignedNodes({ flatten: true }).length > 0;
@@ -375,6 +416,7 @@ export class Toast extends BaseElement {
375
416
  }
376
417
  const visible = this.openValue && Boolean(this.message);
377
418
  this.hidden = !visible;
419
+ this.announcement.update(this, visible ? [toneAccessibleLabel(this.tone), this.heading, this.message].filter(Boolean).join(": ") : "", this.tone === "error" ? "assertive" : "polite");
378
420
  if (!visible) {
379
421
  return;
380
422
  }
@@ -7,6 +7,7 @@ export declare class NumberInput extends FormAssociatedElement {
7
7
  private inputEl;
8
8
  private labelEl;
9
9
  private errorEl;
10
+ private descriptionEl;
10
11
  get disabled(): boolean;
11
12
  set disabled(value: boolean);
12
13
  get label(): string;
@@ -1,7 +1,7 @@
1
- import { FormAssociatedElement, boeFormFieldErrorStyles, formErrorMessageMarkup, } from "../../core/index.js";
1
+ import { FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDescriptionMarkup, formErrorMessageMarkup, } from "../../core/index.js";
2
2
  import { boeNeutralInteractiveStyles } from "../../foundations/tokens/index.js";
3
3
  import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
4
- import { boeRadius } from "../../foundations/geometry/index.js";
4
+ import { boeControl, boeRadius } from "../../foundations/geometry/index.js";
5
5
  const DEFAULT_TAG_NAME = "box-number-input";
6
6
  const parseNumber = (value, fallback) => {
7
7
  if (value == null || value === "") {
@@ -29,11 +29,9 @@ const numberInputStyles = `
29
29
  }
30
30
 
31
31
  [part="label"] {
32
- font-size: 0.8rem;
32
+ font-size: ${boeControl.fontSize};
33
33
  font-weight: 700;
34
- letter-spacing: 0.08em;
35
- text-transform: uppercase;
36
- color: var(--boe-token-text-text-secondary, #6f6f6f);
34
+ color: var(--boe-token-text-text, #222222);
37
35
  }
38
36
 
39
37
  [part="input"] {
@@ -60,6 +58,7 @@ const numberInputStyles = `
60
58
  }
61
59
 
62
60
  ${boeFormFieldErrorStyles}
61
+ ${boeFormFieldSupportStyles}
63
62
  `;
64
63
  export class NumberInput extends FormAssociatedElement {
65
64
  static tagName = DEFAULT_TAG_NAME;
@@ -79,6 +78,7 @@ export class NumberInput extends FormAssociatedElement {
79
78
  inputEl;
80
79
  labelEl;
81
80
  errorEl;
81
+ descriptionEl;
82
82
  get disabled() {
83
83
  return this.hasAttribute("disabled");
84
84
  }
@@ -194,6 +194,7 @@ export class NumberInput extends FormAssociatedElement {
194
194
  <style>${numberInputStyles}</style>
195
195
  <label part="field">
196
196
  <span part="label"></span>
197
+ ${formDescriptionMarkup()}
197
198
  <input type="number" part="input" />
198
199
  ${formErrorMessageMarkup()}
199
200
  </label>
@@ -201,6 +202,7 @@ export class NumberInput extends FormAssociatedElement {
201
202
  this.labelEl = this.shadowRoot.querySelector('[part="label"]');
202
203
  this.inputEl = this.shadowRoot.querySelector('[part="input"]');
203
204
  this.errorEl = this.shadowRoot.querySelector('[part="error-message"]');
205
+ this.descriptionEl = this.shadowRoot.querySelector('[part="description"]');
204
206
  }
205
207
  setupListeners() {
206
208
  this.inputEl.addEventListener("input", event => {
@@ -239,6 +241,7 @@ export class NumberInput extends FormAssociatedElement {
239
241
  else {
240
242
  this.inputEl.removeAttribute("disabled");
241
243
  }
244
+ this.applyFieldSupport(this.labelEl, this.inputEl, this.descriptionEl);
242
245
  this.applyInvalidState(this.inputEl, this.errorEl);
243
246
  }
244
247
  }
@@ -6,6 +6,11 @@ const searchSpinnerMarkup = '<span part="spinner" aria-hidden="true"></span>';
6
6
  const searchFieldStyles = `
7
7
  :host {
8
8
  display: block;
9
+ min-inline-size: 0;
10
+ container: boe-search-field / inline-size;
11
+ /* Inline-size containment otherwise contributes zero intrinsic width in
12
+ content-sized flex/grid rows. Explicit parent widths still take priority. */
13
+ contain-intrinsic-inline-size: 20rem;
9
14
  color: inherit;
10
15
  font: inherit;
11
16
  }
@@ -18,6 +23,7 @@ const searchFieldStyles = `
18
23
 
19
24
  [part="field"] {
20
25
  display: grid;
26
+ grid-template-columns: minmax(0, 1fr);
21
27
  gap: 0.45rem;
22
28
  }
23
29
 
@@ -116,6 +122,16 @@ const searchFieldStyles = `
116
122
  background: var(--boe-token-surface-surface-hover, #f4f4f4);
117
123
  }
118
124
 
125
+ @container boe-search-field (max-width: 22rem) {
126
+ [part="input-shell"] {
127
+ flex-wrap: wrap;
128
+ border-radius: ${boeRadius.control};
129
+ }
130
+ [part="input"] {
131
+ flex-basis: 100%;
132
+ }
133
+ }
134
+
119
135
  [part="submit"]:focus-visible,
120
136
  [part="clear"]:focus-visible {
121
137
  outline: none;
@@ -27,6 +27,8 @@ export declare class ContextMenu extends BaseElement {
27
27
  get disabled(): boolean;
28
28
  set disabled(value: boolean);
29
29
  get open(): boolean;
30
+ /** Open from an explicit keyboard/touch trigger without a synthetic event. */
31
+ show(): void;
30
32
  disconnectedCallback(): void;
31
33
  protected renderTemplate(): void;
32
34
  protected setupListeners(): void;
@@ -139,6 +139,13 @@ export class ContextMenu extends BaseElement {
139
139
  get open() {
140
140
  return this.openValue;
141
141
  }
142
+ /** Open from an explicit keyboard/touch trigger without a synthetic event. */
143
+ show() {
144
+ if (!this.isConnected || !this.surfaceEl || this.disabled || !this.items.length || this.open)
145
+ return;
146
+ const rect = this.getBoundingClientRect();
147
+ this.openAt(rect.left, rect.bottom);
148
+ }
142
149
  disconnectedCallback() {
143
150
  this.unbindDocument();
144
151
  }
@@ -17,11 +17,13 @@ const dialogStyles = `
17
17
  max-* are reset because this element is the full-viewport scrim rather
18
18
  than a centred card. */
19
19
  [part="backdrop"] {
20
+ box-sizing: border-box;
20
21
  position: fixed;
21
22
  inset: 0;
22
23
  z-index: 1200;
23
24
  background: ${boeOverlay.modalBackdrop};
24
25
  display: grid;
26
+ grid-template-columns: minmax(0, 1fr);
25
27
  place-items: center;
26
28
  padding: ${boeOverlay.modalPadding};
27
29
  border: 0;
@@ -45,13 +47,18 @@ const dialogStyles = `
45
47
  }
46
48
 
47
49
  [part="dialog"] {
50
+ box-sizing: border-box;
51
+ min-width: 0;
52
+ max-width: 100%;
53
+ max-height: calc(100dvh - ${boeOverlay.modalPadding} - ${boeOverlay.modalPadding});
48
54
  width: min(${boeOverlay.modalWidth}, calc(100vw - 3rem));
49
55
  background: var(--boe-token-surface-surface, #ffffff);
50
56
  border: 0;
51
57
  border-radius: ${boeOverlay.modalRadius};
52
58
  box-shadow: ${boeOverlay.modalShadow};
53
59
  padding: ${boeOverlay.modalPadding};
54
- display: grid;
60
+ display: flex;
61
+ flex-direction: column;
55
62
  gap: ${boeSpace[5]};
56
63
  color: var(--boe-token-text-text, #222222);
57
64
  }
@@ -62,7 +69,7 @@ const dialogStyles = `
62
69
  [part="dialog"][data-size="fullscreen"] {
63
70
  width: calc(100vw - 2rem);
64
71
  height: calc(100vh - 2rem);
65
- max-width: none;
72
+ max-width: 100%;
66
73
  border-radius: ${boeRadius.large};
67
74
  }
68
75
 
@@ -86,6 +93,9 @@ const dialogStyles = `
86
93
  }
87
94
 
88
95
  [part="body"] {
96
+ min-height: 0;
97
+ overflow: auto;
98
+ overflow-wrap: anywhere;
89
99
  color: var(--boe-token-text-text, #222222);
90
100
  font-size: 14px;
91
101
  line-height: 20px;
@@ -93,6 +103,8 @@ const dialogStyles = `
93
103
 
94
104
  [part="footer"] {
95
105
  display: flex;
106
+ flex-shrink: 0;
107
+ flex-wrap: wrap;
96
108
  justify-content: end;
97
109
  gap: ${boeSpace[2]};
98
110
  margin-top: ${boeSpace[2]};
@@ -246,7 +258,11 @@ export class Dialog extends BaseElement {
246
258
  });
247
259
  this.hostEl.addEventListener("keydown", event => {
248
260
  const keyboardEvent = event;
249
- const dialog = keyboardEvent.target?.closest('[part="dialog"]');
261
+ if (keyboardEvent.defaultPrevented)
262
+ return;
263
+ // Slotted controls are not DOM descendants of the surface; their event
264
+ // path still crosses it. closest() misses keyboard events from slots.
265
+ const dialog = keyboardEvent.composedPath().find(node => node instanceof HTMLElement && node.getAttribute("part") === "dialog");
250
266
  if (!dialog || !this.hostEl.contains(dialog)) {
251
267
  return;
252
268
  }
@@ -64,13 +64,18 @@ const popoverStyles = `
64
64
  unavailable or fails, the surface still renders exactly as before and only
65
65
  the top-layer promotion is lost — see foundations/overlay/top-layer.ts. */
66
66
  [part="surface"] {
67
+ box-sizing: border-box;
68
+ margin: 0;
69
+ inset: auto;
67
70
  display: block;
68
71
  position: fixed;
69
72
  z-index: 30;
70
73
  inset-block-start: 0;
71
74
  inset-inline-start: 0;
72
75
  width: min(360px, calc(100vw - 5rem));
73
- min-width: 200px;
76
+ min-width: min(200px, calc(100vw - 1rem));
77
+ max-height: calc(100dvh - 1rem);
78
+ overflow: auto;
74
79
  padding: ${boeOverlay.padding};
75
80
  border: ${boeOverlay.border};
76
81
  border-radius: ${boeOverlay.radius};