ATProto app badge generator for static web pages
colddark.world/tools/badger/index.html
atproto
web-app
vanilla-js
web-components
oauth
1/*
2Toaster(-inator) - Easy to use toast message element/library thing.
3Copyright (C) 2026 Grant Mulholland <toasterinator@colddark.world>
4
5This program is free software: you can redistribute it and/or modify
6it under the terms of the GNU General Public License as published by
7the Free Software Foundation, either version 3 of the License, or
8(at your option) any later version.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13GNU General Public License for more details.
14
15You should have received a copy of the GNU General Public License
16along with this program. If not, see <https://www.gnu.org/licenses/>.
17*/
18
19class ToasterElement extends HTMLElement {
20 #messages = document.createElement("div");
21 constructor() {
22 super();
23 this.#messages.id = "messages";
24 }
25 connectedCallback() {
26 const shadow = this.attachShadow({ mode: "open" });
27
28 const style = document.createElement("style");
29 style.textContent = `
30 :host {
31 --space: 1ch;
32 }
33 #messages {
34 position: absolute;
35 left: var(--space);
36 bottom: var(--space);
37 transition: translate 0.5s;
38 }
39 .box {
40 background-color: #eee;
41 border: 0.5ch solid #ccc;
42 border-radius: 1ch;
43 margin: var(--space);
44 padding: 1ch;
45 position: relative;
46 width: fit-content;
47 }
48 .box span {
49 padding-inline: 1ch;
50 }
51 @media (prefers-color-scheme: dark) {
52 .box {
53 background-color: #444;
54 border-color: #222;
55 }
56 }
57 .error {
58 border-color: indianred;
59 }
60 .info {
61 border-color: dodgerblue;
62 }
63 `;
64
65 shadow.appendChild(style);
66 shadow.appendChild(this.#messages);
67 }
68 #message(type, msg) {
69 const el = document.createElement("div");
70 el.classList.add(type, "box");
71
72 const btn = document.createElement("button");
73 btn.textContent = "×";
74 btn.classList.add("close-button");
75 btn.addEventListener("click", ()=>{
76 el.remove();
77 });
78
79 const text = document.createElement("span");
80 text.textContent = msg;
81
82 el.appendChild(btn);
83 el.appendChild(text);
84
85 const oldTop = this.#messages.getBoundingClientRect().top;
86 this.#messages.appendChild(el);
87 const newTop = this.#messages.getBoundingClientRect().top;
88 this.#messages.animate([
89 {translate: `0 ${oldTop-newTop}px`},
90 {translate: "0 0"}
91 ], {duration: 500, iterations: 1, easing: "ease"});
92 }
93 info(msg) {
94 this.#message("info", msg);
95 }
96 error(msg) {
97 this.#message("error", msg);
98 }
99}
100
101if (!customElements.get("toaster-inator")) {
102 customElements.define(
103 "toaster-inator",
104 ToasterElement
105 );
106}
107
108let instances = document.getElementsByTagName("toaster-inator");
109if (instances.length == 0) {
110 instances = [document.createElement("toaster-inator")];
111 document.body.appendChild(instances[0]);
112}
113/**
114 * @type {ToasterElement} Automatically created toaster instance.
115 */
116export const toaster = instances[0];
117// call it a crime if you want, it's handy
118globalThis.toaster = toaster;