34ed127653
New <toast-host> overlay: components post 'toast' CustomEvents ({message, kind: success|error|info}) and it shows brief, auto-dismissing, bottom-right toasts (errors linger). <repo-menu> (git + coordination actions) and <pr-list> (create/merge) now post friendly success/error toasts instead of alert(); the activity feed still logs everything. Also adds the slice 11 CHANGELOG entry. Verified live: 'Checked for updates' toast shown.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68 lines
2.4 KiB
JavaScript
68 lines
2.4 KiB
JavaScript
// <toast-host> — a singleton overlay that shows brief command-result toasts.
|
|
//
|
|
// A self-contained control (AGENT.md §1.1): shadow DOM, no data of its own. Any
|
|
// component posts a toast by dispatching a `toast` CustomEvent on document:
|
|
// document.dispatchEvent(new CustomEvent('toast',
|
|
// { detail: { message: 'Published', kind: 'success' } }));
|
|
// kind ∈ success | error | info. Toasts stack bottom-right, auto-dismiss (errors
|
|
// linger longer), and dismiss on click.
|
|
|
|
class ToastHost extends HTMLElement {
|
|
#onToast = null;
|
|
|
|
constructor() {
|
|
super();
|
|
this.attachShadow({ mode: 'open' });
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.#render();
|
|
this.#onToast = (e) => this.#show(e.detail || {});
|
|
document.addEventListener('toast', this.#onToast);
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
document.removeEventListener('toast', this.#onToast);
|
|
}
|
|
|
|
#show({ message, kind = 'info', timeout }) {
|
|
if (!message) return;
|
|
const t = document.createElement('div');
|
|
t.className = 'toast ' + (['success', 'error', 'info'].includes(kind) ? kind : 'info');
|
|
t.textContent = String(message);
|
|
t.addEventListener('click', () => t.remove());
|
|
this.shadowRoot.getElementById('stack').appendChild(t);
|
|
requestAnimationFrame(() => t.classList.add('in'));
|
|
const ms = timeout || (kind === 'error' ? 6000 : 3500);
|
|
setTimeout(() => {
|
|
t.classList.remove('in');
|
|
setTimeout(() => t.remove(), 200);
|
|
}, ms);
|
|
}
|
|
|
|
#render() {
|
|
this.shadowRoot.innerHTML = `
|
|
<style>
|
|
#stack {
|
|
position: fixed; right: 16px; bottom: 16px; z-index: 1100;
|
|
display: flex; flex-direction: column-reverse; gap: 8px;
|
|
max-width: min(360px, 90vw);
|
|
}
|
|
.toast {
|
|
background: var(--surface-2); color: var(--color-fg);
|
|
border: 1px solid var(--border-strong); border-left-width: 3px;
|
|
border-radius: var(--radius); padding: 10px 14px; font-size: 13px;
|
|
box-shadow: 0 8px 28px rgba(0,0,0,.45); cursor: pointer;
|
|
opacity: 0; transform: translateY(8px); transition: opacity .18s, transform .18s;
|
|
}
|
|
.toast.in { opacity: 1; transform: none; }
|
|
.toast.success { border-left-color: var(--color-success); }
|
|
.toast.error { border-left-color: var(--color-danger); }
|
|
.toast.info { border-left-color: var(--fill-accent); }
|
|
</style>
|
|
<div id="stack"></div>`;
|
|
}
|
|
}
|
|
|
|
customElements.define('toast-host', ToastHost);
|