@unofficialbox/box-open-elements 0.2.0 → 0.3.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/README.md +1 -0
- package/dist/components/actions/button.d.ts +15 -0
- package/dist/components/actions/button.js +107 -3
- package/dist/components/actions/menu.d.ts +10 -0
- package/dist/components/actions/menu.js +68 -11
- 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/badgeable.d.ts +14 -0
- package/dist/components/feedback/badgeable.js +84 -0
- package/dist/components/forms/date-field.d.ts +1 -0
- package/dist/components/forms/date-field.js +7 -2
- 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/search-field.d.ts +1 -0
- package/dist/components/forms/search-field.js +7 -2
- package/dist/components/forms/select.d.ts +1 -0
- package/dist/components/forms/select.js +7 -2
- package/dist/components/forms/text-field.d.ts +1 -0
- package/dist/components/forms/text-field.js +8 -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/popover.d.ts +9 -1
- package/dist/components/overlays/popover.js +37 -22
- 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 +158 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/patterns/content-explorer/adapters/table.js +8 -1
- package/dist/patterns/search/filter-bar.js +12 -6
- package/package.json +1 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { boePanel, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
|
+
const DEFAULT_TAG_NAME = "box-table";
|
|
4
|
+
const escapeHtml = (value) => value
|
|
5
|
+
.replaceAll("&", "&")
|
|
6
|
+
.replaceAll("<", "<")
|
|
7
|
+
.replaceAll(">", ">")
|
|
8
|
+
.replaceAll('"', """);
|
|
9
|
+
const tableStyles = `
|
|
10
|
+
:host {
|
|
11
|
+
display: block;
|
|
12
|
+
color: inherit;
|
|
13
|
+
font: inherit;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
[part="shell"] {
|
|
17
|
+
border: ${boePanel.border};
|
|
18
|
+
border-radius: ${boeRadius.large};
|
|
19
|
+
overflow: auto;
|
|
20
|
+
background: var(--boe-token-surface-surface, #ffffff);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
table {
|
|
24
|
+
width: 100%;
|
|
25
|
+
border-collapse: collapse;
|
|
26
|
+
font-size: 13px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
thead th {
|
|
30
|
+
text-align: start;
|
|
31
|
+
padding: ${boeSpace[2]} ${boeSpace[3]};
|
|
32
|
+
background: var(--boe-token-surface-surface-secondary, #fbfbfb);
|
|
33
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
34
|
+
font-size: 11px;
|
|
35
|
+
font-weight: 700;
|
|
36
|
+
letter-spacing: 0.06em;
|
|
37
|
+
text-transform: uppercase;
|
|
38
|
+
white-space: nowrap;
|
|
39
|
+
border-bottom: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
|
|
40
|
+
position: sticky;
|
|
41
|
+
top: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
th[data-align="end"], td[data-align="end"] { text-align: end; }
|
|
45
|
+
th[data-align="center"], td[data-align="center"] { text-align: center; }
|
|
46
|
+
|
|
47
|
+
th[part="sortable"] {
|
|
48
|
+
cursor: pointer;
|
|
49
|
+
user-select: none;
|
|
50
|
+
}
|
|
51
|
+
th[part="sortable"]:hover { color: var(--boe-token-text-text, #222222); }
|
|
52
|
+
th[aria-sort] .boe-sort-arrow::after { content: " ↕"; opacity: 0.5; }
|
|
53
|
+
th[aria-sort="ascending"] .boe-sort-arrow::after { content: " ↑"; opacity: 1; }
|
|
54
|
+
th[aria-sort="descending"] .boe-sort-arrow::after { content: " ↓"; opacity: 1; }
|
|
55
|
+
|
|
56
|
+
tbody td {
|
|
57
|
+
padding: ${boeSpace[2]} ${boeSpace[3]};
|
|
58
|
+
border-bottom: 1px solid var(--boe-token-stroke-stroke, #e8e8e8);
|
|
59
|
+
color: var(--boe-token-text-text, #222222);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
tbody tr:last-child td { border-bottom: none; }
|
|
63
|
+
|
|
64
|
+
tbody tr[part="row"] {
|
|
65
|
+
outline: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
:host([selection-mode]:not([selection-mode="none"])) tbody tr[part="row"] {
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
tbody tr[aria-selected="true"] td {
|
|
73
|
+
background: color-mix(in srgb, var(--boe-token-surface-surface-brand, #0061d5) 10%, transparent);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
tbody tr[part="row"]:focus-visible td {
|
|
77
|
+
box-shadow: inset 0 0 0 2px var(--boe-token-surface-surface-brand, #0061d5);
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
/**
|
|
81
|
+
* A semantic, selectable data table — box-ui-elements `Table` + `makeSelectable`.
|
|
82
|
+
* Rows and columns come from JSON. Selection supports single / Ctrl-click toggle
|
|
83
|
+
* / Shift-click range, plus keyboard (Arrow, Shift+Arrow to extend, Space to
|
|
84
|
+
* toggle, Ctrl/Cmd+A select-all, Escape clear). Sorting is host-owned: sortable
|
|
85
|
+
* headers emit `sort` and the host reorders `rows`.
|
|
86
|
+
*/
|
|
87
|
+
export class BoxTableElement extends BaseElement {
|
|
88
|
+
static get observedAttributes() {
|
|
89
|
+
return ["columns", "rows", "selection-mode", "label", "sort-key", "sort-direction"];
|
|
90
|
+
}
|
|
91
|
+
bodyEl;
|
|
92
|
+
headEl;
|
|
93
|
+
selected = new Set();
|
|
94
|
+
anchorIndex = -1;
|
|
95
|
+
get columns() {
|
|
96
|
+
return this.parseJson("columns", []);
|
|
97
|
+
}
|
|
98
|
+
set columns(value) {
|
|
99
|
+
this.setAttribute("columns", JSON.stringify(value));
|
|
100
|
+
}
|
|
101
|
+
get rows() {
|
|
102
|
+
return this.parseJson("rows", []);
|
|
103
|
+
}
|
|
104
|
+
set rows(value) {
|
|
105
|
+
this.setAttribute("rows", JSON.stringify(value));
|
|
106
|
+
}
|
|
107
|
+
get selectionMode() {
|
|
108
|
+
const mode = this.getAttribute("selection-mode");
|
|
109
|
+
return mode === "single" || mode === "multiple" ? mode : "none";
|
|
110
|
+
}
|
|
111
|
+
set selectionMode(value) {
|
|
112
|
+
this.setAttribute("selection-mode", value);
|
|
113
|
+
}
|
|
114
|
+
get selectedIds() {
|
|
115
|
+
return [...this.selected];
|
|
116
|
+
}
|
|
117
|
+
set selectedIds(value) {
|
|
118
|
+
this.selected = new Set(value);
|
|
119
|
+
if (this.isRendered) {
|
|
120
|
+
this.update();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
get label() {
|
|
124
|
+
return this.getAttribute("label") ?? "Table";
|
|
125
|
+
}
|
|
126
|
+
set label(value) {
|
|
127
|
+
this.setAttribute("label", value);
|
|
128
|
+
}
|
|
129
|
+
parseJson(attr, fallback) {
|
|
130
|
+
const raw = this.getAttribute(attr);
|
|
131
|
+
if (!raw)
|
|
132
|
+
return fallback;
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(raw);
|
|
135
|
+
return parsed ?? fallback;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return fallback;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
cellText(row, column, index) {
|
|
142
|
+
if (Array.isArray(row.cells)) {
|
|
143
|
+
return row.cells[index] ?? "";
|
|
144
|
+
}
|
|
145
|
+
return row.cells[column.key] ?? "";
|
|
146
|
+
}
|
|
147
|
+
renderTemplate() {
|
|
148
|
+
if (!this.shadowRoot)
|
|
149
|
+
return;
|
|
150
|
+
this.shadowRoot.innerHTML = `
|
|
151
|
+
<style>${tableStyles}</style>
|
|
152
|
+
<div part="shell">
|
|
153
|
+
<table part="table">
|
|
154
|
+
<thead><tr part="header-row"></tr></thead>
|
|
155
|
+
<tbody part="body"></tbody>
|
|
156
|
+
</table>
|
|
157
|
+
</div>
|
|
158
|
+
`;
|
|
159
|
+
this.headEl = this.shadowRoot.querySelector('[part="header-row"]');
|
|
160
|
+
this.bodyEl = this.shadowRoot.querySelector('[part="body"]');
|
|
161
|
+
}
|
|
162
|
+
setupListeners() {
|
|
163
|
+
this.headEl.addEventListener("click", event => {
|
|
164
|
+
const th = event.target.closest('th[part="sortable"]');
|
|
165
|
+
if (!th)
|
|
166
|
+
return;
|
|
167
|
+
this.toggleSort(th.dataset.key ?? "");
|
|
168
|
+
});
|
|
169
|
+
this.bodyEl.addEventListener("click", event => {
|
|
170
|
+
const row = event.target.closest('[part="row"]');
|
|
171
|
+
if (!row)
|
|
172
|
+
return;
|
|
173
|
+
const mouse = event;
|
|
174
|
+
this.activateRow(Number(row.dataset.index), { shift: mouse.shiftKey, toggle: mouse.ctrlKey || mouse.metaKey });
|
|
175
|
+
});
|
|
176
|
+
this.bodyEl.addEventListener("keydown", event => this.onBodyKeydown(event));
|
|
177
|
+
}
|
|
178
|
+
rowElements() {
|
|
179
|
+
return Array.from(this.bodyEl.querySelectorAll('[part="row"]'));
|
|
180
|
+
}
|
|
181
|
+
onBodyKeydown(event) {
|
|
182
|
+
const rows = this.rowElements();
|
|
183
|
+
if (rows.length === 0)
|
|
184
|
+
return;
|
|
185
|
+
const currentRow = event.target.closest('[part="row"]');
|
|
186
|
+
const index = currentRow ? Number(currentRow.dataset.index) : -1;
|
|
187
|
+
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "a") {
|
|
188
|
+
if (this.selectionMode === "multiple") {
|
|
189
|
+
event.preventDefault();
|
|
190
|
+
this.selectAll();
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (event.key === "Escape") {
|
|
195
|
+
if (this.selected.size) {
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
this.setSelection([]);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (event.key === " ") {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
this.activateRow(index, { toggle: this.selectionMode === "multiple" });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
let nextIndex = index;
|
|
207
|
+
if (event.key === "ArrowDown")
|
|
208
|
+
nextIndex = Math.min(index + 1, rows.length - 1);
|
|
209
|
+
else if (event.key === "ArrowUp")
|
|
210
|
+
nextIndex = Math.max(index - 1, 0);
|
|
211
|
+
else if (event.key === "Home")
|
|
212
|
+
nextIndex = 0;
|
|
213
|
+
else if (event.key === "End")
|
|
214
|
+
nextIndex = rows.length - 1;
|
|
215
|
+
else
|
|
216
|
+
return;
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
rows[nextIndex]?.focus();
|
|
219
|
+
// Shift+Arrow extends the selection from the anchor.
|
|
220
|
+
if (event.shiftKey && this.selectionMode === "multiple") {
|
|
221
|
+
this.selectRange(this.anchorIndex < 0 ? index : this.anchorIndex, nextIndex);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
toggleSort(key) {
|
|
225
|
+
const currentKey = this.getAttribute("sort-key");
|
|
226
|
+
const currentDir = this.getAttribute("sort-direction");
|
|
227
|
+
const direction = currentKey === key && currentDir === "ascending" ? "descending" : "ascending";
|
|
228
|
+
this.dispatchEvent(new CustomEvent("sort", { bubbles: true, composed: true, detail: { key, direction } }));
|
|
229
|
+
}
|
|
230
|
+
activateRow(index, options = {}) {
|
|
231
|
+
const rows = this.rows;
|
|
232
|
+
const row = rows[index];
|
|
233
|
+
if (!row || this.selectionMode === "none")
|
|
234
|
+
return;
|
|
235
|
+
if (this.selectionMode === "single") {
|
|
236
|
+
this.setSelection([row.id]);
|
|
237
|
+
this.anchorIndex = index;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (options.shift && this.anchorIndex >= 0) {
|
|
241
|
+
this.selectRange(this.anchorIndex, index);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (options.toggle) {
|
|
245
|
+
const next = new Set(this.selected);
|
|
246
|
+
next.has(row.id) ? next.delete(row.id) : next.add(row.id);
|
|
247
|
+
this.setSelection([...next]);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
this.setSelection([row.id]);
|
|
251
|
+
}
|
|
252
|
+
this.anchorIndex = index;
|
|
253
|
+
}
|
|
254
|
+
selectRange(from, to) {
|
|
255
|
+
const rows = this.rows;
|
|
256
|
+
const [start, end] = from <= to ? [from, to] : [to, from];
|
|
257
|
+
const ids = rows.slice(start, end + 1).map(row => row.id);
|
|
258
|
+
this.setSelection(ids);
|
|
259
|
+
}
|
|
260
|
+
selectAll() {
|
|
261
|
+
this.setSelection(this.rows.map(row => row.id));
|
|
262
|
+
}
|
|
263
|
+
setSelection(ids) {
|
|
264
|
+
this.selected = new Set(ids);
|
|
265
|
+
this.applySelectionState();
|
|
266
|
+
this.dispatchEvent(new CustomEvent("selection-changed", {
|
|
267
|
+
bubbles: true,
|
|
268
|
+
composed: true,
|
|
269
|
+
detail: { selectedIds: [...this.selected] },
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
applySelectionState() {
|
|
273
|
+
for (const row of this.rowElements()) {
|
|
274
|
+
const id = row.dataset.id ?? "";
|
|
275
|
+
row.setAttribute("aria-selected", String(this.selected.has(id)));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
update() {
|
|
279
|
+
if (!this.bodyEl || !this.headEl)
|
|
280
|
+
return;
|
|
281
|
+
const columns = this.columns;
|
|
282
|
+
const rows = this.rows;
|
|
283
|
+
const selectable = this.selectionMode !== "none";
|
|
284
|
+
const sortKey = this.getAttribute("sort-key");
|
|
285
|
+
const sortDir = this.getAttribute("sort-direction");
|
|
286
|
+
this.shadowRoot?.querySelector('[part="table"]')?.setAttribute("aria-label", this.label);
|
|
287
|
+
this.shadowRoot?.querySelector('[part="table"]')?.setAttribute("role", selectable ? "grid" : "table");
|
|
288
|
+
this.headEl.innerHTML = columns
|
|
289
|
+
.map(column => {
|
|
290
|
+
const align = column.align ? ` data-align="${column.align}"` : "";
|
|
291
|
+
if (column.sortable) {
|
|
292
|
+
const sort = sortKey === column.key ? ` aria-sort="${sortDir === "descending" ? "descending" : "ascending"}"` : ' aria-sort="none"';
|
|
293
|
+
return `<th part="sortable" scope="col" data-key="${escapeHtml(column.key)}"${align}${sort}>${escapeHtml(column.label)}<span class="boe-sort-arrow" aria-hidden="true"></span></th>`;
|
|
294
|
+
}
|
|
295
|
+
return `<th scope="col"${align}>${escapeHtml(column.label)}</th>`;
|
|
296
|
+
})
|
|
297
|
+
.join("");
|
|
298
|
+
this.bodyEl.innerHTML = rows
|
|
299
|
+
.map((row, index) => {
|
|
300
|
+
const selected = this.selected.has(row.id);
|
|
301
|
+
const rowAttrs = selectable
|
|
302
|
+
? ` part="row" role="row" tabindex="${index === 0 ? "0" : "-1"}" aria-selected="${selected}"`
|
|
303
|
+
: ' part="row" role="row"';
|
|
304
|
+
const cells = columns
|
|
305
|
+
.map(column => {
|
|
306
|
+
const align = column.align ? ` data-align="${column.align}"` : "";
|
|
307
|
+
const role = selectable ? ' role="gridcell"' : ' role="cell"';
|
|
308
|
+
return `<td${align}${role}>${escapeHtml(this.cellText(row, column, columns.indexOf(column)))}</td>`;
|
|
309
|
+
})
|
|
310
|
+
.join("");
|
|
311
|
+
return `<tr${rowAttrs} data-index="${index}" data-id="${escapeHtml(row.id)}">${cells}</tr>`;
|
|
312
|
+
})
|
|
313
|
+
.join("");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
export const defineBoxTableElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
317
|
+
const existingElement = customElements.get(tagName);
|
|
318
|
+
if (existingElement) {
|
|
319
|
+
return existingElement;
|
|
320
|
+
}
|
|
321
|
+
customElements.define(tagName, BoxTableElement);
|
|
322
|
+
return BoxTableElement;
|
|
323
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
export declare class BoxThumbnailCardElement extends BaseElement {
|
|
3
|
+
static get observedAttributes(): string[];
|
|
4
|
+
private cardEl;
|
|
5
|
+
private titleEl;
|
|
6
|
+
private subtitleEl;
|
|
7
|
+
get cardTitle(): string;
|
|
8
|
+
set cardTitle(value: string);
|
|
9
|
+
get subtitle(): string;
|
|
10
|
+
set subtitle(value: string);
|
|
11
|
+
/** When set, the card is a button: focusable, keyboard-activated, emits `activate`. */
|
|
12
|
+
get interactive(): boolean;
|
|
13
|
+
set interactive(value: boolean);
|
|
14
|
+
protected renderTemplate(): void;
|
|
15
|
+
protected setupListeners(): void;
|
|
16
|
+
private activate;
|
|
17
|
+
protected update(): void;
|
|
18
|
+
}
|
|
19
|
+
export declare const defineBoxThumbnailCardElement: (tagName?: string) => typeof BoxThumbnailCardElement;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
import { boePanel, boeRadius, boeSpace } from "../../foundations/geometry/index.js";
|
|
3
|
+
import { boeMotionDuration, boeMotionEasing } from "../../foundations/motion/index.js";
|
|
4
|
+
const DEFAULT_TAG_NAME = "box-thumbnail-card";
|
|
5
|
+
/** Rich file/grid card — box-ui-elements `ThumbnailCard`. */
|
|
6
|
+
const thumbnailCardStyles = `
|
|
7
|
+
:host {
|
|
8
|
+
display: inline-block;
|
|
9
|
+
color: inherit;
|
|
10
|
+
font: inherit;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
[part="card"] {
|
|
14
|
+
display: grid;
|
|
15
|
+
grid-template-rows: auto auto;
|
|
16
|
+
width: 100%;
|
|
17
|
+
min-width: 160px;
|
|
18
|
+
border: ${boePanel.border};
|
|
19
|
+
border-radius: ${boeRadius.large};
|
|
20
|
+
background: var(--boe-token-surface-surface, #ffffff);
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
transition:
|
|
23
|
+
border-color ${boeMotionDuration.interactive} ${boeMotionEasing.standard},
|
|
24
|
+
box-shadow ${boeMotionDuration.interactive} ${boeMotionEasing.standard};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
:host([interactive]) [part="card"] {
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
:host([highlight-on-hover]) [part="card"]:hover,
|
|
32
|
+
:host([interactive]) [part="card"]:hover {
|
|
33
|
+
border-color: var(--boe-token-stroke-stroke-hover, #bcbcbc);
|
|
34
|
+
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[part="card"]:focus-visible {
|
|
38
|
+
outline: 2px solid var(--boe-token-surface-surface-brand, #0061d5);
|
|
39
|
+
outline-offset: 2px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
[part="thumbnail"] {
|
|
43
|
+
display: grid;
|
|
44
|
+
place-items: center;
|
|
45
|
+
aspect-ratio: 16 / 10;
|
|
46
|
+
background: var(--boe-token-surface-surface-secondary, #f4f4f4);
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[part="thumbnail"] ::slotted(img) {
|
|
51
|
+
width: 100%;
|
|
52
|
+
height: 100%;
|
|
53
|
+
object-fit: cover;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
[part="details"] {
|
|
57
|
+
display: grid;
|
|
58
|
+
grid-template-columns: auto 1fr auto;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: ${boeSpace[2]};
|
|
61
|
+
padding: ${boeSpace[3]};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
[part="icon"] {
|
|
65
|
+
display: inline-flex;
|
|
66
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
[part="icon"]:not(:has(*)) {
|
|
70
|
+
display: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[part="text"] {
|
|
74
|
+
min-width: 0;
|
|
75
|
+
display: grid;
|
|
76
|
+
gap: 2px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
[part="title"] {
|
|
80
|
+
font-size: ${boeSpace[3]};
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
color: var(--boe-token-text-text, #222222);
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
text-overflow: ellipsis;
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
[part="subtitle"] {
|
|
89
|
+
font-size: 12px;
|
|
90
|
+
color: var(--boe-token-text-text-secondary, #6f6f6f);
|
|
91
|
+
overflow: hidden;
|
|
92
|
+
text-overflow: ellipsis;
|
|
93
|
+
white-space: nowrap;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
[part="subtitle"][hidden] {
|
|
97
|
+
display: none;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
[part="action"] {
|
|
101
|
+
display: inline-flex;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
[part="action"]:not(:has(*)) {
|
|
105
|
+
display: none;
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
export class BoxThumbnailCardElement extends BaseElement {
|
|
109
|
+
static get observedAttributes() {
|
|
110
|
+
return ["title", "subtitle", "interactive", "highlight-on-hover"];
|
|
111
|
+
}
|
|
112
|
+
cardEl;
|
|
113
|
+
titleEl;
|
|
114
|
+
subtitleEl;
|
|
115
|
+
get cardTitle() {
|
|
116
|
+
return this.getAttribute("title") ?? "";
|
|
117
|
+
}
|
|
118
|
+
set cardTitle(value) {
|
|
119
|
+
this.setAttribute("title", value);
|
|
120
|
+
}
|
|
121
|
+
get subtitle() {
|
|
122
|
+
return this.getAttribute("subtitle") ?? "";
|
|
123
|
+
}
|
|
124
|
+
set subtitle(value) {
|
|
125
|
+
if (value) {
|
|
126
|
+
this.setAttribute("subtitle", value);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
this.removeAttribute("subtitle");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** When set, the card is a button: focusable, keyboard-activated, emits `activate`. */
|
|
133
|
+
get interactive() {
|
|
134
|
+
return this.hasAttribute("interactive");
|
|
135
|
+
}
|
|
136
|
+
set interactive(value) {
|
|
137
|
+
this.toggleAttribute("interactive", Boolean(value));
|
|
138
|
+
}
|
|
139
|
+
renderTemplate() {
|
|
140
|
+
if (!this.shadowRoot) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.shadowRoot.innerHTML = `
|
|
144
|
+
<style>${thumbnailCardStyles}</style>
|
|
145
|
+
<div part="card">
|
|
146
|
+
<div part="thumbnail"><slot name="thumbnail"></slot></div>
|
|
147
|
+
<div part="details">
|
|
148
|
+
<span part="icon"><slot name="icon"></slot></span>
|
|
149
|
+
<div part="text">
|
|
150
|
+
<div part="title"></div>
|
|
151
|
+
<div part="subtitle"></div>
|
|
152
|
+
</div>
|
|
153
|
+
<span part="action"><slot name="action"></slot></span>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
`;
|
|
157
|
+
this.cardEl = this.shadowRoot.querySelector('[part="card"]');
|
|
158
|
+
this.titleEl = this.shadowRoot.querySelector('[part="title"]');
|
|
159
|
+
this.subtitleEl = this.shadowRoot.querySelector('[part="subtitle"]');
|
|
160
|
+
}
|
|
161
|
+
setupListeners() {
|
|
162
|
+
this.cardEl.addEventListener("click", () => this.activate());
|
|
163
|
+
this.cardEl.addEventListener("keydown", event => {
|
|
164
|
+
if (!this.interactive) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const key = event.key;
|
|
168
|
+
if (key === "Enter" || key === " ") {
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
this.activate();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
activate() {
|
|
175
|
+
if (!this.interactive) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.dispatchEvent(new CustomEvent("activate", {
|
|
179
|
+
bubbles: true,
|
|
180
|
+
composed: true,
|
|
181
|
+
detail: { title: this.cardTitle },
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
update() {
|
|
185
|
+
if (!this.cardEl || !this.titleEl || !this.subtitleEl) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.titleEl.textContent = this.cardTitle;
|
|
189
|
+
this.subtitleEl.textContent = this.subtitle;
|
|
190
|
+
this.subtitleEl.hidden = this.subtitle.length === 0;
|
|
191
|
+
if (this.interactive) {
|
|
192
|
+
this.cardEl.setAttribute("role", "button");
|
|
193
|
+
this.cardEl.setAttribute("tabindex", "0");
|
|
194
|
+
this.cardEl.setAttribute("aria-label", this.cardTitle);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
this.cardEl.removeAttribute("role");
|
|
198
|
+
this.cardEl.removeAttribute("tabindex");
|
|
199
|
+
this.cardEl.removeAttribute("aria-label");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export const defineBoxThumbnailCardElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
204
|
+
const existingElement = customElements.get(tagName);
|
|
205
|
+
if (existingElement) {
|
|
206
|
+
return existingElement;
|
|
207
|
+
}
|
|
208
|
+
customElements.define(tagName, BoxThumbnailCardElement);
|
|
209
|
+
return BoxThumbnailCardElement;
|
|
210
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Overlays badges on the corners of its content — the box-ui-elements
|
|
4
|
+
* `Badgeable` wrapper. The subject goes in the default slot; each corner is a
|
|
5
|
+
* named slot (`top-left`, `top-right`, `bottom-left`, `bottom-right`). Corners
|
|
6
|
+
* with no assigned content are hidden so they don't capture pointer events.
|
|
7
|
+
*/
|
|
8
|
+
export declare class BoxBadgeableElement extends BaseElement {
|
|
9
|
+
private corners;
|
|
10
|
+
protected renderTemplate(): void;
|
|
11
|
+
protected setupListeners(): void;
|
|
12
|
+
protected update(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare const defineBoxBadgeableElement: (tagName?: string) => typeof BoxBadgeableElement;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { BaseElement } from "../../core/index.js";
|
|
2
|
+
const DEFAULT_TAG_NAME = "box-badgeable";
|
|
3
|
+
/** Corners a badge can be pinned to, matching box-ui-elements' Badgeable. */
|
|
4
|
+
const CORNERS = ["top-left", "top-right", "bottom-left", "bottom-right"];
|
|
5
|
+
const badgeableStyles = `
|
|
6
|
+
:host {
|
|
7
|
+
display: inline-block;
|
|
8
|
+
color: inherit;
|
|
9
|
+
font: inherit;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
[part="container"] {
|
|
13
|
+
position: relative;
|
|
14
|
+
display: inline-flex;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
[part="badge"] {
|
|
18
|
+
position: absolute;
|
|
19
|
+
z-index: 1;
|
|
20
|
+
display: inline-flex;
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Slotted badge content stays interactive (e.g. a dismiss button). */
|
|
25
|
+
[part="badge"] ::slotted(*) {
|
|
26
|
+
pointer-events: auto;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[part="badge"][hidden] {
|
|
30
|
+
display: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
[part="badge"][data-corner="top-left"] { inset-block-start: 0; inset-inline-start: 0; transform: translate(-35%, -35%); }
|
|
34
|
+
[part="badge"][data-corner="top-right"] { inset-block-start: 0; inset-inline-end: 0; transform: translate(35%, -35%); }
|
|
35
|
+
[part="badge"][data-corner="bottom-left"] { inset-block-end: 0; inset-inline-start: 0; transform: translate(-35%, 35%); }
|
|
36
|
+
[part="badge"][data-corner="bottom-right"] { inset-block-end: 0; inset-inline-end: 0; transform: translate(35%, 35%); }
|
|
37
|
+
`;
|
|
38
|
+
/**
|
|
39
|
+
* Overlays badges on the corners of its content — the box-ui-elements
|
|
40
|
+
* `Badgeable` wrapper. The subject goes in the default slot; each corner is a
|
|
41
|
+
* named slot (`top-left`, `top-right`, `bottom-left`, `bottom-right`). Corners
|
|
42
|
+
* with no assigned content are hidden so they don't capture pointer events.
|
|
43
|
+
*/
|
|
44
|
+
export class BoxBadgeableElement extends BaseElement {
|
|
45
|
+
corners = new Map();
|
|
46
|
+
renderTemplate() {
|
|
47
|
+
if (!this.shadowRoot) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.shadowRoot.innerHTML = `
|
|
51
|
+
<style>${badgeableStyles}</style>
|
|
52
|
+
<span part="container">
|
|
53
|
+
<slot></slot>
|
|
54
|
+
${CORNERS.map(corner => `<span part="badge" data-corner="${corner}" hidden><slot name="${corner}"></slot></span>`).join("")}
|
|
55
|
+
</span>
|
|
56
|
+
`;
|
|
57
|
+
this.corners.clear();
|
|
58
|
+
for (const corner of CORNERS) {
|
|
59
|
+
const wrapper = this.shadowRoot.querySelector(`[data-corner="${corner}"]`);
|
|
60
|
+
const slot = wrapper.querySelector("slot");
|
|
61
|
+
this.corners.set(corner, { wrapper, slot });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
setupListeners() {
|
|
65
|
+
for (const { wrapper, slot } of this.corners.values()) {
|
|
66
|
+
slot.addEventListener("slotchange", () => {
|
|
67
|
+
wrapper.hidden = slot.assignedNodes({ flatten: true }).length === 0;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
update() {
|
|
72
|
+
for (const { wrapper, slot } of this.corners.values()) {
|
|
73
|
+
wrapper.hidden = slot.assignedNodes({ flatten: true }).length === 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export const defineBoxBadgeableElement = (tagName = DEFAULT_TAG_NAME) => {
|
|
78
|
+
const existingElement = customElements.get(tagName);
|
|
79
|
+
if (existingElement) {
|
|
80
|
+
return existingElement;
|
|
81
|
+
}
|
|
82
|
+
customElements.define(tagName, BoxBadgeableElement);
|
|
83
|
+
return BoxBadgeableElement;
|
|
84
|
+
};
|