@unofficialbox/box-open-elements 0.2.1 → 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 (87) hide show
  1. package/dist/components/actions/button.d.ts +15 -0
  2. package/dist/components/actions/button.js +107 -3
  3. package/dist/components/actions/link-button.d.ts +9 -0
  4. package/dist/components/actions/link-button.js +55 -4
  5. package/dist/components/actions/menu.d.ts +10 -0
  6. package/dist/components/actions/menu.js +68 -11
  7. package/dist/components/collections/carousel.d.ts +7 -0
  8. package/dist/components/collections/carousel.js +71 -4
  9. package/dist/components/collections/datalist-item.d.ts +6 -0
  10. package/dist/components/collections/datalist-item.js +39 -1
  11. package/dist/components/collections/draggable-list.js +1 -1
  12. package/dist/components/collections/grid-view.js +7 -3
  13. package/dist/components/collections/table.d.ts +53 -0
  14. package/dist/components/collections/table.js +323 -0
  15. package/dist/components/collections/thumbnail-card.d.ts +19 -0
  16. package/dist/components/collections/thumbnail-card.js +210 -0
  17. package/dist/components/feedback/alert.d.ts +2 -0
  18. package/dist/components/feedback/alert.js +30 -1
  19. package/dist/components/feedback/badge.d.ts +10 -0
  20. package/dist/components/feedback/badge.js +70 -4
  21. package/dist/components/feedback/badgeable.d.ts +14 -0
  22. package/dist/components/feedback/badgeable.js +84 -0
  23. package/dist/components/feedback/chip.d.ts +5 -0
  24. package/dist/components/feedback/chip.js +74 -2
  25. package/dist/components/feedback/error-mask.d.ts +2 -0
  26. package/dist/components/feedback/error-mask.js +23 -0
  27. package/dist/components/feedback/help-text.js +7 -0
  28. package/dist/components/feedback/spinner.d.ts +4 -0
  29. package/dist/components/feedback/spinner.js +30 -2
  30. package/dist/components/feedback/toast.d.ts +5 -0
  31. package/dist/components/feedback/toast.js +59 -11
  32. package/dist/components/forms/category-selector.d.ts +11 -0
  33. package/dist/components/forms/category-selector.js +211 -17
  34. package/dist/components/forms/checkbox.d.ts +6 -0
  35. package/dist/components/forms/checkbox.js +58 -1
  36. package/dist/components/forms/combobox.d.ts +30 -1
  37. package/dist/components/forms/combobox.js +348 -44
  38. package/dist/components/forms/date-field.d.ts +8 -0
  39. package/dist/components/forms/date-field.js +80 -3
  40. package/dist/components/forms/dropdown.d.ts +5 -0
  41. package/dist/components/forms/dropdown.js +32 -3
  42. package/dist/components/forms/pill-selector-dropdown.d.ts +11 -0
  43. package/dist/components/forms/pill-selector-dropdown.js +127 -0
  44. package/dist/components/forms/radio-group.d.ts +9 -8
  45. package/dist/components/forms/radio-group.js +43 -6
  46. package/dist/components/forms/search-field.d.ts +9 -0
  47. package/dist/components/forms/search-field.js +62 -15
  48. package/dist/components/forms/select.d.ts +19 -8
  49. package/dist/components/forms/select.js +110 -15
  50. package/dist/components/forms/text-field.d.ts +14 -0
  51. package/dist/components/forms/text-field.js +144 -3
  52. package/dist/components/forms/time-field.d.ts +11 -0
  53. package/dist/components/forms/time-field.js +51 -0
  54. package/dist/components/identity/avatar.d.ts +4 -0
  55. package/dist/components/identity/avatar.js +65 -1
  56. package/dist/components/identity/contact-datalist-item.d.ts +8 -0
  57. package/dist/components/identity/contact-datalist-item.js +71 -2
  58. package/dist/components/layout/nav-sidebar.js +23 -0
  59. package/dist/components/layout/sidebar-toggle-button.d.ts +3 -0
  60. package/dist/components/layout/sidebar-toggle-button.js +21 -2
  61. package/dist/components/navigation/accordion.d.ts +3 -0
  62. package/dist/components/navigation/accordion.js +17 -2
  63. package/dist/components/navigation/breadcrumb.d.ts +30 -0
  64. package/dist/components/navigation/breadcrumb.js +190 -0
  65. package/dist/components/navigation/tabs.d.ts +4 -0
  66. package/dist/components/navigation/tabs.js +53 -5
  67. package/dist/components/overlays/context-menu.d.ts +42 -0
  68. package/dist/components/overlays/context-menu.js +260 -0
  69. package/dist/components/overlays/dialog.d.ts +8 -0
  70. package/dist/components/overlays/dialog.js +45 -2
  71. package/dist/components/overlays/guide-tooltip.d.ts +51 -0
  72. package/dist/components/overlays/guide-tooltip.js +361 -0
  73. package/dist/components/overlays/popover.d.ts +9 -1
  74. package/dist/components/overlays/popover.js +37 -22
  75. package/dist/components/overlays/tooltip.d.ts +10 -0
  76. package/dist/components/overlays/tooltip.js +74 -11
  77. package/dist/core/form-associated.d.ts +32 -0
  78. package/dist/core/form-associated.js +114 -0
  79. package/dist/core/index.d.ts +1 -1
  80. package/dist/core/index.js +1 -1
  81. package/dist/foundations/overlay/index.d.ts +2 -0
  82. package/dist/foundations/overlay/index.js +1 -0
  83. package/dist/foundations/overlay/positioning.d.ts +74 -0
  84. package/dist/foundations/overlay/positioning.js +162 -0
  85. package/dist/index.d.ts +8 -0
  86. package/dist/index.js +8 -0
  87. package/package.json +1 -1
