Mercurial
comparison design_system/src/components/notifications.js @ 258:60a876c4587a
[ui] Add semantic primitive ownership
Build a layered Zenbu token and sizing system, make authored controls use native-underneath primitives, migrate mrjunejune without imposing visual surfaces, and document/enforce HTML ownership in the catalog and wiki.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Wed, 05 Aug 2026 05:25:40 -0700 |
| parents | 2b6e732087ff |
| children |
comparison
equal
deleted
inserted
replaced
| 257:609d3c6aff4e | 258:60a876c4587a |
|---|---|
| 9 | 9 |
| 10 function isShortString(value, maximum, allowEmpty = false) { | 10 function isShortString(value, maximum, allowEmpty = false) { |
| 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 } | |
| 15 | |
| 16 function resolveCssLength(element, property, fallback) { | |
| 17 const probe = document.createElement("span"); | |
| 18 probe.style.position = "absolute"; | |
| 19 probe.style.inlineSize = `var(${property})`; | |
| 20 probe.style.visibility = "hidden"; | |
| 21 element.append(probe); | |
| 22 const value = Number.parseFloat(getComputedStyle(probe).inlineSize); | |
| 23 probe.remove(); | |
| 24 return Number.isFinite(value) ? value : fallback; | |
| 14 } | 25 } |
| 15 | 26 |
| 16 /** | 27 /** |
| 17 * Validates and normalizes a notification event record. | 28 * Validates and normalizes a notification event record. |
| 18 * | 29 * |
| 83 view: null, | 94 view: null, |
| 84 }; | 95 }; |
| 85 } | 96 } |
| 86 | 97 |
| 87 /** | 98 /** |
| 88 * Renders a bounded, scoped, Sonner-like notification queue. | 99 * Renders a bounded, scoped notification queue. |
| 89 * | 100 * |
| 90 * @extends {HTMLElement} | 101 * @extends {HTMLElement} |
| 91 */ | 102 */ |
| 92 export class ZenNotifications extends HTMLElement { | 103 export class ZenNotifications extends HTMLElement { |
| 93 static version = 1; | 104 static version = 1; |
| 494 layoutStack() { | 505 layoutStack() { |
| 495 if (!this.stack) return; | 506 if (!this.stack) return; |
| 496 const visible = [...this.#records.values()] | 507 const visible = [...this.#records.values()] |
| 497 .slice(-MAX_VISIBLE) | 508 .slice(-MAX_VISIBLE) |
| 498 .filter(record => record.view?.article.isConnected); | 509 .filter(record => record.view?.article.isConnected); |
| 499 const styles = getComputedStyle(this.stack); | 510 const gap = resolveCssLength( |
| 500 const configuredGap = Number.parseFloat( | 511 this.stack, |
| 501 styles.getPropertyValue("--zen-notification-gap"), | 512 "--zenbu-notification-gap", |
| 502 ); | 513 12, |
| 503 const gap = Number.isFinite(configuredGap) | 514 ); |
| 504 ? configuredGap | |
| 505 : 12; | |
| 506 let expandedHeight = 0; | 515 let expandedHeight = 0; |
| 507 let newestHeight = 0; | 516 let newestHeight = 0; |
| 508 | 517 |
| 509 for (let index = visible.length - 1; index >= 0; index--) { | 518 for (let index = visible.length - 1; index >= 0; index--) { |
| 510 const record = visible[index]; | 519 const record = visible[index]; |
| 511 const article = record.view.article; | 520 const article = record.view.article; |
| 512 const depth = visible.length - 1 - index; | 521 const depth = visible.length - 1 - index; |
| 513 const height = article.offsetHeight; | 522 const height = article.offsetHeight; |
| 514 if (depth === 0) newestHeight = height; | 523 if (depth === 0) newestHeight = height; |
| 515 article.style.setProperty( | 524 article.style.setProperty( |
| 516 "--zen-notification-depth", | 525 "--zenbu-notification-depth", |
| 517 String(depth), | 526 String(depth), |
| 518 ); | 527 ); |
| 519 article.style.setProperty( | 528 article.style.setProperty( |
| 520 "--zen-notification-collapsed-offset", | 529 "--zenbu-notification-collapsed-offset", |
| 521 `${-depth * 10}px`, | 530 `${-depth * 10}px`, |
| 522 ); | 531 ); |
| 523 article.style.setProperty( | 532 article.style.setProperty( |
| 524 "--zen-notification-collapsed-opacity", | 533 "--zenbu-notification-collapsed-opacity", |
| 525 String(Math.max(0.55, 1 - depth * 0.18)), | 534 String(Math.max(0.55, 1 - depth * 0.18)), |
| 526 ); | 535 ); |
| 527 article.dataset.depth = String(depth); | 536 article.dataset.depth = String(depth); |
| 528 article.style.setProperty( | 537 article.style.setProperty( |
| 529 "--zen-notification-offset", | 538 "--zenbu-notification-offset", |
| 530 `${-expandedHeight}px`, | 539 `${-expandedHeight}px`, |
| 531 ); | 540 ); |
| 532 article.style.setProperty( | 541 article.style.setProperty( |
| 533 "--zen-notification-scale", | 542 "--zenbu-notification-scale", |
| 534 String(Math.max(0.88, 1 - depth * 0.055)), | 543 String(Math.max(0.88, 1 - depth * 0.055)), |
| 535 ); | 544 ); |
| 536 article.style.zIndex = String(visible.length - depth); | 545 article.style.zIndex = String(visible.length - depth); |
| 537 expandedHeight += height + gap; | 546 expandedHeight += height + gap; |
| 538 } | 547 } |
| 539 | 548 |
| 540 this.stack.dataset.count = String(visible.length); | 549 this.stack.dataset.count = String(visible.length); |
| 541 this.stack.style.setProperty( | 550 this.stack.style.setProperty( |
| 542 "--zen-notification-collapsed-height", | 551 "--zenbu-notification-collapsed-height", |
| 543 `${newestHeight + Math.max(0, visible.length - 1) * 10}px`, | 552 `${newestHeight + Math.max(0, visible.length - 1) * 10}px`, |
| 544 ); | 553 ); |
| 545 this.stack.style.setProperty( | 554 this.stack.style.setProperty( |
| 546 "--zen-notification-expanded-height", | 555 "--zenbu-notification-expanded-height", |
| 547 `${Math.max(0, expandedHeight - gap)}px`, | 556 `${Math.max(0, expandedHeight - gap)}px`, |
| 548 ); | 557 ); |
| 549 } | 558 } |
| 550 | 559 |
| 551 render() { | 560 render() { |