@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.
- package/dist/components/actions/link-button.d.ts +9 -0
- package/dist/components/actions/link-button.js +55 -4
- 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/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/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 +7 -0
- package/dist/components/forms/date-field.js +73 -1
- package/dist/components/forms/dropdown.d.ts +5 -0
- package/dist/components/forms/dropdown.js +32 -3
- 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 +8 -0
- package/dist/components/forms/search-field.js +55 -13
- package/dist/components/forms/select.d.ts +18 -8
- package/dist/components/forms/select.js +104 -14
- package/dist/components/forms/text-field.d.ts +13 -0
- package/dist/components/forms/text-field.js +136 -1
- 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/overlays/guide-tooltip.d.ts +51 -0
- package/dist/components/overlays/guide-tooltip.js +361 -0
- package/dist/components/overlays/tooltip.d.ts +10 -0
- package/dist/components/overlays/tooltip.js +74 -11
- package/dist/foundations/overlay/positioning.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- 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
|
-
<
|
|
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"
|
|
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
|
})
|