@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
@@ -1,6 +1,10 @@
1
1
  import { FormAssociatedElement, boeFormFieldErrorStyles, boeFormFieldSupportStyles, formDescriptionMarkup, formErrorMessageMarkup, } from "../../core/index.js";
2
2
  import { boeControl, boeInputControlStyles, boeSpace } from "../../foundations/geometry/index.js";
3
+ import { boeMotionDuration, boeMotionEasing, boeReducedMotionStyles, } from "../../foundations/motion/index.js";
3
4
  const DEFAULT_TAG_NAME = "box-text-field";
5
+ const VALID_INPUT_TYPES = new Set(["text", "email", "tel", "url", "password", "search", "number"]);
6
+ const spinnerMarkup = '<span part="spinner" aria-hidden="true"></span>';
7
+ const validMarkup = '<svg part="valid-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M6.5 11L3 7.5l1-1 2.5 2.5L12 3.5l1 1z" fill="currentColor"/></svg>';
4
8
  /** Input chrome tracks BUE `@mixin box-inputs` (`src/styles/_inputs.scss`). */
5
9
  const textFieldStyles = `
6
10
  :host {
@@ -33,6 +37,78 @@ const textFieldStyles = `
33
37
  color: var(--boe-token-text-text-placeholder, #909090);
34
38
  }
35
39
 
