Mercurial
comparison design_system/src/components/notifications.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 | fdf3816959cb |
| children | 60a876c4587a |
comparison
equal
deleted
inserted
replaced
| 253:fdf3816959cb | 254:2b6e732087ff |
|---|---|
| 11 return typeof value === "string" && | 11 return typeof value === "string" && |
| 12 value.length <= maximum && | 12 value.length <= maximum && |
| 13 (allowEmpty || value.trim().length > 0); | 13 (allowEmpty || value.trim().length > 0); |
| 14 } | 14 } |
| 15 | 15 |
| 16 /** | |
| 17 * Validates and normalizes a notification event record. | |
| 18 * | |
| 19 * @param {*} value Candidate event detail. | |
| 20 * @return {?Object} Frozen normalized record, or null when invalid. | |
| 21 */ | |
| 16 export function validateNotification(value) { | 22 export function validateNotification(value) { |
| 17 if (!value || | 23 if (!value || |
| 18 value.version !== 1 || | 24 value.version !== 1 || |
| 19 !isShortString(value.id, 128) || | 25 !isShortString(value.id, 128) || |
| 20 !TONES.has(value.tone) || | 26 !TONES.has(value.tone) || |
| 51 persistent: value.persistent === true, | 57 persistent: value.persistent === true, |
| 52 action, | 58 action, |
| 53 }); | 59 }); |
| 54 } | 60 } |
| 55 | 61 |
| 62 /** | |
| 63 * Validates a programmatic notification dismissal record. | |
| 64 * | |
| 65 * @param {*} value Candidate event detail. | |
| 66 * @return {?string} Valid notification ID, or null when invalid. | |
| 67 */ | |
| 56 export function validateNotificationDismiss(value) { | 68 export function validateNotificationDismiss(value) { |
| 57 if (!value || | 69 if (!value || |
| 58 value.version !== 1 || | 70 value.version !== 1 || |
| 59 !isShortString(value.id, 128)) return null; | 71 !isShortString(value.id, 128)) return null; |
| 60 return value.id; | 72 return value.id; |
| 70 timer: null, | 82 timer: null, |
| 71 view: null, | 83 view: null, |
| 72 }; | 84 }; |
| 73 } | 85 } |
| 74 | 86 |
| 87 /** | |
| 88 * Renders a bounded, scoped, Sonner-like notification queue. | |
| 89 * | |
| 90 * @extends {HTMLElement} | |
| 91 */ | |
| 75 export class ZenNotifications extends HTMLElement { | 92 export class ZenNotifications extends HTMLElement { |
| 76 static version = 1; | 93 static version = 1; |
| 77 | 94 |
| 78 #records = new Map(); | 95 #records = new Map(); |
| 79 #announcementQueues = { | 96 #announcementQueues = { |
| 361 article.dataset.zenNotificationId = record.id; | 378 article.dataset.zenNotificationId = record.id; |
| 362 | 379 |
| 363 const tone = document.createElement("span"); | 380 const tone = document.createElement("span"); |
| 364 tone.className = "zen-notification-tone"; | 381 tone.className = "zen-notification-tone"; |
| 365 tone.setAttribute("aria-hidden", "true"); | 382 tone.setAttribute("aria-hidden", "true"); |
| 383 const toneIcon = document.createElement("zen-icon"); | |
| 384 tone.append(toneIcon); | |
| 366 | 385 |
| 367 const content = document.createElement("div"); | 386 const content = document.createElement("div"); |
| 368 content.className = "zen-notification-content"; | 387 content.className = "zen-notification-content"; |
| 369 const message = document.createElement("p"); | 388 const message = document.createElement("p"); |
| 370 message.className = "zen-notification-message"; | 389 message.className = "zen-notification-message"; |
| 371 content.append(message); | 390 content.append(message); |
| 372 | 391 |
| 373 const dismiss = document.createElement("button"); | 392 const dismiss = document.createElement("button"); |
| 374 dismiss.type = "button"; | 393 dismiss.type = "button"; |
| 375 dismiss.className = "zen-notification-dismiss"; | 394 dismiss.className = "zen-notification-dismiss"; |
| 376 dismiss.textContent = "×"; | 395 const dismissIcon = document.createElement("zen-icon"); |
| 396 dismissIcon.setAttribute("name", "close"); | |
| 397 dismiss.append(dismissIcon); | |
| 377 dismiss.addEventListener("click", () => { | 398 dismiss.addEventListener("click", () => { |
| 378 this.dismiss(record.id, "dismissed"); | 399 this.dismiss(record.id, "dismissed"); |
| 379 }); | 400 }); |
| 380 | 401 |
| 381 article.addEventListener("pointerenter", () => { | 402 article.addEventListener("pointerenter", () => { |
| 400 content, | 421 content, |
| 401 description: null, | 422 description: null, |
| 402 dismiss, | 423 dismiss, |
| 403 message, | 424 message, |
| 404 tone, | 425 tone, |
| 426 toneIcon, | |
| 405 }; | 427 }; |
| 406 this.#resizeObserver?.observe(article); | 428 this.#resizeObserver?.observe(article); |
| 407 } | 429 } |
| 408 | 430 |
| 409 const view = record.view; | 431 const view = record.view; |
| 410 view.article.dataset.tone = record.tone; | 432 view.article.dataset.tone = record.tone; |
| 411 view.tone.textContent = { | 433 view.toneIcon.setAttribute("name", { |
| 412 info: "i", | 434 info: "info", |
| 413 success: "✓", | 435 success: "check", |
| 414 warning: "!", | 436 warning: "alert", |
| 415 error: "×", | 437 error: "error", |
| 416 }[record.tone]; | 438 }[record.tone]); |
| 417 view.message.textContent = record.message; | 439 view.message.textContent = record.message; |
| 418 view.dismiss.setAttribute( | 440 view.dismiss.setAttribute( |
| 419 "aria-label", | 441 "aria-label", |
| 420 this.getAttribute("dismiss-label") || "Dismiss notification", | 442 this.getAttribute("dismiss-label") || "Dismiss notification", |
| 421 ); | 443 ); |