@unofficialbox/box-open-elements 0.3.0 → 0.4.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 (58) hide show
  1. package/dist/components/actions/link-button.d.ts +9 -0
  2. package/dist/components/actions/link-button.js +55 -4
  3. package/dist/components/collections/carousel.d.ts +7 -0
  4. package/dist/components/collections/carousel.js +71 -4
  5. package/dist/components/collections/datalist-item.d.ts +6 -0
  6. package/dist/components/collections/datalist-item.js +39 -1
  7. package/dist/components/collections/draggable-list.js +1 -1
  8. package/dist/components/collections/grid-view.js +7 -3
  9. package/dist/components/feedback/alert.d.ts +2 -0
  10. package/dist/components/feedback/alert.js +30 -1
  11. package/dist/components/feedback/badge.d.ts +10 -0
  12. package/dist/components/feedback/badge.js +70 -4
  13. package/dist/components/feedback/chip.d.ts +5 -0
  14. package/dist/components/feedback/chip.js +74 -2
  15. package/dist/components/feedback/error-mask.d.ts +2 -0
  16. package/dist/components/feedback/error-mask.js +23 -0
  17. package/dist/components/feedback/help-text.js +7 -0
  18. package/dist/components/feedback/spinner.d.ts +4 -0
  19. package/dist/components/feedback/spinner.js +30 -2
  20. package/dist/components/feedback/toast.d.ts +5 -0
  21. package/dist/components/feedback/toast.js +59 -11
  22. package/dist/components/forms/category-selector.d.ts +11 -0
  23. package/dist/components/forms/category-selector.js +211 -17
  24. package/dist/components/forms/checkbox.d.ts +6 -0
  25. package/dist/components/forms/checkbox.js +58 -1
  26. package/dist/components/forms/combobox.d.ts +30 -1
  27. package/dist/components/forms/combobox.js +348 -44
  28. package/dist/components/forms/date-field.d.ts +7 -0
  29. package/dist/components/forms/date-field.js +73 -1
  30. package/dist/components/forms/dropdown.d.ts +5 -0
  31. package/dist/components/forms/dropdown.js +32 -3
  32. package/dist/components/forms/radio-group.d.ts +9 -8
  33. package/dist/components/forms/radio-group.js +43 -6
  34. package/dist/components/forms/search-field.d.ts +8 -0
  35. package/dist/components/forms/search-field.js +55 -13
  36. package/dist/components/forms/select.d.ts +18 -8
  37. package/dist/components/forms/select.js +104 -14
  38. package/dist/components/forms/text-field.d.ts +13 -0
  39. package/dist/components/forms/text-field.js +136 -1
  40. package/dist/components/forms/time-field.d.ts +11 -0
  41. package/dist/components/forms/time-field.js +51 -0
  42. package/dist/components/identity/avatar.d.ts +4 -0
  43. package/dist/components/identity/avatar.js +65 -1
  44. package/dist/components/identity/contact-datalist-item.d.ts +8 -0
  45. package/dist/components/identity/contact-datalist-item.js +71 -2
  46. package/dist/components/layout/nav-sidebar.js +23 -0
  47. package/dist/components/layout/sidebar-toggle-button.d.ts +3 -0
  48. package/dist/components/layout/sidebar-toggle-button.js +21 -2
  49. package/dist/components/navigation/accordion.d.ts +3 -0
  50. package/dist/components/navigation/accordion.js +17 -2
  51. package/dist/components/overlays/guide-tooltip.d.ts +51 -0
  52. package/dist/components/overlays/guide-tooltip.js +361 -0
  53. package/dist/components/overlays/tooltip.d.ts +10 -0
  54. package/dist/components/overlays/tooltip.js +74 -11
  55. package/dist/foundations/overlay/positioning.js +5 -1
  56. package/dist/index.d.ts +1 -0
  57. package/dist/index.js +1 -0
  58. package/package.json +1 -1