40
+ /* Control wrapper holds the input plus optional leading icon and trailing
41
+ status. With no adornments it is layout-neutral (default renders unchanged). */
42
+ [part="control"] {
43
+ position: relative;
44
+ display: block;
45
+ }
46
+
47
+ [part="icon"] {
48
+ position: absolute;
49
+ inset-block-start: 50%;
50
+ inset-inline-start: 0.6rem;
51
+ transform: translateY(-50%);
52
+ display: inline-flex;
53
+ align-items: center;
54
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
55
+ pointer-events: none;
56
+ }
57
+
58
+ [part="icon"]:not(.has-content) {
59
+ display: none;
60
+ }
61
+
62
+ [part="icon"]::slotted(*) {
63
+ inline-size: 1rem;
64
+ block-size: 1rem;
65
+ display: block;
66
+ }
67
+
68
+ [part="control"][data-has-icon="true"] [part="input"] {
69
+ padding-inline-start: 2.1rem;
70
+ }
71
+
72
+ [part="status"] {
73
+ position: absolute;
74
+ inset-block-start: 50%;
75
+ inset-inline-end: 0.65rem;
76
+ transform: translateY(-50%);
77
+ display: none;
78
+ align-items: center;
79
+ }
80
+
81
+ [part="control"][data-status="loading"] [part="status"],
82
+ [part="control"][data-status="valid"] [part="status"] {
83
+ display: inline-flex;
84
+ }
85
+
86
+ [part="control"][data-status="loading"] [part="input"],
87
+ [part="control"][data-status="valid"] [part="input"] {
88
+ padding-inline-end: 2.2rem;
89
+ }
90
+
91
+ [part="spinner"] {
92
+ inline-size: 1rem;
93
+ block-size: 1rem;
94
+ border-radius: 999px;
95
+ border: 2px solid color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 20%, transparent);
96
+ border-top-color: var(--boe-token-surface-surface-brand, #0061d5);
97
+ animation: boe-text-field-spin ${boeMotionDuration.spin} ${boeMotionEasing.linear} infinite;
98
+ }
99
+
100
+ @keyframes boe-text-field-spin {
101
+ to { transform: rotate(360deg); }
102
+ }
103
+
104
+ ${boeReducedMotionStyles('[part="spinner"]', "animation-duration: 1.6s;")}
105
+
106
+ [part="valid-icon"] {
107
+ inline-size: 1.05rem;
108
+ block-size: 1.05rem;
109
+ color: var(--boe-token-surface-status-surface-success, #26c281);
110
+ }
111
+
36
112
  ${boeFormFieldErrorStyles}
37
113
  ${boeFormFieldSupportStyles}
38
114
  `;
@@ -43,6 +119,9 @@ export class BoxTextFieldElement extends FormAssociatedElement {
43
119
  "disabled",
44
120
  "label",
45
121
  "placeholder",
122
+ "type",
123
+ "loading",
124
+ "valid",
46
125
  "value",
47
126
  ];
48
127
  }
@@ -51,6 +130,31 @@ export class BoxTextFieldElement extends FormAssociatedElement {
51
130
  labelEl;
52
131
  descriptionEl;
53
132
  errorEl;
133
+ controlEl;
134
+ iconSlot;
135
+ statusEl;
136
+ /** Input type passthrough (text/email/tel/url/password/search/number). */
137
+ get type() {
138
+ const value = this.getAttribute("type");
139
+ return value && VALID_INPUT_TYPES.has(value) ? value : "text";
140
+ }
141
+ set type(value) {
142
+ this.setAttribute("type", value);
143
+ }
144
+ /** Shows a trailing spinner (e.g. while validating/looking up asynchronously). */
145
+ get loading() {
146
+ return this.hasAttribute("loading");
147
+ }
148
+ set loading(value) {
149
+ this.toggleAttribute("loading", Boolean(value));
150
+ }
151
+ /** Shows a trailing success check (explicit valid affirmation). */
152
+ get valid() {
153
+ return this.hasAttribute("valid");
154
+ }
155
+ set valid(value) {
156
+ this.toggleAttribute("valid", Boolean(value));
157
+ }
54
158
  get value() {
55
159
  return this.valueInternal;
56
160
  }
@@ -105,7 +209,11 @@ export class BoxTextFieldElement extends FormAssociatedElement {
105
209
  <label part="field">
106
210
  <span part="label"></span>
107
211
  ${formDescriptionMarkup()}
108
- <input type="text" part="input" />
212
+ <span part="control">
213
+ <slot name="icon" part="icon"></slot>
214
+ <input type="text" part="input" />
215
+ <span part="status" aria-hidden="true"></span>
216
+ </span>
109
217
  ${formErrorMessageMarkup()}
110
218
  </label>
111
219
  `;
@@ -113,8 +221,12 @@ export class BoxTextFieldElement extends FormAssociatedElement {
113
221
  this.descriptionEl = this.shadowRoot.querySelector('[part="description"]');
114
222
  this.inputEl = this.shadowRoot.querySelector('[part="input"]');
115
223
  this.errorEl = this.shadowRoot.querySelector('[part="error-message"]');
224
+ this.controlEl = this.shadowRoot.querySelector('[part="control"]');
225
+ this.iconSlot = this.shadowRoot.querySelector('slot[name="icon"]');
226
+ this.statusEl = this.shadowRoot.querySelector('[part="status"]');
116
227
  }
117
228
  setupListeners() {
229
+ this.iconSlot.addEventListener("slotchange", () => this.syncIconSlot());
118
230
  this.inputEl.addEventListener("input", event => {
119
231
  const nextValue = event.currentTarget.value;
120
232
  this.valueInternal = nextValue;
@@ -127,12 +239,35 @@ export class BoxTextFieldElement extends FormAssociatedElement {
127
239
  }));
128
240
  });
129
241
  }
