@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.
- package/dist/components/actions/button.d.ts +15 -0
- package/dist/components/actions/button.js +107 -3
- package/dist/components/actions/link-button.d.ts +9 -0
- package/dist/components/actions/link-button.js +55 -4
- package/dist/components/actions/menu.d.ts +10 -0
- package/dist/components/actions/menu.js +68 -11
- package/dist/components/collections/carousel.d.ts +7 -0
- package/dist/components/collections/carousel.js +71 -4
- package/dist/components/collections/datalist-item.d.ts +6 -0
- package/dist/components/collections/datalist-item.js +39 -1
- package/dist/components/collections/draggable-list.js +1 -1
- package/dist/components/collections/grid-view.js +7 -3
- package/dist/components/collections/table.d.ts +53 -0
- package/dist/components/collections/table.js +323 -0
- package/dist/components/collections/thumbnail-card.d.ts +19 -0
- package/dist/components/collections/thumbnail-card.js +210 -0
- package/dist/components/feedback/alert.d.ts +2 -0
- package/dist/components/feedback/alert.js +30 -1
- package/dist/components/feedback/badge.d.ts +10 -0
- package/dist/components/feedback/badge.js +70 -4
- package/dist/components/feedback/badgeable.d.ts +14 -0
- package/dist/components/feedback/badgeable.js +84 -0
- package/dist/components/feedback/chip.d.ts +5 -0
- package/dist/components/feedback/chip.js +74 -2
- package/dist/components/feedback/error-mask.d.ts +2 -0
- package/dist/components/feedback/error-mask.js +23 -0
- package/dist/components/feedback/help-text.js +7 -0
- package/dist/components/feedback/spinner.d.ts +4 -0
- package/dist/components/feedback/spinner.js +30 -2
- package/dist/components/feedback/toast.d.ts +5 -0
- package/dist/components/feedback/toast.js +59 -11
- package/dist/components/forms/category-selector.d.ts +11 -0
- package/dist/components/forms/category-selector.js +211 -17
- package/dist/components/forms/checkbox.d.ts +6 -0
- package/dist/components/forms/checkbox.js +58 -1
- package/dist/components/forms/combobox.d.ts +30 -1
- package/dist/components/forms/combobox.js +348 -44
- package/dist/components/forms/date-field.d.ts +8 -0
- package/dist/components/forms/date-field.js +80 -3
- package/dist/components/forms/dropdown.d.ts +5 -0
- package/dist/components/forms/dropdown.js +32 -3
- package/dist/components/forms/pill-selector-dropdown.d.ts +11 -0
- package/dist/components/forms/pill-selector-dropdown.js +127 -0
- package/dist/components/forms/radio-group.d.ts +9 -8
- package/dist/components/forms/radio-group.js +43 -6
- package/dist/components/forms/search-field.d.ts +9 -0
- package/dist/components/forms/search-field.js +62 -15
- package/dist/components/forms/select.d.ts +19 -8
- package/dist/components/forms/select.js +110 -15
- package/dist/components/forms/text-field.d.ts +14 -0
- package/dist/components/forms/text-field.js +144 -3
- package/dist/components/forms/time-field.d.ts +11 -0
- package/dist/components/forms/time-field.js +51 -0
- package/dist/components/identity/avatar.d.ts +4 -0
- package/dist/components/identity/avatar.js +65 -1
- package/dist/components/identity/contact-datalist-item.d.ts +8 -0
- package/dist/components/identity/contact-datalist-item.js +71 -2
- package/dist/components/layout/nav-sidebar.js +23 -0
- package/dist/components/layout/sidebar-toggle-button.d.ts +3 -0
- package/dist/components/layout/sidebar-toggle-button.js +21 -2
- package/dist/components/navigation/accordion.d.ts +3 -0
- package/dist/components/navigation/accordion.js +17 -2
- package/dist/components/navigation/breadcrumb.d.ts +30 -0
- package/dist/components/navigation/breadcrumb.js +190 -0
- package/dist/components/navigation/tabs.d.ts +4 -0
- package/dist/components/navigation/tabs.js +53 -5
- package/dist/components/overlays/context-menu.d.ts +42 -0
- package/dist/components/overlays/context-menu.js +260 -0
- package/dist/components/overlays/dialog.d.ts +8 -0
- package/dist/components/overlays/dialog.js +45 -2
- package/dist/components/overlays/guide-tooltip.d.ts +51 -0
- package/dist/components/overlays/guide-tooltip.js +361 -0
- package/dist/components/overlays/popover.d.ts +9 -1
- package/dist/components/overlays/popover.js +37 -22
- package/dist/components/overlays/tooltip.d.ts +10 -0
- package/dist/components/overlays/tooltip.js +74 -11
- package/dist/core/form-associated.d.ts +32 -0
- package/dist/core/form-associated.js +114 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/foundations/overlay/index.d.ts +2 -0
- package/dist/foundations/overlay/index.js +1 -0
- package/dist/foundations/overlay/positioning.d.ts +74 -0
- package/dist/foundations/overlay/positioning.js +162 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/package.json +1 -1
|
@@ -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"
|
|
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
|
-
/*
|
|
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
|
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
export interface BreadcrumbItem {
|
|
3
|
+
label: string;
|
|
4
|
+
/** Optional link target. Omit for a purely JS-driven crumb. */
|
|
5
|
+
href?: string;
|
|
6
|
+
/** Value reported on `navigate`; defaults to `href` or `label`. */
|
|
7
|
+
value?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* File-path breadcrumb trail with overflow collapse — box-ui-elements
|
|
11
|
+
* `Breadcrumb`. When the trail is longer than `max-items` the middle crumbs
|
|
12
|
+
* collapse into an ellipsis, always keeping the first crumb and the last two.
|
|
13
|
+
*/
|
|
14
|
+
export declare class BoxBreadcrumbElement extends BaseElement {
|
|
15
|
+
static get observedAttributes(): string[];
|
|
16
|
+
private listEl;
|
|
17
|
+
get items(): BreadcrumbItem[];
|
|
18
|
+
set items(value: BreadcrumbItem[]);
|
|
19
|
+
get label(): string;
|
|
20
|
+
set label(value: string);
|
|
21
|
+
/** Max crumbs shown before the middle collapses to an ellipsis. 0 disables collapse. */
|
|
22
|
+
get maxItems(): number;
|
|
23
|
+
set maxItems(value: number);
|
|
24
|
+
/** Keep the first crumb + last two, collapsing the middle into an ellipsis. */
|
|
25
|
+
private collapse;
|
|
26
|
+
protected renderTemplate(): void;
|
|
27
|
+
protected setupListeners(): void;
|
|
28
|
+
protected update(): void;
|
|
29
|
+
}
|
|
30
|
+
export declare const defineBoxBreadcrumbElement: (tagName?: string) => typeof BoxBreadcrumbElement;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
|
+
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
4
|
+
const DEFAULT_TAG_NAME = "box-breadcrumb";
|
|
5
|
+
const escapeHtml = (value) => value
|
|
6
|
+
.replaceAll("&", "&")
|
|
7
|
+
.replaceAll("<", "<")
|
|
8
|
+
.replaceAll(">", ">")
|
|
9
|
+
.replaceAll('"', """);
|
|
10
|
+
const breadcrumbStyles = `
|
|
11
|
+
:host {
|
|
12
|
+
display: block;
|
|
13
|
+
color: inherit;
|
|
14
|
+
font: inherit;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
[part="list"] {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-wrap: wrap;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: ${boeSpace[1]};
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
list-style: none;
|
|
25
|
+
font-size: 13px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
[part="crumb"] {
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
[part="link"] {
|
|
34
|
+
appearance: none;
|
|
35
|
+
border: none;
|
|
36
|
+
background: none;
|
|
37
|
+
font: inherit;
|
|
38
|
+
padding: 2px 6px;
|
|
39
|
+
border-radius: ${boeRadius.med};
|
|
40
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
41
|
+
text-decoration: none;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
max-width: 22ch;
|
|
44
|
+
overflow: hidden;
|
|
45
|
+
text-overflow: ellipsis;
|
|
46
|
+
white-space: nowrap;
|
|
47
|
+
transition:
|
|
48
|
+
color ${boeMotionDuration.interactive} ${boeMotionEasing.standard},
|
|
49
|
+
background ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[part="link"]:hover {
|
|
53
|
+
color: var(--boe-token-text-text, #222222);
|
|
54
|
+
background: var(--boe-token-surface-surface-secondary, #f4f4f4);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
[part="link"]:focus-visible {
|
|
58
|
+
outline: 2px solid var(--boe-token-surface-surface-brand, #0061d5);
|
|
59
|
+
outline-offset: 1px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
[part="link"][aria-current="page"] {
|
|
63
|
+
color: var(--boe-token-text-text, #222222);
|
|
64
|
+
font-weight: 600;
|
|
65
|
+
cursor: default;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[part="separator"] {
|
|
69
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
70
|
+
user-select: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[part="ellipsis"] {
|
|
74
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
75
|
+
padding: 0 2px;
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
/** A collapsed marker inserted when the trail exceeds `max-items`. */
|
|
79
|
+
const ELLIPSIS = Symbol("ellipsis");
|
|
80
|
+
/**
|
|
81
|
+
* File-path breadcrumb trail with overflow collapse — box-ui-elements
|
|
82
|
+
* `Breadcrumb`. When the trail is longer than `max-items` the middle crumbs
|
|
83
|
+
* collapse into an ellipsis, always keeping the first crumb and the last two.
|
|
84
|
+
*/
|
|
85
|
+
export class BoxBreadcrumbElement extends BaseElement {
|
|
86
|
+
static get observedAttributes() {
|
|
87
|
+
return ["items", "label", "max-items"];
|
|
88
|
+
}
|
|
89
|
+
listEl;
|
|
90
|
+
get items() {
|
|
91
|
+
const raw = this.getAttribute("items");
|
|
92
|
+
if (!raw) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(raw);
|
|
97
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
set items(value) {
|
|
104
|
+
this.setAttribute("items", JSON.stringify(value));
|
|
105
|
+
}
|
|
106
|
+
get label() {
|
|
107
|
+
return this.getAttribute("label") ?? "Breadcrumb";
|
|
108
|
+
}
|
|
109
|
+
set label(value) {
|
|
110
|
+
this.setAttribute("label", value);
|
|
111
|
+
}
|
|
112
|
+
/** Max crumbs shown before the middle collapses to an ellipsis. 0 disables collapse. */
|
|
113
|
+
get maxItems() {
|
|
114
|
+
const raw = Number(this.getAttribute("max-items"));
|
|
115
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 4;
|
|
116
|
+
}
|
|
117
|
+
set maxItems(value) {
|
|
118
|
+
this.setAttribute("max-items", String(value));
|
|
119
|
+
}
|
|
120
|
+
/** Keep the first crumb + last two, collapsing the middle into an ellipsis. */
|
|
121
|
+
collapse(items) {
|
|
122
|
+
const max = this.maxItems;
|
|
123
|
+
if (items.length <= max) {
|
|
124
|
+
return items;
|
|
125
|
+
}
|
|
126
|
+
return [items[0], ELLIPSIS, ...items.slice(-2)];
|
|
127
|
+
}
|
|
128
|
+
renderTemplate() {
|
|
129
|
+
if (!this.shadowRoot) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.shadowRoot.innerHTML = `
|
|
133
|
+
<style>${breadcrumbStyles}</style>
|
|
134
|
+
<nav part="nav">
|
|
135
|
+
<ol part="list"></ol>
|
|
136
|
+
</nav>
|
|
137
|
+
`;
|
|
138
|
+
this.listEl = this.shadowRoot.querySelector('[part="list"]');
|
|
139
|
+
}
|
|
140
|
+
setupListeners() {
|
|
141
|
+
this.listEl.addEventListener("click", event => {
|
|
142
|
+
const link = event.target.closest('[part="link"]');
|
|
143
|
+
if (!link || link.getAttribute("aria-current") === "page") {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const value = link.dataset.value ?? "";
|
|
147
|
+
const href = link.getAttribute("href") ?? undefined;
|
|
148
|
+
// JS-only crumbs (no href) shouldn't navigate the browser.
|
|
149
|
+
if (!href) {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
}
|
|
152
|
+
this.dispatchEvent(new CustomEvent("navigate", {
|
|
153
|
+
bubbles: true,
|
|
154
|
+
composed: true,
|
|
155
|
+
detail: { value, href },
|
|
156
|
+
}));
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
update() {
|
|
160
|
+
if (!this.listEl) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.shadowRoot?.querySelector('[part="nav"]')?.setAttribute("aria-label", this.label);
|
|
164
|
+
const crumbs = this.collapse(this.items);
|
|
165
|
+
const lastIndex = crumbs.length - 1;
|
|
166
|
+
this.listEl.innerHTML = crumbs
|
|
167
|
+
.map((crumb, index) => {
|
|
168
|
+
const separator = index < lastIndex ? `<span part="separator" aria-hidden="true">/</span>` : "";
|
|
169
|
+
if (crumb === ELLIPSIS) {
|
|
170
|
+
return `<li part="crumb"><span part="ellipsis" aria-label="Hidden path">…</span>${separator}</li>`;
|
|
171
|
+
}
|
|
172
|
+
const isLast = index === lastIndex;
|
|
173
|
+
const value = escapeHtml(crumb.value ?? crumb.href ?? crumb.label);
|
|
174
|
+
const tag = crumb.href && !isLast ? "a" : "button";
|
|
175
|
+
const hrefAttr = crumb.href && !isLast ? ` href="${escapeHtml(crumb.href)}"` : "";
|
|
176
|
+
const typeAttr = tag === "button" ? ' type="button"' : "";
|
|
177
|
+
const current = isLast ? ' aria-current="page"' : "";
|
|
178
|
+
return `<li part="crumb"><${tag} part="link" data-value="${value}"${hrefAttr}${typeAttr}${current}>${escapeHtml(crumb.label)}</${tag}>${separator}</li>`;
|
|
179
|
+
})
|
|
180
|
+
.join("");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export const defineBoxBreadcrumbElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
184
|
+
const existingElement = customElements.get(tagName);
|
|
185
|
+
if (existingElement) {
|
|
186
|
+
return existingElement;
|
|
187
|
+
}
|
|
188
|
+
customElements.define(tagName, BoxBreadcrumbElement);
|
|
189
|
+
return BoxBreadcrumbElement;
|
|
190
|
+
};
|
|
@@ -8,6 +8,8 @@ export declare class BoxTabsElement extends BaseElement {
|
|
|
8
8
|
private valueInternal;
|
|
9
9
|
private lastOptionsJson;
|
|
10
10
|
private tabsEl;
|
|
11
|
+
private panelsEl;
|
|
12
|
+
private readonly uid;
|
|
11
13
|
get label(): string;
|
|
12
14
|
set label(value: string);
|
|
13
15
|
get layout(): "attached" | "separated";
|
|
@@ -19,6 +21,8 @@ export declare class BoxTabsElement extends BaseElement {
|
|
|
19
21
|
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
|
|
20
22
|
private selectValue;
|
|
21
23
|
private renderTabsMarkup;
|
|
24
|
+
/** One tabpanel per option, fed by a slot named for the option value. */
|
|
25
|
+
private renderPanelsMarkup;
|
|
22
26
|
protected renderTemplate(): void;
|
|
23
27
|
protected setupListeners(): void;
|
|
24
28
|
protected update(): void;
|