@@ -0,0 +1,210 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ import { boePanel, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
3
+ import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
4
+ const DEFAULT_TAG_NAME = "box-thumbnail-card";
5
+ /** Rich file/grid card — box-ui-elements `ThumbnailCard`. */
6
+ const thumbnailCardStyles = `
7
+ :host {
8
+ display: inline-block;
9
+ color: inherit;
10
+ font: inherit;
11
+ }
12
+
13
+ [part="card"] {
14
+ display: grid;
15
+ grid-template-rows: auto auto;
16
+ width: 100%;
17
+ min-width: 160px;
18
+ border: ${boePanel.border};
19
+ border-radius: ${boeRadius.large};
20
+ background: var(--boe-token-surface-surface, #ffffff);
21
+ overflow: hidden;
22
+ transition:
23
+ border-color ${boeMotionDuration.interactive} ${boeMotionEasing.standard},
24
+ box-shadow ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
25
+ }
26
+
27
+ :host([interactive]) [part="card"] {
28
+ cursor: pointer;
29
+ }
30
+
31
+ :host([highlight-on-hover]) [part="card"]:hover,
32
+ :host([interactive]) [part="card"]:hover {
33
+ border-color: var(--boe-token-stroke-stroke-hover, #bcbcbc);
34
+ box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
35
+ }
36
+
37
+ [part="card"]:focus-visible {
38
+ outline: 2px solid var(--boe-token-surface-surface-brand, #0061d5);
39
+ outline-offset: 2px;
40
+ }
41
+
42
+ [part="thumbnail"] {
43
+ display: grid;
44
+ place-items: center;
45
+ aspect-ratio: 16 / 10;
46
+ background: var(--boe-token-surface-surface-secondary, #f4f4f4);
47
+ overflow: hidden;
48
+ }
49
+
50
+ [part="thumbnail"] ::slotted(img) {
51
+ width: 100%;
52
+ height: 100%;
53
+ object-fit: cover;
54
+ }
55
+
56
+ [part="details"] {
57
+ display: grid;
58
+ grid-template-columns: auto 1fr auto;
59
+ align-items: center;
60
+ gap: ${boeSpace[2]};
61
+ padding: ${boeSpace[3]};
62
+ }
63
+
64
+ [part="icon"] {
65
+ display: inline-flex;
66
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
67
+ }
68
+
69
+ [part="icon"]:not(:has(*)) {
70
+ display: none;
71
+ }
72
+
73
+ [part="text"] {
74
+ min-width: 0;
75
+ display: grid;
76
+ gap: 2px;
77
+ }
78
+
79
+ [part="title"] {
80
+ font-size: ${boeSpace[3]};
81
+ font-weight: 600;
82
+ color: var(--boe-token-text-text, #222222);
83
+ overflow: hidden;
84
+ text-overflow: ellipsis;
85
+ white-space: nowrap;
86
+ }
87
+
88
+ [part="subtitle"] {
89
+ font-size: 12px;
90
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
91
+ overflow: hidden;
92
+ text-overflow: ellipsis;
93
+ white-space: nowrap;
94
+ }
95
+
96
+ [part="subtitle"][hidden] {
97
+ display: none;
98
+ }
99
+
100
+ [part="action"] {
101
+ display: inline-flex;
102
+ }
103
+
104
+ [part="action"]:not(:has(*)) {
105
+ display: none;
106
+ }
107
+ `;
108
+ export class BoxThumbnailCardElement extends BaseElement {
109
+ static get observedAttributes() {
110
+ return ["title", "subtitle", "interactive", "highlight-on-hover"];
111
+ }
112
+ cardEl;
113
+ titleEl;
114
+ subtitleEl;
115
+ get cardTitle() {
116
+ return this.getAttribute("title") ?? "";
117
+ }
118
+ set cardTitle(value) {
119
+ this.setAttribute("title", value);
120
+ }
121
+ get subtitle() {
122
+ return this.getAttribute("subtitle") ?? "";
123
+ }
124
+ set subtitle(value) {
125
+ if (value) {
126
+ this.setAttribute("subtitle", value);
127
+ }
128
+ else {
129
+ this.removeAttribute("subtitle");
130
+ }
131
+ }
132
+ /** When set, the card is a button: focusable, keyboard-activated, emits `activate`. */
133
+ get interactive() {
134
+ return this.hasAttribute("interactive");
135
+ }
136
+ set interactive(value) {
137
+ this.toggleAttribute("interactive", Boolean(value));
138
+ }
139
+ renderTemplate() {
140
+ if (!this.shadowRoot) {
141
+ return;
142
+ }
143
+ this.shadowRoot.innerHTML = `
144
+ <style>${thumbnailCardStyles}</style>
145
+ <div part="card">
146
+ <div part="thumbnail"><slot name="thumbnail"></slot></div>
147
+ <div part="details">
148
+ <span part="icon"><slot name="icon"></slot></span>
149
+ <div part="text">
150
+ <div part="title"></div>
151
+ <div part="subtitle"></div>
152
+ </div>
153
+ <span part="action"><slot name="action"></slot></span>
154
+ </div>
155
+ </div>
156
+ `;
157
+ this.cardEl = this.shadowRoot.querySelector('[part="card"]');
158
+ this.titleEl = this.shadowRoot.querySelector('[part="title"]');
159
+ this.subtitleEl = this.shadowRoot.querySelector('[part="subtitle"]');
160
+ }
161
+ setupListeners() {
162
+ this.cardEl.addEventListener("click", () => this.activate());
163
+ this.cardEl.addEventListener("keydown", event => {
164
+ if (!this.interactive) {
165
+ return;
166
+ }
167
+ const key = event.key;
168
+ if (key === "Enter" || key === " ") {
169
+ event.preventDefault();
170
+ this.activate();
171
+ }
172
+ });
173
+ }
174
+ activate() {
175
+ if (!this.interactive) {
176
+ return;
177
+ }
178
+ this.dispatchEvent(new CustomEvent("activate", {
179
+ bubbles: true,
180
+ composed: true,
181
+ detail: { title: this.cardTitle },
182
+ }));
183
+ }
184
+ update() {
185
+ if (!this.cardEl || !this.titleEl || !this.subtitleEl) {
186
+ return;
187
+ }
188
+ this.titleEl.textContent = this.cardTitle;
189
+ this.subtitleEl.textContent = this.subtitle;
190
+ this.subtitleEl.hidden = this.subtitle.length === 0;
191
+ if (this.interactive) {
192
+ this.cardEl.setAttribute("role", "button");
193
+ this.cardEl.setAttribute("tabindex", "0");
194
+ this.cardEl.setAttribute("aria-label", this.cardTitle);
195
+ }
196
+ else {
197
+ this.cardEl.removeAttribute("role");
198
+ this.cardEl.removeAttribute("tabindex");
199
+ this.cardEl.removeAttribute("aria-label");
200
+ }
201
+ }
202
+ }
203
+ export const defineBoxThumbnailCardElement = (tagName = DEFAULT_TAG_NAME) => {
204
+ const existingElement = customElements.get(tagName);
205
+ if (existingElement) {
206
+ return existingElement;
207
+ }
208
+ customElements.define(tagName, BoxThumbnailCardElement);
209
+ return BoxThumbnailCardElement;
210
+ };
@@ -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) => {
@@ -0,0 +1,14 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ /**
3
+ * Overlays badges on the corners of its content — the box-ui-elements
4
+ * `Badgeable` wrapper. The subject goes in the default slot; each corner is a
5
+ * named slot (`top-left`, `top-right`, `bottom-left`, `bottom-right`). Corners
6
+ * with no assigned content are hidden so they don't capture pointer events.
7
+ */
8
+ export declare class BoxBadgeableElement extends BaseElement {
9
+ private corners;
10
+ protected renderTemplate(): void;
11
+ protected setupListeners(): void;
12
+ protected update(): void;
13
+ }
14
+ export declare const defineBoxBadgeableElement: (tagName?: string) => typeof BoxBadgeableElement;
@@ -0,0 +1,84 @@
1
+ import { BaseElement } from "../../core/index.js";
2
+ const DEFAULT_TAG_NAME = "box-badgeable";
3
+ /** Corners a badge can be pinned to, matching box-ui-elements' Badgeable. */
4
+ const CORNERS = ["top-left", "top-right", "bottom-left", "bottom-right"];
5
+ const badgeableStyles = `
6
+ :host {
7
+ display: inline-block;
8
+ color: inherit;
9
+ font: inherit;
10
+ }
11
+
12
+ [part="container"] {
13
+ position: relative;
14
+ display: inline-flex;
15
+ }
16
+
17
+ [part="badge"] {
18
+ position: absolute;
19
+ z-index: 1;
20
+ display: inline-flex;
21
+ pointer-events: none;
22
+ }
23
+
24
+ /* Slotted badge content stays interactive (e.g. a dismiss button). */
25
+ [part="badge"] ::slotted(*) {
26
+ pointer-events: auto;
27
+ }
28
+
29
+ [part="badge"][hidden] {
30
+ display: none;
31
+ }
32
+
33
+ [part="badge"][data-corner="top-left"] { inset-block-start: 0; inset-inline-start: 0; transform: translate(-35%, -35%); }
34
+ [part="badge"][data-corner="top-right"] { inset-block-start: 0; inset-inline-end: 0; transform: translate(35%, -35%); }
35
+ [part="badge"][data-corner="bottom-left"] { inset-block-end: 0; inset-inline-start: 0; transform: translate(-35%, 35%); }
36
+ [part="badge"][data-corner="bottom-right"] { inset-block-end: 0; inset-inline-end: 0; transform: translate(35%, 35%); }
37
+ `;
38
+ /**
39
+ * Overlays badges on the corners of its content — the box-ui-elements
40
+ * `Badgeable` wrapper. The subject goes in the default slot; each corner is a
41
+ * named slot (`top-left`, `top-right`, `bottom-left`, `bottom-right`). Corners
42
+ * with no assigned content are hidden so they don't capture pointer events.
43
+ */
44
+ export class BoxBadgeableElement extends BaseElement {
45
+ corners = new Map();
46
+ renderTemplate() {
47
+ if (!this.shadowRoot) {
48
+ return;
49
+ }
50
+ this.shadowRoot.innerHTML = `
51
+ <style>${badgeableStyles}</style>
52
+ <span part="container">
53
+ <slot></slot>
54
+ ${CORNERS.map(corner => `<span part="badge" data-corner="${corner}" hidden><slot name="${corner}"></slot></span>`).join("")}
55
+ </span>
56
+ `;
57
+ this.corners.clear();
58
+ for (const corner of CORNERS) {
59
+ const wrapper = this.shadowRoot.querySelector(`[data-corner="${corner}"]`);
60
+ const slot = wrapper.querySelector("slot");
61
+ this.corners.set(corner, { wrapper, slot });
62
+ }
63
+ }
64
+ setupListeners() {
65
+ for (const { wrapper, slot } of this.corners.values()) {
66
+ slot.addEventListener("slotchange", () => {
67
+ wrapper.hidden = slot.assignedNodes({ flatten: true }).length === 0;
68
+ });
69
+ }
70
+ }
71
+ update() {
72
+ for (const { wrapper, slot } of this.corners.values()) {
73
+ wrapper.hidden = slot.assignedNodes({ flatten: true }).length === 0;
74
+ }
75
+ }
76
+ }
77
+ export const defineBoxBadgeableElement = (tagName = DEFAULT_TAG_NAME) => {
78
+ const existingElement = customElements.get(tagName);
79
+ if (existingElement) {
80
+ return existingElement;
81
+ }
82
+ customElements.define(tagName, BoxBadgeableElement);
83
+ return BoxBadgeableElement;
84
+ };
@@ -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;
@@ -42,12 +42,59 @@ const chipStyles = `
42
42
  background: color-mix(in srgb, var(--boe-token-surface-surface-hover, #f4f4f4) 70%, var(--boe-token-surface-surface-secondary, #fbfbfb) 30%);
43
43
  }
44
44
 
45
- [part="chip"][data-tone="brand"] {
45
+ [part="chip"][data-tone="brand"],
46
+ [part="chip"][data-tone="info"] {
46
47
  border-color: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 26%, transparent);
47
48
  background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 10%, var(--boe-token-surface-surface, #ffffff) 90%);
48
49
  color: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 72%, var(--boe-token-text-text, #222222));
49
50
  }
50
51
 
52
+ [part="chip"][data-tone="success"] {
53
+ border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26c281) 32%, transparent);
54
+ background: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26c281) 12%, var(--boe-token-surface-surface, #ffffff) 88%);
55
+ color: color-mix(in srgb, var(--boe-token-surface-status-surface-success, #26c281) 60%, var(--boe-token-text-text, #222222));
56
+ }
57
+
58
+ [part="chip"][data-tone="warning"] {
59
+ border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 40%, transparent);
60
+ background: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 16%, var(--boe-token-surface-surface, #ffffff) 84%);
61
+ color: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 58%, var(--boe-token-text-text, #222222));
62
+ }
63
+
64
+ [part="chip"][data-tone="error"] {
65
+ border-color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 30%, transparent);
66
+ background: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 10%, var(--boe-token-surface-surface, #ffffff) 90%);
67
+ color: color-mix(in srgb, var(--boe-token-surface-status-surface-error, #ed3757) 70%, var(--boe-token-text-text, #222222));
68
+ }
69
+
70
+ /* Compact size. */
71
+ [part="chip"][data-size="small"] {
72
+ gap: 0.25rem;
73
+ padding: 0.14rem 0.28rem 0.14rem 0.55rem;
74
+ font-size: 0.74rem;
75
+ }
76
+
77
+ [part="chip"][data-size="small"]:not([data-removable="true"]) {
78
+ padding-inline-end: 0.55rem;
79
+ }
80
+
81
+ /* Leading icon slot — hidden (and taking no gap) until content is assigned. */
82
+ [part="icon"] {
83
+ display: inline-flex;
84
+ align-items: center;
85
+ flex: none;
86
+ }
87
+
88
+ [part="icon"]:not(.has-content) {
89
+ display: none;
90
+ }
91
+
92
+ [part="icon"]::slotted(*) {
93
+ inline-size: 0.95rem;
94
+ block-size: 0.95rem;
95
+ display: block;
96
+ }
97
+
51
98
  [part="chip"][data-selected="true"],
52
99
  [part="chip"][data-selected="true"]:hover:not([data-disabled="true"]),
53
100
  [part="chip"][data-selected="true"]:active:not([data-disabled="true"]) {
@@ -94,11 +141,19 @@ const chipStyles = `
94
141
  */
95
142
  export class BoxChipElement extends BaseElement {
96
143
  static get observedAttributes() {
97
- return ["disabled", "label", "removable", "selectable", "selected", "tone", "value"];
144
+ return ["disabled", "label", "removable", "selectable", "selected", "size", "tone", "value"];
98
145
  }
99
146
  chipEl;
100
147
  labelEl;
148
+ iconSlot;
101
149
  removeEl = null;
150
+ /** Chip size: `medium` (default) or `small`. */
151
+ get size() {
152
+ return this.getAttribute("size") === "small" ? "small" : "medium";
153
+ }
154
+ set size(value) {
155
+ this.setAttribute("size", value);
156
+ }
102
157
  get label() {
103
158
  return this.getAttribute("label") ?? "";
104
159
  }
@@ -169,13 +224,16 @@ export class BoxChipElement extends BaseElement {
169
224
  this.shadowRoot.innerHTML = `
170
225
  <style>${chipStyles}</style>
171
226
  <span part="chip">
227
+ <slot name="icon" part="icon"></slot>
172
228
  <span part="label"></span>
173
229
  </span>
174
230
  `;
175
231
  this.chipEl = this.shadowRoot.querySelector('[part="chip"]');
176
232
  this.labelEl = this.shadowRoot.querySelector('[part="label"]');
233
+ this.iconSlot = this.shadowRoot.querySelector('slot[name="icon"]');
177
234
  }
178
235
  setupListeners() {
236
+ this.iconSlot.addEventListener("slotchange", () => this.syncIconSlot());
179
237
  this.chipEl.addEventListener("click", event => {
180
238
  if (event.target.closest('[part="remove"]')) {
181
239
  return;
@@ -217,11 +275,25 @@ export class BoxChipElement extends BaseElement {
217
275
  this.removeEl = null;
218
276
  }
219
277
  }
278
+ syncIconSlot() {
279
+ if (!this.iconSlot) {
280
+ return;
281
+ }
282
+ const hasContent = this.iconSlot.assignedNodes({ flatten: true }).length > 0;
283
+ this.iconSlot.classList.toggle("has-content", hasContent);
284
+ }
220
285
  update() {
221
286
  if (!this.chipEl) {
222
287
  return;
223
288
  }
224
289
  this.chipEl.dataset.tone = this.tone;
290
+ if (this.size === "medium") {
291
+ this.chipEl.removeAttribute("data-size");
292
+ }
293
+ else {
294
+ this.chipEl.dataset.size = this.size;
295
+ }
296
+ this.syncIconSlot();
225
297
  this.chipEl.dataset.removable = this.removable ? "true" : "false";
226
298
  this.chipEl.dataset.selected = this.selected ? "true" : "false";
227
299
  this.chipEl.dataset.disabled = this.disabled ? "true" : "false";
@@ -9,6 +9,7 @@ export declare class BoxErrorMaskElement extends BaseElement {
9
9
  static get observedAttributes(): string[];
10
10
  private titleEl;
11
11
  private messageEl;
12
+ private bodySlot;
12
13
  private actionEl;
13
14
  get heading(): string;
14
15
  set heading(value: string);
@@ -19,6 +20,7 @@ export declare class BoxErrorMaskElement extends BaseElement {
19
20
  get actionLabel(): string;
20
21
  set actionLabel(value: string);
21
22
  protected renderTemplate(): void;
23
+ protected setupListeners(): void;
22
24
  private syncActionButton;
23
25
  protected update(): void;
24
26
  }