242
+ syncIconSlot() {
243
+ if (!this.iconSlot || !this.controlEl) {
244
+ return;
245
+ }
246
+ const hasContent = this.iconSlot.assignedNodes({ flatten: true }).length > 0;
247
+ this.iconSlot.classList.toggle("has-content", hasContent);
248
+ this.controlEl.dataset.hasIcon = hasContent ? "true" : "false";
249
+ }
130
250
  update() {
131
251
  if (!this.inputEl || !this.labelEl || !this.errorEl) {
132
252
  return;
133
253
  }
134
254
  this.labelEl.textContent = this.label;
255
+ this.inputEl.type = this.type;
135
256
  this.inputEl.placeholder = this.placeholder;
257
+ this.syncIconSlot();
258
+ // Trailing status: loading spinner takes precedence over the valid check.
259
+ if (this.loading) {
260
+ this.controlEl.dataset.status = "loading";
261
+ this.statusEl.innerHTML = spinnerMarkup;
262
+ }
263
+ else if (this.valid) {
264
+ this.controlEl.dataset.status = "valid";
265
+ this.statusEl.innerHTML = validMarkup;
266
+ }
267
+ else {
268
+ this.controlEl.removeAttribute("data-status");
269
+ this.statusEl.innerHTML = "";
270
+ }
136
271
  if (this.shadowRoot?.activeElement !== this.inputEl) {
137
272
  this.inputEl.value = this.valueInternal;
138
273
  }
@@ -21,6 +21,17 @@ export declare class BoxTimeFieldElement extends FormAssociatedElement {
21
21
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
22
22
  protected getFormValue(): FormValue;
23
23
  protected restoreFormValue(value: FormValue): void;
24
+ /**
25
+ * Parse a human time string in 24-hour (`13:30`) or 12-hour (`1:30 PM`,
26
+ * `1 pm`) form to canonical `HH:MM`. Returns `""` for empty input and `null`
27
+ * when it cannot be parsed.
28
+ */
29
+ static parseTime(raw: string): string | null;
30
+ /**
31
+ * Set the value from a human 12h/24h string. On success updates the value and
32
+ * returns true; on a parse failure emits `parse-error` and returns false.
33
+ */
34
+ setTimeString(value: string): boolean;
24
35
  private syncValue;
25
36
  protected renderTemplate(): void;
26
37
  protected setupListeners(): void;
@@ -132,6 +132,57 @@ export class BoxTimeFieldElement extends FormAssociatedElement {
132
132
  this.update();
133
133
  }
134
134
  }
135
+ /**
136
+ * Parse a human time string in 24-hour (`13:30`) or 12-hour (`1:30 PM`,
137
+ * `1 pm`) form to canonical `HH:MM`. Returns `""` for empty input and `null`
138
+ * when it cannot be parsed.
139
+ */
140
+ static parseTime(raw) {
141
+ const value = raw.trim().toLowerCase();
142
+ if (!value) {
143
+ return "";
144
+ }
145
+ const twentyFour = value.match(/^([01]?\d|2[0-3]):([0-5]\d)$/);
146
+ if (twentyFour) {
147
+ return `${twentyFour[1].padStart(2, "0")}:${twentyFour[2]}`;
148
+ }
149
+ const twelve = value.match(/^(\d{1,2})(?::([0-5]\d))?\s*(am|pm)$/);
150
+ if (twelve) {
151
+ let hours = Number.parseInt(twelve[1], 10);
152
+ const minutes = twelve[2] ?? "00";
153
+ if (hours < 1 || hours > 12) {
154
+ return null;
155
+ }
156
+ if (twelve[3] === "pm" && hours !== 12) {
157
+ hours += 12;
158
+ }
159
+ else if (twelve[3] === "am" && hours === 12) {
160
+ hours = 0;
161
+ }
162
+ return `${String(hours).padStart(2, "0")}:${minutes}`;
163
+ }
164
+ return null;
165
+ }
166
+ /**
167
+ * Set the value from a human 12h/24h string. On success updates the value and
168
+ * returns true; on a parse failure emits `parse-error` and returns false.
169
+ */
170
+ setTimeString(value) {
171
+ const parsed = BoxTimeFieldElement.parseTime(value);
172
+ if (parsed === null) {
173
+ this.dispatchEvent(new CustomEvent("parse-error", {
174
+ bubbles: true,
175
+ composed: true,
176
+ detail: { value },
177
+ }));
178
+ return false;
179
+ }
180
+ this.syncValue(parsed);
181
+ if (this.isRendered) {
182
+ this.update();
183
+ }
184
+ return true;
185
+ }
135
186
  syncValue(nextValue) {
136
187
  this.valueInternal = nextValue;
137
188
  this.setAttribute("value", nextValue);
@@ -2,6 +2,9 @@ import { BaseElement } from "../../core/index.js";
2
2
  export declare class BoxAvatarElement extends BaseElement {
3
3
  static get observedAttributes(): string[];
4
4
  private avatarEl;
5
+ /** Corner indicator: `""`/`none` (default), `online`, or `external`. */
6
+ get badge(): string;
7
+ set badge(value: string);
5
8
  get alt(): string;
6
9
  set alt(value: string);
7
10
  get initials(): string;
@@ -16,6 +19,7 @@ export declare class BoxAvatarElement extends BaseElement {
16
19
  set tone(value: string);
17
20
  protected renderTemplate(): void;
18
21
  protected update(): void;
22
+ private syncBadge;
19
23
  private showAvatarFallback;
20
24
  }
21
25
  export declare const defineBoxAvatarElement: (tagName?: string) => typeof BoxAvatarElement;
@@ -76,12 +76,49 @@ const avatarStyles = `
76
76
  [part="image"][hidden] {
77
77
  display: none;
78
78
  }
79
+
80
+ /* Corner status/external indicator. Absent by default (no layout impact). */
81
+ [part="badge"] {
82
+ position: absolute;
83
+ inset-block-end: 0;
84
+ inset-inline-end: 0;
85
+ box-sizing: border-box;
86
+ inline-size: max(8px, calc(var(--avatar-size, ${DEFAULT_SIZE}px) * 0.34));
87
+ block-size: max(8px, calc(var(--avatar-size, ${DEFAULT_SIZE}px) * 0.34));
88
+ border-radius: 999px;
89
+ border: 2px solid var(--boe-token-surface-surface, #ffffff);
90
+ display: grid;
91
+ place-items: center;
92
+ color: #ffffff;
93
+ }
94
+
95
+ [part="badge"][data-badge="online"] {
96
+ background: var(--boe-token-surface-status-surface-success, #26c281);
97
+ }
98
+
99
+ [part="badge"][data-badge="external"] {
100
+ background: var(--boe-token-surface-status-surface-inprogress, #f5b31b);
101
+ }
102
+
103
+ [part="badge"] svg {
104
+ inline-size: 62%;
105
+ block-size: 62%;
106
+ fill: currentColor;
107
+ }
79
108
  `;
80
109
  export class BoxAvatarElement extends BaseElement {
81
110
  static get observedAttributes() {
82
- return ["alt", "initials", "name", "size", "src", "tone"];
111
+ return ["alt", "badge", "initials", "name", "size", "src", "tone"];
83
112
  }
84
113
  avatarEl;
114
+ /** Corner indicator: `""`/`none` (default), `online`, or `external`. */
115
+ get badge() {
116
+ const value = this.getAttribute("badge");
117
+ return value === "online" || value === "external" ? value : "none";
118
+ }
119
+ set badge(value) {
120
+ this.setAttribute("badge", value);
121
+ }
85
122
  get alt() {
86
123
  return this.getAttribute("alt") ?? this.name;
87
124
  }
@@ -171,6 +208,33 @@ export class BoxAvatarElement extends BaseElement {
171
208
  }
172
209
  this.showAvatarFallback(existingFallback, fallback);
173
210
  }
211
+ this.syncBadge();
212
+ }
213
+ syncBadge() {
214
+ const badge = this.badge;
215
+ let badgeEl = this.avatarEl.querySelector('[part="badge"]');
216
+ if (badge === "none") {
217
+ badgeEl?.remove();
218
+ return;
219
+ }
220
+ if (!badgeEl) {
221
+ badgeEl = document.createElement("span");
222
+ badgeEl.setAttribute("part", "badge");
223
+ this.avatarEl.append(badgeEl);
224
+ }
225
+ badgeEl.dataset.badge = badge;
226
+ if (badge === "external") {
227
+ badgeEl.setAttribute("aria-label", "External user");
228
+ badgeEl.setAttribute("role", "img");
229
+ // Outward arrow marks an external collaborator.
230
+ badgeEl.innerHTML =
231
+ '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 3h7v7h-2V6.4l-6.3 6.3-1.4-1.4L9.6 5H6V3z"/></svg>';
232
+ }
233
+ else {
234
+ badgeEl.setAttribute("aria-label", "Online");
235
+ badgeEl.setAttribute("role", "img");
236
+ badgeEl.innerHTML = "";
237
+ }
174
238
  }
175
239
  showAvatarFallback(existingFallback, fallback) {
176
240
  const fallbackEl = existingFallback ?? document.createElement("span");
@@ -10,7 +10,15 @@ export declare class BoxContactDatalistItemElement extends BaseElement {
10
10
  private itemEl;
11
11
  private avatarEl;
12
12
  private nameEl;
13
+ private externalEl;
14
+ private subtitleEl;
13
15
  private emailEl;
16
+ /** Marks the contact as an external collaborator. */
17
+ get external(): boolean;
18
+ set external(value: boolean);
19
+ /** Optional secondary detail line below the email (e.g. role or company). */
20
+ get subtitle(): string;
21
+ set subtitle(value: string);
14
22
  get name(): string;
15
23
  set name(value: string);
16
24
  get email(): string;
@@ -71,6 +71,13 @@ const contactStyles = `
71
71
  gap: 0.1rem;
72
72
  }
73
73
 
74
+ [part="name-row"] {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 0.4rem;
78
+ min-inline-size: 0;
79
+ }
80
+
74
81
  [part="name"] {
75
82
  font-size: 0.9rem;
76
83
  font-weight: 600;
@@ -79,6 +86,35 @@ const contactStyles = `
79
86
  white-space: nowrap;
80
87
  }
81
88
 
89
+ /* External-collaborator marker beside the name. */
90
+ [part="external"] {
91
+ flex: none;
92
+ font-size: 0.66rem;
93
+ font-weight: 700;
94
+ letter-spacing: 0.04em;
95
+ text-transform: uppercase;
96
+ padding: 1px 5px;
97
+ border-radius: 999px;
98
+ background: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 18%, var(--boe-token-surface-surface, #ffffff) 82%);
99
+ color: color-mix(in srgb, var(--boe-token-surface-status-surface-inprogress, #f5b31b) 55%, var(--boe-token-text-text, #222222));
100
+ }
101
+
102
+ [part="external"][hidden] {
103
+ display: none;
104
+ }
105
+
106
+ [part="subtitle"] {
107
+ font-size: 0.74rem;
108
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
109
+ overflow: hidden;
110
+ text-overflow: ellipsis;
111
+ white-space: nowrap;
112
+ }
113
+
114
+ [part="subtitle"][hidden] {
115
+ display: none;
116
+ }
117
+
82
118
  [part="email"] {
83
119
  font-size: 0.78rem;
84
120
  color: var(--boe-token-text-text-secondary, #6f6f6f);
@@ -99,12 +135,28 @@ const contactStyles = `
99
135
  */
100
136
  export class BoxContactDatalistItemElement extends BaseElement {
101
137
  static get observedAttributes() {
102
- return ["disabled", "email", "name", "selected", "src", "value"];
138
+ return ["disabled", "email", "external", "name", "selected", "src", "subtitle", "value"];
103
139
  }
104
140
  itemEl;
105
141
  avatarEl;
106
142
  nameEl;
143
+ externalEl;
144
+ subtitleEl;
107
145
  emailEl;
146
+ /** Marks the contact as an external collaborator. */
147
+ get external() {
148
+ return this.hasAttribute("external");
149
+ }
150
+ set external(value) {
151
+ this.toggleAttribute("external", value);
152
+ }
153
+ /** Optional secondary detail line below the email (e.g. role or company). */
154
+ get subtitle() {
155
+ return this.getAttribute("subtitle") ?? "";
156
+ }
157
+ set subtitle(value) {
158
+ this.setAttribute("subtitle", value);
159
+ }
108
160
  get name() {
109
161
  return this.getAttribute("name") ?? "";
110
162
  }
@@ -160,14 +212,20 @@ export class BoxContactDatalistItemElement extends BaseElement {
160
212
  <div part="item" role="option">
161
213
  <span part="avatar" aria-hidden="true"></span>
162
214
  <span part="body">
163
- <span part="name"></span>
215
+ <span part="name-row">
216
+ <span part="name"></span>
217
+ <span part="external" hidden>External</span>
218
+ </span>
164
219
  <span part="email" hidden></span>
220
+ <span part="subtitle" hidden></span>
165
221
  </span>
166
222
  </div>
167
223
  `;
168
224
  this.itemEl = this.shadowRoot.querySelector('[part="item"]');
169
225
  this.avatarEl = this.shadowRoot.querySelector('[part="avatar"]');
170
226
  this.nameEl = this.shadowRoot.querySelector('[part="name"]');
227
+ this.externalEl = this.shadowRoot.querySelector('[part="external"]');
228
+ this.subtitleEl = this.shadowRoot.querySelector('[part="subtitle"]');
171
229
  this.emailEl = this.shadowRoot.querySelector('[part="email"]');
172
230
  }
173
231
  setupListeners() {
@@ -215,6 +273,14 @@ export class BoxContactDatalistItemElement extends BaseElement {
215
273
  }
216
274
  }
217
275
  this.nameEl.textContent = this.name;
276
+ const external = this.external;
277
+ this.externalEl.hidden = !external;
278
+ if (external) {
279
+ this.itemEl.setAttribute("aria-description", "External collaborator");
280
+ }
281
+ else {
282
+ this.itemEl.removeAttribute("aria-description");
283
+ }
218
284
  if (this.email) {
219
285
  this.emailEl.hidden = false;
220
286
  this.emailEl.textContent = this.email;
@@ -223,6 +289,9 @@ export class BoxContactDatalistItemElement extends BaseElement {
223
289
  this.emailEl.hidden = true;
224
290
  this.emailEl.textContent = "";
225
291
  }
292
+ const subtitle = this.subtitle;
293
+ this.subtitleEl.hidden = !subtitle;
294
+ this.subtitleEl.textContent = subtitle;
226
295
  }
227
296
  }
228
297
  export const defineBoxContactDatalistItemElement = (tagName = DEFAULT_TAG_NAME) => {
@@ -88,6 +88,29 @@ const navSidebarStyles = `
88
88
  background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 8%, var(--boe-token-surface-surface, #ffffff) 92%);
89
89
  }
90
90
 
91
+ /* Grouped nav (NavList): slot a [data-nav-group] element as a section header,
92
+ and <hr> as a divider. Group labels collapse to nothing in the icon strip. */
93
+ [part="body"] ::slotted([data-nav-group]) {
94
+ display: block;
95
+ padding: 0.5rem 0.45rem 0.15rem;
96
+ font-size: 0.72rem;
97
+ font-weight: 700;
98
+ letter-spacing: 0.06em;
99
+ text-transform: uppercase;
100
+ color: var(--boe-token-text-text-secondary, #6f6f6f);
101
+ }
102
+
103
+ [part="sidebar"][data-collapsed="true"] ::slotted([data-nav-group]) {
104
+ display: none;
105
+ }
106
+
107
+ [part="body"] ::slotted(hr) {
108
+ inline-size: 100%;
109
+ border: 0;
110
+ border-top: 1px solid color-mix(in srgb, var(--boe-token-stroke-stroke, #e8e8e8) 68%, transparent);
111
+ margin: 0.35rem 0.2rem;
112
+ }
113
+
91
114
  [part="sidebar"][data-collapsed="true"] ::slotted(a),
92
115
  [part="sidebar"][data-collapsed="true"] ::slotted(button) {
93
116
  justify-content: center;
@@ -9,6 +9,9 @@ import { BaseElement } from "../../core/index.js";
9
9
  export declare class BoxSidebarToggleButtonElement extends BaseElement {
10
10
  static get observedAttributes(): string[];
11
11
  private buttonEl;
12
+ /** Which side the companion sidebar sits on: `left` (default) or `right`. */
13
+ get direction(): string;
14
+ set direction(value: string);
12
15
  get expanded(): boolean;
13
16
  set expanded(value: boolean);
14
17
  get disabled(): boolean;
@@ -37,10 +37,19 @@ const sidebarToggleButtonStyles = `
37
37
  color: var(--boe-token-text-text, #222222);
38
38
  }
39
39
 
40
- /* Rotate the chevron to point the way the next activation will move. */
40
+ /* Point the glyph the way the next activation will move (left sidebar default). */
41
41
  [part="button"][data-expanded="false"] svg {
42
42
  transform: scaleX(-1);
43
43
  }
44
+
45
+ /* Right-side sidebar mirrors the direction. */
46
+ [part="button"][data-direction="right"][data-expanded="true"] svg {
47
+ transform: scaleX(-1);
48
+ }
49
+
50
+ [part="button"][data-direction="right"][data-expanded="false"] svg {
51
+ transform: none;
52
+ }
44
53
  `;
45
54
  /**
46
55
  * A disclosure control that expands and collapses a companion `box-nav-sidebar`.
@@ -51,9 +60,16 @@ const sidebarToggleButtonStyles = `
51
60
  */
52
61
  export class BoxSidebarToggleButtonElement extends BaseElement {
53
62
  static get observedAttributes() {
54
- return ["controls", "disabled", "expanded", "label"];
63
+ return ["controls", "disabled", "expanded", "label", "direction"];
55
64
  }
56
65
  buttonEl;
66
+ /** Which side the companion sidebar sits on: `left` (default) or `right`. */
67
+ get direction() {
68
+ return this.getAttribute("direction") === "right" ? "right" : "left";
69
+ }
70
+ set direction(value) {
71
+ this.setAttribute("direction", value);
72
+ }
57
73
  get expanded() {
58
74
  // Default to expanded: a sidebar renders open until the user collapses it.
59
75
  return !this.hasAttribute("expanded") || this.getAttribute("expanded") !== "false";
@@ -116,8 +132,11 @@ export class BoxSidebarToggleButtonElement extends BaseElement {
116
132
  }
117
133
  const expanded = this.expanded;
118
134
  this.buttonEl.dataset.expanded = expanded ? "true" : "false";
135
+ this.buttonEl.dataset.direction = this.direction;
119
136
  this.buttonEl.setAttribute("aria-expanded", expanded ? "true" : "false");
120
137
  this.buttonEl.setAttribute("aria-label", this.label);
138
+ // Native hover tooltip reflects the action the next click performs.
139
+ this.buttonEl.title = expanded ? "Collapse sidebar" : "Expand sidebar";
121
140
  if (this.controls) {
122
141
  this.buttonEl.setAttribute("aria-controls", this.controls);
123
142
  }
@@ -6,6 +6,9 @@ type BoxAccordionItem = {
6
6
  };
7
7
  export declare class BoxAccordionElement extends BaseElement {
8
8
  static get observedAttributes(): string[];
9
+ /** Flat variant with no outer card border/background. */
10
+ get borderless(): boolean;
11
+ set borderless(value: boolean);
9
12
  private valueInternal;
10
13
  private lastItemsJson;
11
14
  private accordionEl;
@@ -84,11 +84,26 @@ const accordionStyles = `
84
84
  display: none;
85
85
  }
86
86
 
87
+ /* Borderless variant: drop the outer card chrome, keep row dividers. */
88
+ :host([borderless]) [part="accordion"] {
89
+ border: none;
90
+ border-radius: 0;
91
+ background: transparent;
92
+ overflow: visible;
93
+ }
94
+
87
95
  ${boeNeutralInteractiveStyles('[part="trigger"]')}
88
96
  `;
89
97
  export class BoxAccordionElement extends BaseElement {
90
98
  static get observedAttributes() {
91
- return ["items", "label", "value"];
99
+ return ["items", "label", "value", "borderless"];
100
+ }
101
+ /** Flat variant with no outer card border/background. */
102
+ get borderless() {
103
+ return this.hasAttribute("borderless");
104
+ }
105
+ set borderless(value) {
106
+ this.toggleAttribute("borderless", Boolean(value));
92
107
  }
93
108
  valueInternal = "";
94
109
  lastItemsJson = "";
@@ -158,7 +173,7 @@ export class BoxAccordionElement extends BaseElement {
158
173
  role="region"
159
174
  aria-labelledby="${triggerId}"
160
175
  ${isOpen ? "" : "hidden"}
161
- >${escapeHtml(item.content ?? "")}</div>
176
+ ><slot name="panel-${escapeHtml(item.value)}">${escapeHtml(item.content ?? "")}</slot></div>
162
177
  </section>
163
178
  `;
164
179
  })