@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
@@ -1,7 +1,20 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
+ export type ButtonType = "button" | "submit" | "reset";
2
3
  export declare class BoxButtonElement extends BaseElement {
4
+ static formAssociated: boolean;
3
5
  static get observedAttributes(): string[];
4
6
  private buttonEl;
7
+ private spinnerEl;
8
+ private iconEl;
9
+ private labelEl;
10
+ private iconSlot;
11
+ private readonly internals;
12
+ constructor();
13
+ /** Loading state: shows a spinner and blocks activation (stays focusable). */
14
+ get isLoading(): boolean;
15
+ set isLoading(value: boolean);
16
+ get type(): ButtonType;
17
+ set type(value: ButtonType);
5
18
  get disabled(): boolean;
6
19
  set disabled(value: boolean);
7
20
  get label(): string;
@@ -11,6 +24,8 @@ export declare class BoxButtonElement extends BaseElement {
11
24
  get tone(): string;
12
25
  set tone(value: string);
13
26
  protected renderTemplate(): void;
27
+ protected setupListeners(): void;
28
+ private syncIconVisibility;
14
29
  protected update(): void;
15
30
  }
16
31
  export declare const defineBoxButtonElement: (tagName?: string) => typeof BoxButtonElement;
@@ -120,12 +120,79 @@ const buttonStyles = `
120
120
  opacity: ${boeControl.disabledOpacity};
121
121
  box-shadow: none;
122
122
  }
123
+
124
+ button[aria-busy="true"] {
125
+ cursor: default;
126
+ }
127
+
128
+ /* Leading icon slot — hidden when nothing is slotted (kept off the flex gap). */
129
+ [part="icon"] {
130
+ display: inline-flex;
131
+ width: 16px;
132
+ height: 16px;
133
+ }
134
+
135
+ [part="icon"][hidden] {
136
+ display: none;
137
+ }
138
+
139
+ [part="icon"] ::slotted(svg) {
140
+ width: 16px;
141
+ height: 16px;
142
+ }
143
+
144
+ /* Inline loading spinner, shown while is-loading. */
145
+ [part="spinner"] {
146
+ width: 14px;
147
+ height: 14px;
148
+ border-radius: 50%;
149
+ border: 2px solid currentColor;
150
+ border-top-color: transparent;
151
+ animation: boe-btn-spin 0.7s linear infinite;
152
+ }
153
+
154
+ [part="spinner"][hidden] {
155
+ display: none;
156
+ }
157
+
158
+ @keyframes boe-btn-spin {
159
+ to { transform: rotate(360deg); }
160
+ }
161
+
162
+ @media (prefers-reduced-motion: reduce) {
163
+ [part="spinner"] { animation-duration: 1.6s; }
164
+ }
123
165
  `;
124
166
  export class BoxButtonElement extends BaseElement {
167
+ static formAssociated = true;
125
168
  static get observedAttributes() {
126
- return ["disabled", "label", "size", "tone"];
169
+ return ["disabled", "label", "size", "tone", "is-loading", "type"];
127
170
  }
128
171
  buttonEl;
172
+ spinnerEl;
173
+ iconEl;
174
+ labelEl;
175
+ iconSlot;
176
+ internals;
177
+ constructor() {
178
+ super();
179
+ // Form association lets type="submit"/"reset" drive the owning form.
180
+ this.internals = this.attachInternals();
181
+ }
182
+ /** Loading state: shows a spinner and blocks activation (stays focusable). */
183
+ get isLoading() {
184
+ return this.hasAttribute("is-loading");
185
+ }
186
+ set isLoading(value) {
187
+ this.toggleAttribute("is-loading", Boolean(value));
188
+ }
189
+ get type() {
190
+ const type = this.getAttribute("type");
191
+ return type === "submit" || type === "reset" ? type : "button";
192
+ }
193
+ set type(value) {
194
+ this.setAttribute("type", value);
195
+ }
129
196
  get disabled() {
130
197
  return this.hasAttribute("disabled");
131
198
  }
@@ -161,9 +228,40 @@ export class BoxButtonElement extends BaseElement {
161
228
  }
162
229
  this.shadowRoot.innerHTML = `
163
230
  <style>${buttonStyles}</style>
164
- <button type="button" part="button"></button>
231
+ <button type="button" part="button">
232
+ <span part="spinner" aria-hidden="true" hidden></span>
233
+ <span part="icon" hidden><slot name="icon"></slot></span>
234
+ <span part="label"></span>
235
+ </button>
165
236
  `;
166
237
  this.buttonEl = this.shadowRoot.querySelector('[part="button"]');
238
+ this.spinnerEl = this.shadowRoot.querySelector('[part="spinner"]');
239
+ this.iconEl = this.shadowRoot.querySelector('[part="icon"]');
240
+ this.labelEl = this.shadowRoot.querySelector('[part="label"]');
241
+ this.iconSlot = this.iconEl.querySelector("slot");
242
+ }
243
+ setupListeners() {
244
+ this.buttonEl.addEventListener("click", event => {
245
+ if (this.disabled || this.isLoading) {
246
+ event.preventDefault();
247
+ event.stopImmediatePropagation();
248
+ return;
249
+ }
250
+ // Drive the owning form for submit/reset (custom elements don't do this
251
+ // natively). Prefer the form-associated form; fall back to the nearest
252
+ // ancestor form so it works without full ElementInternals.form support.
253
+ const form = this.internals.form ?? this.closest("form");
254
+ if (this.type === "submit") {
255
+ form?.requestSubmit();
256
+ }
257
+ else if (this.type === "reset") {
258
+ form?.reset();
259
+ }
260
+ });
261
+ this.iconSlot.addEventListener("slotchange", () => this.syncIconVisibility());
262
+ }
263
+ syncIconVisibility() {
264
+ this.iconEl.hidden = this.iconSlot.assignedNodes({ flatten: true }).length === 0;
167
265
  }
168
266
  update() {
169
267
  if (!this.buttonEl) {
@@ -171,13 +269,19 @@ export class BoxButtonElement extends BaseElement {
171
269
  }
172
270
  this.buttonEl.dataset.tone = this.tone;
173
271
  this.buttonEl.dataset.size = this.size;
272
+ this.labelEl.textContent = this.label;
273
+ const busy = this.isLoading;
274
+ this.spinnerEl.hidden = !busy;
275
+ this.buttonEl.setAttribute("aria-busy", String(busy));
276
+ // A loading button stays focusable but inert; a disabled one is fully off.
174
277
  if (this.disabled) {
175
278
  this.buttonEl.setAttribute("disabled", "");
176
279
  }
177
280
  else {
178
281
  this.buttonEl.removeAttribute("disabled");
179
282
  }
180
- this.buttonEl.textContent = this.label;
283
+ this.buttonEl.setAttribute("aria-disabled", String(this.disabled || busy));
284
+ this.syncIconVisibility();
181
285
  }
182
286
  }
183
287
  export const defineBoxButtonElement = (tagName = DEFAULT_TAG_NAME) => {
@@ -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) => {
@@ -3,6 +3,14 @@ type BoxMenuItem = {
3
3
  disabled?: boolean;
4
4
  id: string;
5
5
  label: string;
6
+ /** Render a divider before this item. */
7
+ separator?: boolean;
8
+ /** Render a non-interactive section header instead of a menuitem. */
9
+ header?: boolean;
10
+ /** Render as a link (navigates on activate). */
11
+ href?: string;
12
+ /** Checkable item — renders menuitemcheckbox with aria-checked. */
13
+ checked?: boolean;
6
14
  };
7
15
  export declare class BoxMenuElement extends BaseElement {
8
16
  static get observedAttributes(): string[];
@@ -13,6 +21,8 @@ export declare class BoxMenuElement extends BaseElement {
13
21
  set label(value: string);
14
22
  get items(): BoxMenuItem[];
15
23
  set items(value: BoxMenuItem[]);
24
+ /** Render one item: header, separator + item, link, checkable, or plain. */
25
+ private renderItem;
16
26
  protected renderTemplate(): void;
17
27
  protected setupListeners(): void;
18
28
  private enabledMenuItems;
@@ -1,6 +1,6 @@
1
1
  import { BaseElement } from "../../core/index.js";
2
2
  import { applyRovingTabindex, focusRovingItem, nextRovingIndex, } from "../../foundations/a11y/index.js";
3
- import { boeControl, boeOverlay } from "../../foundations/geometry/index.js";
3
+ import { boeControl, boeOverlay, boeSpace } from "../../foundations/geometry/index.js";
4
4
  import { boeNeutralInteractiveStyles } from "../../foundations/tokens/index.js";
5
5
  import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
6
6
  const DEFAULT_TAG_NAME = "box-menu";
@@ -62,6 +62,41 @@ const menuStyles = `
62
62
  [part="menu-item"]:disabled {
63
63
  opacity: ${boeControl.disabledOpacity};
64
64
  }
65
+
66
+ /* Link items keep the menu-item chrome but render as anchors. */
67
+ a[part="menu-item"] {
68
+ display: flex;
69
+ align-items: center;
70
+ text-decoration: none;
71
+ }
72
+
73
+ [part="menu-separator"] {
74
+ height: 1px;
75
+ margin: ${boeSpace[1]} 0;
76
+ background: var(--boe-token-stroke-stroke, #e8e8e8);
77
+ }
78
+
79
+ [part="menu-header"] {
80
+ padding: ${boeSpace[1]} ${boeSpace[2]};
81
+ font-size: 11px;
82
+ font-weight: 700;
83
+ letter-spacing: 0.06em;
84
+ text-transform: uppercase;
85
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
86
+ }
87
+
88
+ /* Check indicator for checkable items. */
89
+ [part="menu-item"][role="menuitemcheckbox"]::before {
90
+ content: "";
91
+ display: inline-block;
92
+ width: 1em;
93
+ margin-inline-end: ${boeSpace[1]};
94
+ flex-shrink: 0;
95
+ }
96
+
97
+ [part="menu-item"][aria-checked="true"]::before {
98
+ content: "✓";
99
+ }
65
100
  `;
66
101
  export class BoxMenuElement extends BaseElement {
67
102
  static get observedAttributes() {
@@ -101,6 +136,27 @@ export class BoxMenuElement extends BaseElement {
101
136
  set items(value) {
102
137
  this.setAttribute("items", JSON.stringify(value));
103
138
  }
139
+ /** Render one item: header, separator + item, link, checkable, or plain. */
140
+ renderItem(item) {
141
+ const separator = item.separator ? `<div part="menu-separator" role="separator"></div>` : "";
142
+ if (item.header) {
143
+ return `${separator}<div part="menu-header" role="presentation">${escapeHtml(item.label)}</div>`;
144
+ }
145
+ const disabled = this.disabled || item.disabled;
146
+ const id = escapeHtml(item.id);
147
+ const label = escapeHtml(item.label);
148
+ // Checkable item — role=menuitemcheckbox with aria-checked.
149
+ if (typeof item.checked === "boolean") {
150
+ const checkedAttr = item.checked ? ' aria-checked="true"' : ' aria-checked="false"';
151
+ return `${separator}<button type="button" part="menu-item" role="menuitemcheckbox" data-item-id="${id}"${checkedAttr} ${disabled ? "disabled" : ""}>${label}</button>`;
152
+ }
153
+ // Link item — an anchor styled as a menuitem.
154
+ if (item.href) {
155
+ const aria = disabled ? ' aria-disabled="true"' : "";
156
+ return `${separator}<a part="menu-item" role="menuitem" data-item-id="${id}" href="${escapeHtml(item.href)}" tabindex="-1"${aria}>${label}</a>`;
157
+ }
158
+ return `${separator}<button type="button" part="menu-item" role="menuitem" data-item-id="${id}" ${disabled ? "disabled" : ""}>${label}</button>`;
159
+ }
104
160
  renderTemplate() {
105
161
  if (!this.shadowRoot) {
106
162
  return;
@@ -113,15 +169,22 @@ export class BoxMenuElement extends BaseElement {
113
169
  }
114
170
  setupListeners() {
115
171
  this.menuEl.addEventListener("click", event => {
116
- const button = event.target?.closest('[part="menu-item"]');
117
- if (!button || !this.menuEl.contains(button) || button.disabled) {
172
+ const el = event.target?.closest('[part="menu-item"]');
173
+ // Anchors have no `.disabled`; both use aria-disabled / native disabled.
174
+ const isDisabled = el?.hasAttribute("disabled") || el?.getAttribute("aria-disabled") === "true";
175
+ if (!el || !this.menuEl.contains(el) || isDisabled) {
118
176
  return;
119
177
  }
120
- const itemId = button.getAttribute("data-item-id");
178
+ const itemId = el.getAttribute("data-item-id");
121
179
  const item = this.items.find(entry => entry.id === itemId);
122
180
  if (!item || this.disabled || item.disabled) {
123
181
  return;
124
182
  }
183
+ // Checkable items toggle their aria-checked optimistically; the host owns
184
+ // the source of truth and can re-render from the event.
185
+ if (typeof item.checked === "boolean") {
186
+ el.setAttribute("aria-checked", String(!item.checked));
187
+ }
125
188
  this.dispatchEvent(new CustomEvent("item-selected", {
126
189
  bubbles: true,
127
190
  composed: true,
@@ -157,13 +220,7 @@ export class BoxMenuElement extends BaseElement {
157
220
  return;
158
221
  }
159
222
  this.menuEl.setAttribute("aria-label", this.label);
160
- this.menuEl.innerHTML = this.items
161
- .map(item => `
162
- <button type="button" part="menu-item" role="menuitem" data-item-id="${escapeHtml(item.id)}" ${this.disabled || item.disabled ? "disabled" : ""}>
163
- ${escapeHtml(item.label)}
164
- </button>
165
- `)
166
- .join("");
223
+ this.menuEl.innerHTML = this.items.map(item => this.renderItem(item)).join("");
167
224
  const buttons = this.enabledMenuItems();
168
225
  applyRovingTabindex(buttons, 0);
169
226
  }
@@ -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;