@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
@@ -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
  }
@@ -57,6 +57,17 @@ const errorMaskStyles = `
57
57
  display: none;
58
58
  }
59
59
 
60
+ /* Optional rich body — slotted markup below the message.
61
+ Hidden (no column gap) until content is assigned. */
62
+ [part="body"] {
63
+ display: block;
64
+ max-width: 32rem;
65
+ }
66
+
67
+ [part="body"]:not(.has-content) {
68
+ display: none;
69
+ }
70
+
60
71
  [part="action"] {
61
72
  appearance: none;
62
73
  box-sizing: border-box;
@@ -100,6 +111,7 @@ export class BoxErrorMaskElement extends BaseElement {
100
111
  }
101
112
  titleEl;
102
113
  messageEl;
114
+ bodySlot;
103
115
  actionEl = null;
104
116
  get heading() {
105
117
  return this.getAttribute("heading") ?? "Something went wrong";
@@ -141,10 +153,19 @@ export class BoxErrorMaskElement extends BaseElement {
141
153
  </span>
142
154
  <h2 part="title"></h2>
143
155
  <span part="message description" hidden></span>
156
+ <slot part="body"></slot>
144
157
  </section>
145
158
  `;
146
159
  this.titleEl = this.shadowRoot.querySelector('[part="title"]');
147
160
  this.messageEl = this.shadowRoot.querySelector('[part~="message"]');
161
+ this.bodySlot = this.shadowRoot.querySelector('slot[part="body"]');
162
+ }
163
+ setupListeners() {
164
+ this.bodySlot.addEventListener("slotchange", () => {
165
+ if (this.isRendered) {
166
+ this.update();
167
+ }
168
+ });
148
169
  }