@@ -2,6 +2,14 @@ import { BaseElement } from "../../core/index.js";
2
2
  export declare class BoxLinkButtonElement extends BaseElement {
3
3
  static get observedAttributes(): string[];
4
4
  private linkEl;
5
+ private labelEl;
6
+ private contentSlot;
7
+ /** Anchor target (e.g. `_blank`). Opening a new tab auto-adds a safe rel. */
8
+ get target(): string;
9
+ set target(value: string);
10
+ /** Explicit rel; defaults to `noopener noreferrer` when target is `_blank`. */
11
+ get rel(): string;
12
+ set rel(value: string);
5
13
  get href(): string;
6
14
  set href(value: string);
7
15
  get label(): string;
@@ -9,6 +17,7 @@ export declare class BoxLinkButtonElement extends BaseElement {
9
17
  get tone(): string;
10
18
  set tone(value: string);
11
19
  protected renderTemplate(): void;
20
+ protected setupListeners(): void;
12
21
  protected update(): void;
13
22
  }
14
23
  export declare const defineBoxLinkButtonElement: (tagName?: string) => typeof BoxLinkButtonElement;
@@ -86,9 +86,25 @@ const linkButtonStyles = `
86
86
  `;
87
87
  export class BoxLinkButtonElement extends BaseElement {
88
88
  static get observedAttributes() {
89
- return ["href", "label", "tone"];
89
+ return ["href", "label", "tone", "target", "rel"];
90
90
  }
91
91
  linkEl;
92
+ labelEl;
93
+ contentSlot;
94
+ /** Anchor target (e.g. `_blank`). Opening a new tab auto-adds a safe rel. */
95
+ get target() {
96
+ return this.getAttribute("target") ?? "";
97
+ }
98
+ set target(value) {
99
+ this.setAttribute("target", value);
100
+ }
101
+ /** Explicit rel; defaults to `noopener noreferrer` when target is `_blank`. */
102
+ get rel() {
103
+ return this.getAttribute("rel") ?? "";
104
+ }
105
+ set rel(value) {
106
+ this.setAttribute("rel", value);
107
+ }
92
108
  get href() {
93
109
  return this.getAttribute("href") ?? "#";
94
110
  }
@@ -113,9 +129,18 @@ export class BoxLinkButtonElement extends BaseElement {
113
129
  }
114
130
  this.shadowRoot.innerHTML = `
115
131
  <style>${linkButtonStyles}</style>
116
- <a part="link"></a>
132
+ <a part="link"><slot><span part="label"></span></slot></a>
117
133
  `;
118
134
  this.linkEl = this.shadowRoot.querySelector('[part="link"]');
135
+ this.contentSlot = this.shadowRoot.querySelector("slot");
136
+ this.labelEl = this.shadowRoot.querySelector('[part="label"]');
137
+ }
138
+ setupListeners() {
139
+ this.contentSlot.addEventListener("slotchange", () => {
140
+ if (this.isRendered) {
141
+ this.update();
142
+ }
143
+ });
119
144
  }
120
145
  update() {
121
146
  if (!this.linkEl) {
@@ -123,8 +148,34 @@ export class BoxLinkButtonElement extends BaseElement {
123
148
  }
124
149
  this.linkEl.dataset.tone = this.tone;
125
150
  this.linkEl.setAttribute("href", escapeHtml(safeHref(this.href)));
126
- this.linkEl.setAttribute("aria-label", this.label);
127
- this.linkEl.textContent = this.label;
151
+ // Rich children (slotted) replace the `label` text; keep the fallback in
152
+ // sync. Detect real host content only — assignedNodes() without flatten
153
+ // excludes the fallback span; ignore whitespace-only text nodes.
154
+ const hasSlotted = this.contentSlot
155
+ .assignedNodes()
156
+ .some(node => node.nodeType === Node.ELEMENT_NODE || (node.textContent ?? "").trim() !== "");
157
+ this.labelEl.textContent = this.label;
158
+ if (hasSlotted) {
159
+ this.linkEl.removeAttribute("aria-label");
160
+ }
161
+ else {
162
+ this.linkEl.setAttribute("aria-label", this.label);
163
+ }
164
+ const target = this.target;
165
+ if (target) {
166
+ this.linkEl.setAttribute("target", target);
167
+ }
168
+ else {
169
+ this.linkEl.removeAttribute("target");
170
+ }
171
+ // Explicit rel wins; opening a new tab defaults to a safe rel.
172
+ const rel = this.rel || (target === "_blank" ? "noopener noreferrer" : "");
173
+ if (rel) {
174
+ this.linkEl.setAttribute("rel", rel);
175
+ }
176
+ else {
177
+ this.linkEl.removeAttribute("rel");
178
+ }
128
179
  }
129
180
  }
130
181
  export const defineBoxLinkButtonElement = (tagName = DEFAULT_TAG_NAME) => {
@@ -12,6 +12,9 @@ export declare class BoxCarouselElement extends BaseElement {
12
12
  private carouselEl;
13
13
  private labelEl;
14
14
  private stageEl;
15
+ private viewportEl;
16
+ private slottedStageEl;
17
+ private slideSlot;
15
18
  private mediaEl;
16
19
  private eyebrowEl;
17
20
  private titleEl;
@@ -28,6 +31,10 @@ export declare class BoxCarouselElement extends BaseElement {
28
31
  get value(): number;
29
32
  set value(value: number);
30
33
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
34
+ /** Slotted slides (elements assigned to the `slide` slot). */
35
+ private slottedSlides;
36
+ /** Active slide count — slotted slides when present, else JSON items. */
37
+ private slideCount;
31
38
  private normalizeIndex;
32
39
  private emitValueChanged;
33
40
  private setIndex;
@@ -182,6 +182,15 @@ const carouselStyles = `
182
182
  line-height: 1.58;
183
183
  }
184
184
 
185
+ /* Slotted-slides mode: only the active slide is shown. */
186
+ [part="slotted-stage"][hidden] {
187
+ display: none;
188
+ }
189
+
190
+ [part="slotted-stage"] ::slotted(:not([data-carousel-active])) {
191
+ display: none;
192
+ }
193
+
185
194
  @media (max-width: 720px) {
186
195
  [part="viewport"] {
187
196
  grid-template-columns: 1fr;
@@ -197,6 +206,9 @@ export class BoxCarouselElement extends BaseElement {
197
206
  carouselEl;
198
207
  labelEl;
199
208
  stageEl;
209
+ viewportEl;
210
+ slottedStageEl;
211
+ slideSlot;
200
212
  mediaEl;
201
213
  eyebrowEl;
202
214
  titleEl;
@@ -244,8 +256,19 @@ export class BoxCarouselElement extends BaseElement {
244
256
  }
245
257
  super.attributeChangedCallback(name, oldValue, newValue);
246
258
  }
259
+ /** Slotted slides (elements assigned to the `slide` slot). */
260
+ slottedSlides() {
261
+ return this.slideSlot
262
+ ? this.slideSlot.assignedElements({ flatten: true })
263
+ : [];
264
+ }
265
+ /** Active slide count — slotted slides when present, else JSON items. */
266
+ slideCount() {
267
+ const slotted = this.slottedSlides().length;
268
+ return slotted > 0 ? slotted : this.items.length;
269
+ }
247
270
  normalizeIndex(value) {
248
- const maxIndex = Math.max(0, this.items.length - 1);
271
+ const maxIndex = Math.max(0, this.slideCount() - 1);
249
272
  if (!Number.isFinite(value)) {
250
273
  return 0;
251
274
  }
@@ -265,10 +288,11 @@ export class BoxCarouselElement extends BaseElement {
265
288
  this.emitValueChanged(nextIndex);
266
289
  }
267
290
  changeBy(delta) {
268
- if (this.items.length === 0) {
291
+ const count = this.slideCount();
292
+ if (count === 0) {
269
293
  return;
270
294
  }
271
- const nextIndex = (this.valueInternal + delta + this.items.length) % this.items.length;
295
+ const nextIndex = (this.valueInternal + delta + count) % count;
272
296
  this.setIndex(nextIndex);
273
297
  }
274
298
  renderTemplate() {
@@ -288,6 +312,7 @@ export class BoxCarouselElement extends BaseElement {
288
312
  <div part="description" hidden></div>
289
313
  </div>
290
314
  </div>
315
+ <div part="slotted-stage" hidden><slot name="slide"></slot></div>
291
316
  </div>
292
317
  <div part="controls">
293
318
  <div part="nav">
@@ -302,6 +327,9 @@ export class BoxCarouselElement extends BaseElement {
302
327
  this.carouselEl = this.shadowRoot.querySelector('[part="carousel"]');
303
328
  this.labelEl = this.shadowRoot.querySelector('[part="label"]');
304
329
  this.stageEl = this.shadowRoot.querySelector('[part="stage"]');
330
+ this.viewportEl = this.shadowRoot.querySelector('[part="viewport"]');
331
+ this.slottedStageEl = this.shadowRoot.querySelector('[part="slotted-stage"]');
332
+ this.slideSlot = this.shadowRoot.querySelector('slot[name="slide"]');
305
333
  this.mediaEl = this.shadowRoot.querySelector('[part="media"]');
306
334
  this.eyebrowEl = this.shadowRoot.querySelector('[part="eyebrow"]');
307
335
  this.titleEl = this.shadowRoot.querySelector('[part="title"]');
@@ -315,6 +343,12 @@ export class BoxCarouselElement extends BaseElement {
315
343
  setupListeners() {
316
344
  this.previousEl.addEventListener("click", () => this.changeBy(-1));
317
345
  this.nextEl.addEventListener("click", () => this.changeBy(1));
346
+ this.slideSlot.addEventListener("slotchange", () => {
347
+ this.valueInternal = this.normalizeIndex(this.valueInternal);
348
+ if (this.isRendered) {
349
+ this.update();
350
+ }
351
+ });
318
352
  this.paginationEl.addEventListener("click", event => {
319
353
  const target = event.target?.closest('[part~="dot"]');
320
354
  if (!target || !this.paginationEl.contains(target)) {
@@ -342,11 +376,44 @@ export class BoxCarouselElement extends BaseElement {
342
376
  }
343
377
  const items = this.items;
344
378
  const itemsJson = this.getAttribute("items") ?? "";
345
- const activeItem = items[this.valueInternal] ?? null;
346
379
  const label = this.label;
347
380
  this.carouselEl.setAttribute("aria-label", label);
348
381
  this.labelEl.textContent = label;
349
382
  this.paginationEl.setAttribute("aria-label", `${label} slides`);
383
+ // Slotted-slides mode takes precedence when the host provides slide content.
384
+ const slotted = this.slottedSlides();
385
+ if (slotted.length > 0) {
386
+ this.viewportEl.hidden = true;
387
+ this.slottedStageEl.hidden = false;
388
+ this.stageEl.hidden = false;
389
+ this.controlsEl.hidden = false;
390
+ this.emptyEl.hidden = true;
391
+ slotted.forEach((slide, index) => {
392
+ const active = index === this.valueInternal;
393
+ slide.toggleAttribute("data-carousel-active", active);
394
+ slide.setAttribute("aria-hidden", active ? "false" : "true");
395
+ });
396
+ this.paginationEl.innerHTML = slotted
397
+ .map((slide, index) => {
398
+ const dotPart = index === this.valueInternal ? "dot dot-selected" : "dot";
399
+ const title = slide.getAttribute("data-title") ?? slide.getAttribute("aria-label") ?? `${index + 1}`;
400
+ return `
401
+ <button
402
+ type="button"
403
+ part="${dotPart}"
404
+ aria-label="Go to slide ${index + 1}: ${escapeHtml(title)}"
405
+ aria-current="${index === this.valueInternal ? "true" : "false"}"
406
+ data-index="${index}"
407
+ ></button>
408
+ `;
409
+ })
410
+ .join("");
411
+ this.lastItemsJson = "";
412
+ return;
413
+ }
414
+ this.slottedStageEl.hidden = true;
415
+ this.viewportEl.hidden = false;
416
+ const activeItem = items[this.valueInternal] ?? null;
350
417
  if (!activeItem) {
351
418
  this.stageEl.hidden = true;
352
419
  this.controlsEl.hidden = true;
@@ -10,8 +10,13 @@ export declare class BoxDatalistItemElement extends BaseElement {
10
10
  static get observedAttributes(): string[];
11
11
  private itemEl;
12
12
  private thumbEl;
13
+ private bodyEl;
13
14
  private labelEl;
14
15
  private metaEl;
16
+ private contentSlot;
17
+ /** Active-descendant highlight — set by a host that keeps DOM focus elsewhere. */
18
+ get active(): boolean;
19
+ set active(value: boolean);
15
20
  get label(): string;
16
21
  set label(value: string);
17
22
  get meta(): string;
@@ -27,6 +32,7 @@ export declare class BoxDatalistItemElement extends BaseElement {
27
32
  private choose;
28
33
  protected renderTemplate(): void;
29
34
  protected setupListeners(): void;
35
+ private syncSlot;
30
36
  protected update(): void;
31
37
  }
32
38
  export declare const defineBoxDatalistItemElement: (tagName?: string) => typeof BoxDatalistItemElement;
@@ -28,6 +28,11 @@ const datalistItemStyles = `
28
28
  background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 10%, var(--boe-token-surface-surface, #ffffff) 90%);
29
29
  }
30
30
 
31
+ /* Active-descendant highlight (host sets active; DOM focus stays on the input). */
32
+ [part="item"][data-active="true"] {
33
+ background: var(--boe-token-surface-surface-hover, #f4f4f4);
34
+ }
35
+
31
36
  [part="item"][data-disabled="true"] {
32
37
  opacity: 0.55;
33
38
  cursor: not-allowed;
@@ -73,6 +78,16 @@ const datalistItemStyles = `
73
78
  [part="meta"][hidden] {
74
79
  display: none;
75
80
  }
81
+
82
+ /* Arbitrary slotted content replaces the default label/meta when present. */
83
+ [part="content"]:not(.has-content) {
84
+ display: none;
85
+ }
86
+
87
+ [part="body"].has-slotted [part="label"],
88
+ [part="body"].has-slotted [part="meta"] {
89
+ display: none;
90
+ }
76
91
  `;
77
92
  /**
78
93
  * A single selectable row for a picker/typeahead list — a leading glyph, a
@@ -83,12 +98,21 @@ const datalistItemStyles = `
83
98
  */
84
99
  export class BoxDatalistItemElement extends BaseElement {
85
100
  static get observedAttributes() {
86
- return ["disabled", "icon", "label", "meta", "selected", "value"];
101
+ return ["active", "disabled", "icon", "label", "meta", "selected", "value"];
87
102
  }
88
103
  itemEl;
89
104
  thumbEl;
105
+ bodyEl;
90
106
  labelEl;
91
107
  metaEl;
108
+ contentSlot;
109
+ /** Active-descendant highlight — set by a host that keeps DOM focus elsewhere. */
110
+ get active() {
111
+ return this.hasAttribute("active");
112
+ }
113
+ set active(value) {
114
+ this.toggleAttribute("active", value);
115
+ }
92
116
  get label() {
93
117
  return this.getAttribute("label") ?? "";
94
118
  }
@@ -146,13 +170,16 @@ export class BoxDatalistItemElement extends BaseElement {
146
170
  <span part="body">
147
171
  <span part="label"></span>
148
172
  <span part="meta" hidden></span>
173
+ <slot part="content"></slot>
149
174
  </span>
150
175
  </div>
151
176
  `;
152
177
  this.itemEl = this.shadowRoot.querySelector('[part="item"]');
153
178
  this.thumbEl = this.shadowRoot.querySelector('[part="thumb"]');
179
+ this.bodyEl = this.shadowRoot.querySelector('[part="body"]');
154
180
  this.labelEl = this.shadowRoot.querySelector('[part="label"]');
155
181
  this.metaEl = this.shadowRoot.querySelector('[part="meta"]');
182
+ this.contentSlot = this.shadowRoot.querySelector('slot[part="content"]');
156
183
  }
157
184
  setupListeners() {
158
185
  this.itemEl.addEventListener("click", () => this.choose());
@@ -162,6 +189,15 @@ export class BoxDatalistItemElement extends BaseElement {
162
189
  this.choose();
163
190
  }
164
191
  });
192
+ this.contentSlot.addEventListener("slotchange", () => this.syncSlot());
193
+ }
194
+ syncSlot() {
195
+ if (!this.contentSlot) {
196
+ return;
197
+ }
198
+ const hasContent = this.contentSlot.assignedNodes({ flatten: true }).length > 0;
199
+ this.contentSlot.classList.toggle("has-content", hasContent);
200
+ this.bodyEl.classList.toggle("has-slotted", hasContent);
165
201
  }
166
202
  update() {
167
203
  if (!this.itemEl || !this.thumbEl || !this.labelEl || !this.metaEl) {
@@ -173,7 +209,9 @@ export class BoxDatalistItemElement extends BaseElement {
173
209
  this.itemEl.setAttribute("aria-selected", selected ? "true" : "false");
174
210
  this.itemEl.dataset.selected = selected ? "true" : "false";
175
211
  this.itemEl.dataset.disabled = disabled ? "true" : "false";
212
+ this.itemEl.dataset.active = this.active ? "true" : "false";
176
213
  this.itemEl.tabIndex = disabled ? -1 : 0;
214
+ this.syncSlot();
177
215
  if (disabled) {
178
216
  this.itemEl.setAttribute("aria-disabled", "true");
179
217
  }
@@ -172,7 +172,7 @@ export class BoxDraggableListElement extends BaseElement {
172
172
  <path d="M6 4h.01M10 4h.01M6 8h.01M10 8h.01M6 12h.01M10 12h.01" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
173
173
  </svg>
174
174
  </button>
175
- <span part="item-label">${escapeHtml(item.label)}</span>
175
+ <slot name="row-${escapeHtml(item.value)}" part="item-content"><span part="item-label">${escapeHtml(item.label)}</span></slot>
176
176
  </li>
177
177
  `;
178
178
  })
@@ -202,6 +202,8 @@ export class BoxGridViewElement extends BaseElement {
202
202
  const metaMarkup = item.meta
203
203
  ? `<span part="meta">${escapeHtml(item.meta)}</span>`
204
204
  : "";
205
+ // Per-item named slot lets a host override the tile body (slotRenderer
206
+ // parity); the default thumb/label/meta is the slot's fallback content.
205
207
  return `
206
208
  <button
207
209
  type="button"
@@ -211,9 +213,11 @@ export class BoxGridViewElement extends BaseElement {
211
213
  aria-selected="${String(isSelected)}"
212
214
  tabindex="${item.value === tabbableValue ? "0" : "-1"}"
213
215
  >
214
- <span part="thumb" aria-hidden="true">${iconMarkup}</span>
215
- <span part="tile-label">${escapeHtml(item.label)}</span>
216
- ${metaMarkup}
216
+ <slot name="tile-${escapeHtml(item.value)}" part="tile-slot">
217
+ <span part="thumb" aria-hidden="true">${iconMarkup}</span>
218
+ <span part="tile-label">${escapeHtml(item.label)}</span>
219
+ ${metaMarkup}
220
+ </slot>
217
221
  </button>
218
222
  `;
219
223
  })
@@ -6,6 +6,7 @@ export declare class BoxAlertElement extends BaseElement {
6
6
  private toneLabelEl;
7
7
  private titleEl;
8
8
  private messageEl;
9
+ private richSlot;
9
10
  private dismissEl;
10
11
  get open(): boolean;
11
12
  set open(value: boolean);
@@ -21,6 +22,7 @@ export declare class BoxAlertElement extends BaseElement {
21
22
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
22
23
  dismiss(): void;
23
24
  protected renderTemplate(): void;
25
+ private hasRichContent;
24
26
  protected setupListeners(): void;
25
27
  protected update(): void;
26
28
  }
@@ -90,6 +90,22 @@ const alertStyles = `
90
90
  display: none;
91
91
  }
92
92
 
93
+ /* Rich body — slotted markup (links, buttons) beyond the plain message.
94
+ Hidden (taking no grid gap) until content is assigned. */
95
+ [part="rich"] {
96
+ display: block;
97
+ line-height: 1.45;
98
+ }
99
+
100
+ [part="rich"]:not(.has-content) {
101
+ display: none;
102
+ }
103
+
104
+ [part="rich"]::slotted(a) {
105
+ color: var(--boe-token-surface-surface-brand, #0061d5);
106
+ font-weight: 600;
107
+ }
108
+
93
109
  .sr-only {
94
110
  position: absolute;
95
111
  inline-size: 1px;
@@ -129,6 +145,7 @@ export class BoxAlertElement extends BaseElement {
129
145
  toneLabelEl;
130
146
  titleEl;
131
147
  messageEl;
148
+ richSlot;
132
149
  dismissEl;
133
150
  get open() {
134
151
  return this.openValue;
@@ -202,6 +219,7 @@ export class BoxAlertElement extends BaseElement {
202
219
  <span part="tone-label" class="sr-only"></span>
203
220
  <h2 part="title" id="alert-title" hidden></h2>
204
221
  <span part="description message" hidden></span>
222
+ <slot part="rich"></slot>
205
223
  </div>
206
224
  <button type="button" part="dismiss" aria-label="Dismiss alert">Dismiss</button>
207
225
  </div>
@@ -210,19 +228,30 @@ export class BoxAlertElement extends BaseElement {
210
228
  this.toneLabelEl = this.shadowRoot.querySelector('[part="tone-label"]');
211
229
  this.titleEl = this.shadowRoot.querySelector('[part="title"]');
212
230
  this.messageEl = this.shadowRoot.querySelector('[part~="description"]');
231
+ this.richSlot = this.shadowRoot.querySelector('slot[part="rich"]');
213
232
  this.dismissEl = this.shadowRoot.querySelector('[part="dismiss"]');
214
233
  }
234
+ hasRichContent() {
235
+ return this.richSlot?.assignedNodes({ flatten: true }).length > 0;
236
+ }
215
237
  setupListeners() {
216
238
  this.dismissEl.addEventListener("click", () => {
217
239
  this.dismiss();
218
240
  });
241
+ this.richSlot.addEventListener("slotchange", () => {
242
+ if (this.isRendered) {
243
+ this.update();
244
+ }
245
+ });
219
246
  }
220
247
  update() {
221
248
  if (!this.alertEl) {
222
249
  return;
223
250
  }
224
- const visible = this.openValue && Boolean(this.heading || this.message);
251
+ const hasRich = this.hasRichContent();
252
+ const visible = this.openValue && Boolean(this.heading || this.message || hasRich);
225
253
  this.hidden = !visible;
254
+ this.richSlot.classList.toggle("has-content", hasRich);
226
255
  if (!visible) {
227
256
  return;
228
257
  }
@@ -2,10 +2,20 @@ import { BaseElement } from "../../core/index.js";
2
2
  export declare class BoxBadgeElement extends BaseElement {
3
3
  static get observedAttributes(): string[];
4
4
  private badgeEl;
5
+ private lastText;
5
6
  get label(): string;
6
7
  set label(value: string);
7
8
  get tone(): string;
8
9
  set tone(value: string);
10
+ /** When the label is a count, cap the displayed number, e.g. max=99 → "99+". */
11
+ get max(): number | null;
12
+ set max(value: number | null);
13
+ /** Hide the badge entirely when the count is zero or the label is empty. */
14
+ get hideWhenZero(): boolean;
15
+ set hideWhenZero(value: boolean);
16
+ /** The label after applying the `max` cap (e.g. "128" with max=99 → "99+"). */
17
+ private displayText;
18
+ private isEmptyCount;
9
19
  protected renderTemplate(): void;
10
20
  protected update(): void;
11
21
  }
@@ -1,6 +1,6 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
2
  import { boeRadius } from "../../foundations/geometry/index.js";
3
- import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
3
+ import { boeMotionDuration, boeMotionEasing, boeReducedMotionStyles, } from "../../foundations/motion/index.js";
4
4
  const DEFAULT_TAG_NAME = "box-badge";
5
5
  const badgeStyles = `
6
6
  :host {
@@ -52,12 +52,31 @@ const badgeStyles = `
52
52
  background: var(--boe-token-surface-status-surface-inprogress, #f5b31b);
53
53
  color: #ffffff;
54
54
  }
55
+
56
+ /* Hidden when used as a count that is zero/empty with hide-when-zero. */
57
+ :host([hidden]) {
58
+ display: none;
59
+ }
60
+
61
+ /* Count "pop" when the value changes (opt-in via the animate attribute). */
62
+ [part="badge"].boe-pop {
63
+ animation: boe-badge-pop ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
64
+ }
65
+
66
+ @keyframes boe-badge-pop {
67
+ 0% { transform: scale(0.7); }
68
+ 60% { transform: scale(1.15); }
69
+ 100% { transform: scale(1); }
70
+ }
71
+
72
+ ${boeReducedMotionStyles('[part="badge"].boe-pop', "animation: none;")}
55
73
  `;
56
74
  export class BoxBadgeElement extends BaseElement {
57
75
  static get observedAttributes() {
58
- return ["label", "tone"];
76
+ return ["label", "tone", "max", "hide-when-zero", "animate"];
59
77
  }
60
78
  badgeEl;
79
+ lastText = null;
61
80
  get label() {
62
81
  return this.getAttribute("label") ?? "";
63
82
  }
@@ -70,6 +89,42 @@ export class BoxBadgeElement extends BaseElement {
70
89
  set tone(value) {
71
90
  this.setAttribute("tone", value);
72
91
  }
92
+ /** When the label is a count, cap the displayed number, e.g. max=99 → "99+". */
93
+ get max() {
94
+ const raw = this.getAttribute("max");
95
+ if (raw === null)
96
+ return null;
97
+ const parsed = Number(raw);
98
+ return Number.isFinite(parsed) ? parsed : null;
99
+ }
100
+ set max(value) {
101
+ if (value === null) {
102
+ this.removeAttribute("max");
103
+ }
104
+ else {
105
+ this.setAttribute("max", String(value));
106
+ }
107
+ }
108
+ /** Hide the badge entirely when the count is zero or the label is empty. */
109
+ get hideWhenZero() {
110
+ return this.hasAttribute("hide-when-zero");
111
+ }
112
+ set hideWhenZero(value) {
113
+ this.toggleAttribute("hide-when-zero", value);
114
+ }
115
+ /** The label after applying the `max` cap (e.g. "128" with max=99 → "99+"). */
116
+ displayText() {
117
+ const raw = this.label;
118
+ const max = this.max;
119
+ if (max !== null && /^\d+$/.test(raw.trim()) && Number(raw) > max) {
120
+ return `${max}+`;
121
+ }
122
+ return raw;
123
+ }
124
+ isEmptyCount() {
125
+ const raw = this.label.trim();
126
+ return raw === "" || raw === "0";
127
+ }
73
128
  renderTemplate() {
74
129
  if (!this.shadowRoot) {
75
130
  return;
@@ -84,9 +139,20 @@ export class BoxBadgeElement extends BaseElement {
84
139
  if (!this.badgeEl) {
85
140
  return;
86
141
  }
142
+ // Hide when used as a count that is zero/empty and hide-when-zero is set.
143
+ const shouldHide = this.hideWhenZero && this.isEmptyCount();
144
+ this.toggleAttribute("hidden", shouldHide);
145
+ const text = this.displayText();
87
146
  this.badgeEl.dataset.tone = this.tone;
88
- this.badgeEl.setAttribute("aria-label", this.label);
89
- this.badgeEl.textContent = this.label;
147
+ this.badgeEl.setAttribute("aria-label", text);
148
+ this.badgeEl.textContent = text;
149
+ // Re-trigger the pop animation when an animated count actually changes.
150
+ if (this.hasAttribute("animate") && this.lastText !== null && this.lastText !== text) {
151
+ this.badgeEl.classList.remove("boe-pop");
152
+ void this.badgeEl.offsetWidth; // force reflow so the animation restarts
153
+ this.badgeEl.classList.add("boe-pop");
154
+ }
155
+ this.lastText = text;
90
156
  }
91
157
  }
92
158
  export const defineBoxBadgeElement = (tagName = DEFAULT_TAG_NAME) => {
@@ -8,7 +8,11 @@ export declare class BoxChipElement extends BaseElement {
8
8
  static get observedAttributes(): string[];
9
9
  private chipEl;
10
10
  private labelEl;
11
+ private iconSlot;
11
12
  private removeEl;
13
+ /** Chip size: `medium` (default) or `small`. */
14
+ get size(): string;
15
+ set size(value: string);
12
16
  get label(): string;
13
17
  set label(value: string);
14
18
  get value(): string;
@@ -28,6 +32,7 @@ export declare class BoxChipElement extends BaseElement {
28
32
  protected renderTemplate(): void;
29
33
  protected setupListeners(): void;
30
34
  private syncRemoveButton;
35
+ private syncIconSlot;
31
36
  protected update(): void;
32
37
  }
33
38
  export declare const defineBoxChipElement: (tagName?: string) => typeof BoxChipElement;