Mercurial
comparison design_system/src/components/collections.js @ 254:2b6e732087ff
[ui] Add complete native component catalog
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 04 Aug 2026 15:12:09 -0700 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 253:fdf3816959cb | 254:2b6e732087ff |
|---|---|
| 1 const HTMLElementBase = globalThis.HTMLElement || class {}; | |
| 2 | |
| 3 function rememberAttribute(state, element, name) { | |
| 4 let attributes = state.get(element); | |
| 5 if (!attributes) { | |
| 6 attributes = new Map(); | |
| 7 state.set(element, attributes); | |
| 8 } | |
| 9 if (!attributes.has(name)) { | |
| 10 attributes.set(name, element.getAttribute(name)); | |
| 11 } | |
| 12 } | |
| 13 | |
| 14 function setManagedAttribute(state, element, name, value) { | |
| 15 rememberAttribute(state, element, name); | |
| 16 if (value === null) element.removeAttribute(name); | |
| 17 else element.setAttribute(name, value); | |
| 18 } | |
| 19 | |
| 20 function originalAttribute(state, element, name) { | |
| 21 const attributes = state.get(element); | |
| 22 return attributes?.has(name) | |
| 23 ? attributes.get(name) | |
| 24 : element.getAttribute(name); | |
| 25 } | |
| 26 | |
| 27 function restoreElement(state, element) { | |
| 28 const attributes = state.get(element); | |
| 29 if (!attributes) return; | |
| 30 for (const [name, value] of attributes) { | |
| 31 if (value === null) element.removeAttribute(name); | |
| 32 else element.setAttribute(name, value); | |
| 33 } | |
| 34 state.delete(element); | |
| 35 } | |
| 36 | |
| 37 function restoreAttributes(state) { | |
| 38 for (const element of [...state.keys()]) restoreElement(state, element); | |
| 39 } | |
| 40 | |
| 41 function pruneAttributes(state, retained) { | |
| 42 for (const element of [...state.keys()]) { | |
| 43 if (!retained.has(element)) restoreElement(state, element); | |
| 44 } | |
| 45 } | |
| 46 | |
| 47 function setDefaultAttribute(state, element, name, value) { | |
| 48 if (!element.hasAttribute(name) || state.get(element)?.has(name)) { | |
| 49 setManagedAttribute(state, element, name, value); | |
| 50 } | |
| 51 } | |
| 52 | |
| 53 function eventElement(event) { | |
| 54 const target = event.target; | |
| 55 if (target?.nodeType === 1) return target; | |
| 56 return target?.parentElement || null; | |
| 57 } | |
| 58 | |
| 59 function ownedElement(host, selector) { | |
| 60 return [...host.querySelectorAll(selector)].find( | |
| 61 element => element.closest(host.localName) === host, | |
| 62 ) || null; | |
| 63 } | |
| 64 | |
| 65 function emit(element, type, detail) { | |
| 66 element.dispatchEvent(new CustomEvent(type, { | |
| 67 bubbles: true, | |
| 68 detail, | |
| 69 })); | |
| 70 } | |
| 71 | |
| 72 function isDisabled(control) { | |
| 73 return Boolean(control?.disabled) || | |
| 74 control?.getAttribute("aria-disabled") === "true"; | |
| 75 } | |
| 76 | |
| 77 /** | |
| 78 * Enhances a native scroll-snap viewport with navigation controls. | |
| 79 * | |
| 80 * @extends {HTMLElement} | |
| 81 */ | |
| 82 export class ZenCarousel extends HTMLElementBase { | |
| 83 connectedCallback() { | |
| 84 this._managed ||= new Map(); | |
| 85 this._onClick ||= event => { | |
| 86 const control = eventElement(event)?.closest( | |
| 87 "[data-zen-prev], [data-zen-next]", | |
| 88 ); | |
| 89 if (control === this._prev && !isDisabled(control)) this.move(-1); | |
| 90 else if (control === this._next && !isDisabled(control)) this.move(1); | |
| 91 }; | |
| 92 this._onKeydown ||= event => { | |
| 93 if (event.altKey || event.ctrlKey || event.metaKey || | |
| 94 !["ArrowLeft", "ArrowRight"].includes(event.key)) { | |
| 95 return; | |
| 96 } | |
| 97 const target = eventElement(event); | |
| 98 if (target?.closest("input, textarea, select, [contenteditable]")) return; | |
| 99 event.preventDefault(); | |
| 100 this.move(event.key === "ArrowLeft" ? -1 : 1); | |
| 101 }; | |
| 102 this._onScroll ||= () => { | |
| 103 globalThis.clearTimeout(this._scrollTimer); | |
| 104 this._scrollTimer = globalThis.setTimeout(() => { | |
| 105 this._scrollTimer = null; | |
| 106 this.setIndex(this.currentIndex(), true); | |
| 107 }, 100); | |
| 108 }; | |
| 109 this.addEventListener("click", this._onClick); | |
| 110 this.addEventListener("keydown", this._onKeydown); | |
| 111 this._observer ||= new MutationObserver(() => this.sync()); | |
| 112 this._observer.observe(this, { childList: true, subtree: true }); | |
| 113 this.sync(); | |
| 114 } | |
| 115 | |
| 116 disconnectedCallback() { | |
| 117 this._observer?.disconnect(); | |
| 118 this.removeEventListener("click", this._onClick); | |
| 119 this.removeEventListener("keydown", this._onKeydown); | |
| 120 this._viewport?.removeEventListener("scroll", this._onScroll); | |
| 121 globalThis.clearTimeout(this._scrollTimer); | |
| 122 restoreAttributes(this._managed); | |
| 123 this._viewport = null; | |
| 124 this._slides = []; | |
| 125 this._prev = null; | |
| 126 this._next = null; | |
| 127 this._index = null; | |
| 128 this._scrollTimer = null; | |
| 129 } | |
| 130 | |
| 131 sync() { | |
| 132 const viewport = ownedElement(this, "[data-zen-viewport]"); | |
| 133 if (viewport !== this._viewport) { | |
| 134 this._viewport?.removeEventListener("scroll", this._onScroll); | |
| 135 this._viewport = viewport; | |
| 136 viewport?.addEventListener("scroll", this._onScroll, { passive: true }); | |
| 137 } | |
| 138 this._slides = viewport ? [...viewport.children] : []; | |
| 139 this._prev = ownedElement(this, "[data-zen-prev]"); | |
| 140 this._next = ownedElement(this, "[data-zen-next]"); | |
| 141 | |
| 142 const retained = new Set([ | |
| 143 this, | |
| 144 viewport, | |
| 145 this._prev, | |
| 146 this._next, | |
| 147 ...this._slides, | |
| 148 ].filter(Boolean)); | |
| 149 pruneAttributes(this._managed, retained); | |
| 150 setDefaultAttribute(this._managed, this, "role", "region"); | |
| 151 setDefaultAttribute( | |
| 152 this._managed, | |
| 153 this, | |
| 154 "aria-roledescription", | |
| 155 "carousel", | |
| 156 ); | |
| 157 setDefaultAttribute(this._managed, this, "aria-label", "Carousel"); | |
| 158 if (viewport) { | |
| 159 setDefaultAttribute(this._managed, viewport, "tabindex", "0"); | |
| 160 } | |
| 161 for (const [index, slide] of this._slides.entries()) { | |
| 162 setDefaultAttribute(this._managed, slide, "role", "group"); | |
| 163 setDefaultAttribute( | |
| 164 this._managed, | |
| 165 slide, | |
| 166 "aria-roledescription", | |
| 167 "slide", | |
| 168 ); | |
| 169 setDefaultAttribute( | |
| 170 this._managed, | |
| 171 slide, | |
| 172 "aria-label", | |
| 173 `Slide ${index + 1} of ${this._slides.length}`, | |
| 174 ); | |
| 175 } | |
| 176 | |
| 177 const index = this.currentIndex(); | |
| 178 this._index = index; | |
| 179 this.render(); | |
| 180 } | |
| 181 | |
| 182 currentIndex() { | |
| 183 if (!this._viewport || !this._slides.length) return 0; | |
| 184 const viewportLeft = this._viewport.getBoundingClientRect().left; | |
| 185 let closest = 0; | |
| 186 let distance = Infinity; | |
| 187 for (const [index, slide] of this._slides.entries()) { | |
| 188 const current = Math.abs(slide.getBoundingClientRect().left - viewportLeft); | |
| 189 if (current < distance) { | |
| 190 closest = index; | |
| 191 distance = current; | |
| 192 } | |
| 193 } | |
| 194 return closest; | |
| 195 } | |
| 196 | |
| 197 move(offset) { | |
| 198 if (!this._slides.length) return; | |
| 199 const current = Number.isInteger(this._index) | |
| 200 ? this._index | |
| 201 : this.currentIndex(); | |
| 202 this.scrollTo(Math.max(0, Math.min(this._slides.length - 1, current + offset))); | |
| 203 } | |
| 204 | |
| 205 scrollTo(index) { | |
| 206 const slide = this._slides[index]; | |
| 207 if (!this._viewport || !slide || index === this._index) return; | |
| 208 const viewportRect = this._viewport.getBoundingClientRect(); | |
| 209 const slideRect = slide.getBoundingClientRect(); | |
| 210 const left = this._viewport.scrollLeft + slideRect.left - viewportRect.left; | |
| 211 if (typeof this._viewport.scrollTo === "function") { | |
| 212 this._viewport.scrollTo({ left, behavior: "smooth" }); | |
| 213 } else if (typeof slide.scrollIntoView === "function") { | |
| 214 slide.scrollIntoView({ | |
| 215 behavior: "smooth", | |
| 216 block: "nearest", | |
| 217 inline: "start", | |
| 218 }); | |
| 219 } | |
| 220 this.setIndex(index, true); | |
| 221 } | |
| 222 | |
| 223 setIndex(index, notify) { | |
| 224 const next = Math.max( | |
| 225 0, | |
| 226 Math.min(this._slides.length ? this._slides.length - 1 : 0, index), | |
| 227 ); | |
| 228 if (next === this._index) { | |
| 229 this.render(); | |
| 230 return; | |
| 231 } | |
| 232 this._index = next; | |
| 233 this.render(); | |
| 234 if (notify) emit(this, "zen-change", { index: next }); | |
| 235 } | |
| 236 | |
| 237 render() { | |
| 238 const last = Math.max(0, this._slides.length - 1); | |
| 239 this.syncDisabled(this._prev, !this._slides.length || this._index <= 0); | |
| 240 this.syncDisabled( | |
| 241 this._next, | |
| 242 !this._slides.length || this._index >= last, | |
| 243 ); | |
| 244 } | |
| 245 | |
| 246 syncDisabled(control, disabled) { | |
| 247 if (!control) return; | |
| 248 const authored = originalAttribute(this._managed, control, "disabled"); | |
| 249 setManagedAttribute( | |
| 250 this._managed, | |
| 251 control, | |
| 252 "disabled", | |
| 253 disabled || authored !== null ? "" : null, | |
| 254 ); | |
| 255 } | |
| 256 } | |
| 257 | |
| 258 /** | |
| 259 * Keeps a message viewport pinned while the reader remains near its end. | |
| 260 * | |
| 261 * @extends {HTMLElement} | |
| 262 */ | |
| 263 export class ZenMessageScroller extends HTMLElementBase { | |
| 264 connectedCallback() { | |
| 265 this._managed ||= new Map(); | |
| 266 this._onScroll ||= () => { | |
| 267 this._nearEnd = this.isNearEnd(); | |
| 268 this.render(); | |
| 269 }; | |
| 270 this._onClick ||= event => { | |
| 271 const button = eventElement(event)?.closest("[data-zen-scroll-end]"); | |
| 272 if (button === this._button && !isDisabled(button)) this.scrollEnd(true); | |
| 273 }; | |
| 274 this._viewport = ownedElement(this, "[data-zen-viewport]") || this; | |
| 275 this._button = ownedElement(this, "[data-zen-scroll-end]"); | |
| 276 this._nearEnd = this.isNearEnd(); | |
| 277 this.addEventListener("click", this._onClick); | |
| 278 this._viewport.addEventListener("scroll", this._onScroll, { passive: true }); | |
| 279 this._observer ||= new MutationObserver(records => { | |
| 280 if (!records.some(record => record.addedNodes.length)) return; | |
| 281 const wasNearEnd = this._nearEnd; | |
| 282 if (wasNearEnd) this.scrollEnd(false); | |
| 283 else this.render(); | |
| 284 }); | |
| 285 this._observer.observe(this._viewport, { childList: true, subtree: true }); | |
| 286 this.render(); | |
| 287 } | |
| 288 | |
| 289 disconnectedCallback() { | |
| 290 this._observer?.disconnect(); | |
| 291 this.removeEventListener("click", this._onClick); | |
| 292 this._viewport?.removeEventListener("scroll", this._onScroll); | |
| 293 restoreAttributes(this._managed); | |
| 294 this._viewport = null; | |
| 295 this._button = null; | |
| 296 } | |
| 297 | |
| 298 isNearEnd() { | |
| 299 if (!this._viewport) return true; | |
| 300 return this._viewport.scrollHeight - this._viewport.scrollTop - | |
| 301 this._viewport.clientHeight <= 24; | |
| 302 } | |
| 303 | |
| 304 scrollEnd(smooth) { | |
| 305 if (!this._viewport) return; | |
| 306 if (typeof this._viewport.scrollTo === "function") { | |
| 307 this._viewport.scrollTo({ | |
| 308 top: this._viewport.scrollHeight, | |
| 309 behavior: smooth ? "smooth" : "auto", | |
| 310 }); | |
| 311 } else { | |
| 312 this._viewport.scrollTop = this._viewport.scrollHeight; | |
| 313 } | |
| 314 this._nearEnd = true; | |
| 315 this.render(); | |
| 316 } | |
| 317 | |
| 318 render() { | |
| 319 if (!this._button) return; | |
| 320 setManagedAttribute( | |
| 321 this._managed, | |
| 322 this._button, | |
| 323 "hidden", | |
| 324 this._nearEnd ? "" : null, | |
| 325 ); | |
| 326 } | |
| 327 } | |
| 328 | |
| 329 /** | |
| 330 * Adds pointer and keyboard resizing to two native content panels. | |
| 331 * | |
| 332 * @extends {HTMLElement} | |
| 333 */ | |
| 334 export class ZenResizable extends HTMLElementBase { | |
| 335 static get observedAttributes() { | |
| 336 return ["min", "max"]; | |
| 337 } | |
| 338 | |
| 339 connectedCallback() { | |
| 340 this._managed ||= new Map(); | |
| 341 this._onPointerDown ||= event => { | |
| 342 const handle = eventElement(event)?.closest("[data-zen-handle]"); | |
| 343 if (handle !== this._handle || (event.button !== 0 && event.button !== -1)) { | |
| 344 return; | |
| 345 } | |
| 346 event.preventDefault(); | |
| 347 this._pointerId = event.pointerId; | |
| 348 this._handle.setPointerCapture?.(event.pointerId); | |
| 349 }; | |
| 350 this._onPointerMove ||= event => { | |
| 351 if (event.pointerId !== this._pointerId) return; | |
| 352 const rect = this.getBoundingClientRect(); | |
| 353 if (!rect.width) return; | |
| 354 this.setPercent((event.clientX - rect.left) / rect.width * 100, true); | |
| 355 }; | |
| 356 this._onPointerEnd ||= event => { | |
| 357 if (event.pointerId !== this._pointerId) return; | |
| 358 if (this._handle?.hasPointerCapture?.(event.pointerId)) { | |
| 359 this._handle.releasePointerCapture(event.pointerId); | |
| 360 } | |
| 361 this._pointerId = null; | |
| 362 }; | |
| 363 this._onKeydown ||= event => { | |
| 364 const handle = eventElement(event)?.closest("[data-zen-handle]"); | |
| 365 if (handle !== this._handle || | |
| 366 !["ArrowLeft", "ArrowRight"].includes(event.key)) { | |
| 367 return; | |
| 368 } | |
| 369 event.preventDefault(); | |
| 370 const step = Number(this.getAttribute("step")); | |
| 371 const amount = Number.isFinite(step) && step > 0 ? step : 1; | |
| 372 this.setPercent( | |
| 373 this._percent + (event.key === "ArrowLeft" ? -amount : amount), | |
| 374 true, | |
| 375 ); | |
| 376 }; | |
| 377 this.addEventListener("pointerdown", this._onPointerDown); | |
| 378 this.addEventListener("pointermove", this._onPointerMove); | |
| 379 this.addEventListener("pointerup", this._onPointerEnd); | |
| 380 this.addEventListener("pointercancel", this._onPointerEnd); | |
| 381 this.addEventListener("keydown", this._onKeydown); | |
| 382 this._observer ||= new MutationObserver(() => this.sync()); | |
| 383 this._observer.observe(this, { childList: true }); | |
| 384 this.sync(); | |
| 385 } | |
| 386 | |
| 387 disconnectedCallback() { | |
| 388 this._observer?.disconnect(); | |
| 389 this.removeEventListener("pointerdown", this._onPointerDown); | |
| 390 this.removeEventListener("pointermove", this._onPointerMove); | |
| 391 this.removeEventListener("pointerup", this._onPointerEnd); | |
| 392 this.removeEventListener("pointercancel", this._onPointerEnd); | |
| 393 this.removeEventListener("keydown", this._onKeydown); | |
| 394 restoreAttributes(this._managed); | |
| 395 this._first = null; | |
| 396 this._second = null; | |
| 397 this._handle = null; | |
| 398 this._pointerId = null; | |
| 399 this._percent = null; | |
| 400 } | |
| 401 | |
| 402 attributeChangedCallback() { | |
| 403 if (this.isConnected && Number.isFinite(this._percent)) { | |
| 404 this.setPercent(this._percent, false); | |
| 405 } | |
| 406 } | |
| 407 | |
| 408 limits() { | |
| 409 const minimum = this.getAttribute("min"); | |
| 410 const maximum = this.getAttribute("max"); | |
| 411 const min = minimum !== null && Number.isFinite(Number(minimum)) | |
| 412 ? Math.max(0, Math.min(100, Number(minimum))) | |
| 413 : 0; | |
| 414 const max = maximum !== null && Number.isFinite(Number(maximum)) | |
| 415 ? Math.max(min, Math.min(100, Number(maximum))) | |
| 416 : 100; | |
| 417 return { min, max }; | |
| 418 } | |
| 419 | |
| 420 sync() { | |
| 421 const children = [...this.children]; | |
| 422 const handle = children.find(child => child.hasAttribute("data-zen-handle")); | |
| 423 const handleIndex = children.indexOf(handle); | |
| 424 const first = children.slice(0, handleIndex).reverse().find( | |
| 425 child => child.hasAttribute("data-zen-panel"), | |
| 426 ); | |
| 427 const second = children.slice(handleIndex + 1).find( | |
| 428 child => child.hasAttribute("data-zen-panel"), | |
| 429 ); | |
| 430 | |
| 431 if (!first || !second || !handle) { | |
| 432 if (this._first) restoreElement(this._managed, this._first); | |
| 433 if (this._handle) restoreElement(this._managed, this._handle); | |
| 434 this._first = null; | |
| 435 this._second = null; | |
| 436 this._handle = null; | |
| 437 this._percent = null; | |
| 438 return; | |
| 439 } | |
| 440 if (this._first && this._first !== first) restoreElement(this._managed, this._first); | |
| 441 if (this._handle && this._handle !== handle) { | |
| 442 restoreElement(this._managed, this._handle); | |
| 443 } | |
| 444 this._first = first; | |
| 445 this._second = second; | |
| 446 this._handle = handle; | |
| 447 | |
| 448 setManagedAttribute(this._managed, this._handle, "role", "separator"); | |
| 449 setManagedAttribute( | |
| 450 this._managed, | |
| 451 this._handle, | |
| 452 "aria-orientation", | |
| 453 "vertical", | |
| 454 ); | |
| 455 setDefaultAttribute(this._managed, this._handle, "tabindex", "0"); | |
| 456 | |
| 457 if (!Number.isFinite(this._percent)) { | |
| 458 const containerWidth = this.getBoundingClientRect().width; | |
| 459 const rendered = containerWidth | |
| 460 ? this._first.getBoundingClientRect().width / containerWidth * 100 | |
| 461 : Number.parseFloat(this._first.style.flexBasis); | |
| 462 this._percent = Number.isFinite(rendered) ? rendered : 50; | |
| 463 } | |
| 464 this.setPercent(this._percent, false); | |
| 465 } | |
| 466 | |
| 467 setPercent(percent, notify) { | |
| 468 if (!this._first || !this._handle) return; | |
| 469 const { min, max } = this.limits(); | |
| 470 const next = Math.round(Math.max(min, Math.min(max, percent)) * 100) / 100; | |
| 471 const changed = next !== this._percent; | |
| 472 this._percent = next; | |
| 473 rememberAttribute(this._managed, this._first, "style"); | |
| 474 this._first.style.flexBasis = `${next}%`; | |
| 475 setManagedAttribute(this._managed, this._handle, "aria-valuemin", String(min)); | |
| 476 setManagedAttribute(this._managed, this._handle, "aria-valuemax", String(max)); | |
| 477 setManagedAttribute(this._managed, this._handle, "aria-valuenow", String(next)); | |
| 478 if (notify && changed) emit(this, "zen-resize", { percent: next }); | |
| 479 } | |
| 480 } | |
| 481 | |
| 482 /** | |
| 483 * Adds stable client-side sorting to a native HTML table. | |
| 484 * | |
| 485 * @extends {HTMLElement} | |
| 486 */ | |
| 487 export class ZenDataTable extends HTMLElementBase { | |
| 488 connectedCallback() { | |
| 489 this._managed ||= new Map(); | |
| 490 this._onClick ||= event => { | |
| 491 const button = eventElement(event)?.closest("button[data-zen-sort]"); | |
| 492 if (!this._buttons.includes(button) || isDisabled(button)) return; | |
| 493 this.sort(button); | |
| 494 }; | |
| 495 this.addEventListener("click", this._onClick); | |
| 496 this._observer ||= new MutationObserver(() => this.sync()); | |
| 497 this._observer.observe(this, { childList: true, subtree: true }); | |
| 498 this.sync(); | |
| 499 } | |
| 500 | |
| 501 disconnectedCallback() { | |
| 502 this._observer?.disconnect(); | |
| 503 this.removeEventListener("click", this._onClick); | |
| 504 restoreAttributes(this._managed); | |
| 505 this._table = null; | |
| 506 this._buttons = []; | |
| 507 } | |
| 508 | |
| 509 sync() { | |
| 510 this._table = | |
| 511 this.querySelector(":scope > table") || this.querySelector("table"); | |
| 512 this._buttons = this._table | |
| 513 ? [...this._table.querySelectorAll("button[data-zen-sort]")].filter( | |
| 514 button => button.closest("table") === this._table && | |
| 515 button.closest("th"), | |
| 516 ) | |
| 517 : []; | |
| 518 pruneAttributes( | |
| 519 this._managed, | |
| 520 new Set(this._buttons.map(button => button.closest("th"))), | |
| 521 ); | |
| 522 } | |
| 523 | |
| 524 sort(button) { | |
| 525 const key = button.getAttribute("data-zen-sort"); | |
| 526 const header = button.closest("th"); | |
| 527 if (!this._table || !key || !header) return; | |
| 528 const direction = | |
| 529 header.getAttribute("aria-sort") === "ascending" | |
| 530 ? "descending" | |
| 531 : "ascending"; | |
| 532 | |
| 533 for (const candidate of this._buttons) { | |
| 534 const candidateHeader = candidate.closest("th"); | |
| 535 setManagedAttribute( | |
| 536 this._managed, | |
| 537 candidateHeader, | |
| 538 "aria-sort", | |
| 539 candidateHeader === header ? direction : null, | |
| 540 ); | |
| 541 } | |
| 542 | |
| 543 for (const body of this._table.tBodies) { | |
| 544 const entries = [...body.rows].map((row, index) => { | |
| 545 const cell = [...row.cells].find( | |
| 546 candidate => candidate.getAttribute("data-key") === key, | |
| 547 ); | |
| 548 return { index, row, value: cell?.textContent?.trim() ?? "" }; | |
| 549 }); | |
| 550 const numeric = entries.every( | |
| 551 entry => Number.isFinite(Number(entry.value)), | |
| 552 ); | |
| 553 const factor = direction === "ascending" ? 1 : -1; | |
| 554 entries.sort((left, right) => { | |
| 555 const compared = numeric | |
| 556 ? Number(left.value) - Number(right.value) | |
| 557 : left.value.localeCompare(right.value); | |
| 558 return compared ? compared * factor : left.index - right.index; | |
| 559 }); | |
| 560 body.append(...entries.map(entry => entry.row)); | |
| 561 } | |
| 562 emit(this, "zen-sort", { column: key, direction }); | |
| 563 } | |
| 564 } | |
| 565 | |
| 566 const definitions = { | |
| 567 "zen-carousel": ZenCarousel, | |
| 568 "zen-data-table": ZenDataTable, | |
| 569 "zen-message-scroller": ZenMessageScroller, | |
| 570 "zen-resizable": ZenResizable, | |
| 571 }; | |
| 572 | |
| 573 if (globalThis.customElements) { | |
| 574 for (const [name, definition] of Object.entries(definitions)) { | |
| 575 if (!globalThis.customElements.get(name)) { | |
| 576 globalThis.customElements.define(name, definition); | |
| 577 } | |
| 578 } | |
| 579 } |