149
170
  syncActionButton() {
150
171
  const section = this.shadowRoot?.querySelector('[part="error-mask"]');
@@ -178,6 +199,8 @@ export class BoxErrorMaskElement extends BaseElement {
178
199
  return;
179
200
  }
180
201
  this.titleEl.textContent = this.heading;
202
+ const hasBody = this.bodySlot.assignedNodes({ flatten: true }).length > 0;
203
+ this.bodySlot.classList.toggle("has-content", hasBody);
181
204
  if (this.message) {
182
205
  this.messageEl.hidden = false;
183
206
  this.messageEl.textContent = this.message;
@@ -152,6 +152,13 @@ export class BoxHelpTextElement extends BaseElement {
152
152
  : "info";
153
153
  const iconMarkup = resolveDesignIcon(iconName) ?? "i";
154
154
  this.helpTextEl.dataset.tone = this.tone;
155
+ // Error help text is an inline validation error: announce it assertively.
156
+ if (this.tone === "error") {
157
+ this.helpTextEl.setAttribute("role", "alert");
158
+ }
159
+ else {
160
+ this.helpTextEl.setAttribute("role", "note");
161
+ }
155
162
  this.helpTextEl.setAttribute("aria-label", this.label || "Help text");
156
163
  this.iconEl.setAttribute("data-icon-source", iconMarkup === "i" ? "text" : "design-system");
157
164
  this.iconEl.innerHTML = iconMarkup;
@@ -2,8 +2,12 @@ import { BaseElement } from "../../core/index.js";
2
2
  export declare class BoxSpinnerElement extends BaseElement {
3
3
  static get observedAttributes(): string[];
4
4
  private labelEl;
5
+ private indicatorEl;
5
6
  get label(): string;
6
7
  set label(value: string);
8
+ /** Indicator size: `small`, `medium` (default), or `large`. */
9
+ get size(): string;
10
+ set size(value: string);
7
11
  protected renderTemplate(): void;
8
12
  protected update(): void;
9
13
  }
@@ -25,6 +25,18 @@ const spinnerStyles = `
25
25
  animation: boe-spinner-rotate ${boeMotionDuration.spin} ${boeMotionEasing.linear} infinite;
26
26
  }
27
27
 
28
+ [part="indicator"][data-size="small"] {
29
+ width: 0.85rem;
30
+ height: 0.85rem;
31
+ border-width: 2px;
32
+ }
33
+
34
+ [part="indicator"][data-size="large"] {
35
+ width: 1.6rem;
36
+ height: 1.6rem;
37
+ border-width: 3px;
38
+ }
39
+
28
40
  @keyframes boe-spinner-rotate {
29
41
  to {
30
42
  transform: rotate(360deg);
@@ -41,15 +53,24 @@ const spinnerStyles = `
41
53
  `;
42
54
  export class BoxSpinnerElement extends BaseElement {
43
55
  static get observedAttributes() {
44
- return ["label"];
56
+ return ["label", "size"];
45
57
  }
46
58
  labelEl;
59
+ indicatorEl;
47
60
  get label() {
48
61
  return this.getAttribute("label") ?? "Loading";
49
62
  }
50
63
  set label(value) {
51
64
  this.setAttribute("label", value);
52
65
  }
66
+ /** Indicator size: `small`, `medium` (default), or `large`. */
67
+ get size() {
68
+ const value = this.getAttribute("size");
69
+ return value === "small" || value === "large" ? value : "medium";
70
+ }
71
+ set size(value) {
72
+ this.setAttribute("size", value);
73
+ }
53
74
  renderTemplate() {
54
75
  if (!this.shadowRoot) {
55
76
  return;
@@ -62,12 +83,19 @@ export class BoxSpinnerElement extends BaseElement {
62
83
  </div>
63
84
  `;
64
85
  this.labelEl = this.shadowRoot.querySelector('[part="label"]');
86
+ this.indicatorEl = this.shadowRoot.querySelector('[part="indicator"]');
65
87
  }
66
88
  update() {
67
- if (!this.labelEl) {
89
+ if (!this.labelEl || !this.indicatorEl) {
68
90
  return;
69
91
  }
70
92
  this.labelEl.textContent = this.label;
93
+ if (this.size === "medium") {
94
+ this.indicatorEl.removeAttribute("data-size");
95
+ }
96
+ else {
97
+ this.indicatorEl.dataset.size = this.size;
98
+ }
71
99
  }
72
100
  }
73
101
  export const defineBoxSpinnerElement = (tagName = DEFAULT_TAG_NAME) => {
@@ -5,9 +5,14 @@ export declare class BoxToastElement extends BaseElement {
5
5
  private timeoutId;
6
6
  private toastEl;
7
7
  private messageEl;
8
+ private actionSlot;
8
9
  private dismissEl;
10
+ /** Auto-dismiss delay in ms for the declarative `open` path. 0 = sticky. */
11
+ get duration(): number;
12
+ set duration(value: number);
9
13
  get open(): boolean;
10
14
  set open(value: boolean);
15
+ private scheduleAutoDismiss;
11
16
  get message(): string;
12
17
  set message(value: string);
13
18
  get tone(): string;
@@ -53,6 +53,28 @@ const toastStyles = `
53
53
  font-size: 15px;
54
54
  font-weight: 700;
55
55
  line-height: 1.35;
56
+ /* Long unbroken tokens (URLs, filenames) wrap instead of overflowing. */
57
+ overflow-wrap: anywhere;
58
+ }
59
+
60
+ /* Optional action affordance (e.g. "Undo") before the dismiss button.
61
+ Hidden — taking no flex gap — until content is assigned. */
62
+ [part="action"] {
63
+ display: inline-flex;
64
+ align-items: center;
65
+ flex: none;
66
+ }
67
+
68
+ [part="action"]:not(.has-content) {
69
+ display: none;
70
+ }
71
+
72
+ [part="action"]::slotted(*) {
73
+ font: inherit;
74
+ font-size: 13px;
75
+ font-weight: 700;
76
+ color: var(--boe-token-surface-surface-brand, #0061d5);
77
+ cursor: pointer;
56
78
  }
57
79
 
58
80
  [part="dismiss"] {
@@ -88,13 +110,22 @@ const toastStyles = `
88
110
  `;
89
111
  export class BoxToastElement extends BaseElement {
90
112
  static get observedAttributes() {
91
- return ["message", "open", "tone"];
113
+ return ["message", "open", "tone", "duration"];
92
114
  }
93
115
  openValue = false;
94
116
  timeoutId = null;
95
117
  toastEl;
96
118
  messageEl;
119
+ actionSlot;
97
120
  dismissEl;
121
+ /** Auto-dismiss delay in ms for the declarative `open` path. 0 = sticky. */
122
+ get duration() {
123
+ const raw = Number(this.getAttribute("duration"));
124
+ return Number.isFinite(raw) && raw >= 0 ? raw : 0;
125
+ }
126
+ set duration(value) {
127
+ this.setAttribute("duration", String(value));
128
+ }
98
129
  get open() {
99
130
  return this.openValue;
100
131
  }
@@ -110,11 +141,29 @@ export class BoxToastElement extends BaseElement {
110
141
  else {
111
142
  this.removeAttribute("open");
112
143
  }
144
+ if (nextOpen) {
145
+ // Declarative open (attribute/property) auto-dismisses per `duration`;
146
+ // show() reschedules afterwards with its own option when provided.
147
+ this.scheduleAutoDismiss(this.duration);
148
+ }
149
+ else if (this.timeoutId) {
150
+ clearTimeout(this.timeoutId);
151
+ this.timeoutId = null;
152
+ }
113
153
  this.dispatchEvent(new CustomEvent("open-changed", { bubbles: true, composed: true, detail: { open: nextOpen } }));
114
154
  if (this.isRendered) {
115
155
  this.update();
116
156
  }
117
157
  }
158
+ scheduleAutoDismiss(duration) {
159
+ if (this.timeoutId) {
160
+ clearTimeout(this.timeoutId);
161
+ this.timeoutId = null;
162
+ }
163
+ if (duration > 0) {
164
+ this.timeoutId = setTimeout(() => this.hide(), duration);
165
+ }
166
+ }
118
167
  get message() {
119
168
  return this.getAttribute("message") ?? "";
120
169
  }
@@ -147,16 +196,9 @@ export class BoxToastElement extends BaseElement {
147
196
  this.tone = options.tone;
148
197
  }
149
198
  this.open = true;
150
- if (this.timeoutId) {
151
- clearTimeout(this.timeoutId);
152
- this.timeoutId = null;
153
- }
154
- const duration = options?.duration ?? 2500;
155
- if (duration > 0) {
156
- this.timeoutId = setTimeout(() => {
157
- this.hide();
158
- }, duration);
159
- }
199
+ // options.duration wins; else a declarative `duration` attribute; else 2500.
200
+ const duration = options?.duration ?? (this.duration || 2500);
201
+ this.scheduleAutoDismiss(duration);
160
202
  }
161
203
  hide() {
162
204
  this.open = false;
@@ -169,11 +211,13 @@ export class BoxToastElement extends BaseElement {
169
211
  <style>${toastStyles}</style>
170
212
  <div part="toast" role="status" aria-live="polite">
171
213
  <span part="message"></span>
214
+ <slot name="action" part="action"></slot>
172
215
  <button type="button" part="dismiss">Dismiss</button>
173
216
  </div>
174
217
  `;
175
218
  this.toastEl = this.shadowRoot.querySelector('[part="toast"]');
176
219
  this.messageEl = this.shadowRoot.querySelector('[part="message"]');
220
+ this.actionSlot = this.shadowRoot.querySelector('slot[name="action"]');
177
221
  this.dismissEl = this.shadowRoot.querySelector('[part="dismiss"]');
178
222
  }
179
223
  setupListeners() {
@@ -181,6 +225,10 @@ export class BoxToastElement extends BaseElement {
181
225
  this.dispatchEvent(new CustomEvent("dismiss", { bubbles: true, composed: true }));
182
226
  this.hide();
183
227
  });
228
+ this.actionSlot.addEventListener("slotchange", () => {
229
+ const hasContent = this.actionSlot.assignedNodes({ flatten: true }).length > 0;
230
+ this.actionSlot.classList.toggle("has-content", hasContent);
231
+ });
184
232
  }
185
233
  update() {
186
234
  if (!this.toastEl) {
@@ -17,6 +17,12 @@ export declare class BoxCategorySelectorElement extends BaseElement {
17
17
  private lastOptionsJson;
18
18
  private hadFocus;
19
19
  private rootEl;
20
+ private moreOpen;
21
+ private positionCleanup;
22
+ private readonly onDocumentPointerDown;
23
+ /** Max inline categories before the rest collapse into a "More" menu (0 = no limit). */
24
+ get maxLinks(): number;
25
+ set maxLinks(value: number);
20
26
  get label(): string;
21
27
  set label(value: string);
22
28
  get options(): CategoryOption[];
@@ -25,9 +31,14 @@ export declare class BoxCategorySelectorElement extends BaseElement {
25
31
  set value(nextValue: string);
26
32
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
27
33
  private selectableOptions;
34
+ private splitOptions;
28
35
  private select;
29
36
  private moveSelection;
30
37
  protected renderTemplate(): void;
38
+ private resolvedPlacement;
39
+ private openMore;
40
+ private closeMore;
41
+ disconnectedCallback(): void;
31
42
  protected setupListeners(): void;
32
43
  protected update(): void;
33
44
  }