Window Management in pure JS + CSS
by Nicholas Berlette
HTML
<script type="importmap">
{
"imports": {
"uno.css": "https://unpkg.com/@unocss/[email protected]/full.global.js",
"@unocss/": "https://unpkg.com/@unocss/"
},
"scopes": {}
}
</script>
<script type="module">
const safelist = [
"opacity-0",
"display-none",
"opacity-100",
"display-inline",
"display-inline-block",
"display-block",
"bg-red-400",
"bg-blue-400",
"bg-red-800",
"window-buttons",
"panel-container",
"tab-container",
"a-tab",
"a-panel",
"window",
"terminal",
"terminal-window",
"bg-btn-close",
"bg-btn-minimize",
"bg-btn-maximize",
"blurred",
"focused",
"fullscreen",
"tab-active",
"tab-hidden",
"tab-button",
"subtitle",
"avatar",
]
globalThis.__unocss = {
get safelist() {
return [
...new Set([
...safelist,
...Object.keys(this.shortcuts),
...Object.values(this.shortcuts).flatMap((v) => v.split(/\s+/)),
]),
]
},
shortcuts: {
invisible: "opacity-0",
visible: "!opacity-100 inline-block",
"visible-block": "!opacity-100 block",
"btn-window":
"w-3 h-3 rounded-full block bg-btn-maximize ring-inset ring-1 ring-offset-1 ring-btn-maximize cursor-pointer shadow-sm hover:shadow hover:bg-green-300 hover:ring-btn-maximize transition-all duration-300 ease-in",
"btn-create-window":
"text-3xl font-black font-sans lowercase tracking-tight rounded-full px-5 py-4 absolute right-6 top-6 border-none text-white bg-gradient-to-tr via-indigo-800/10 from-cyan-600/30 ring-4 ring-offset-2 ring-offset-white hover:ring-green-200/80 cursor-pointer transition-all duration-300 ease-out shadow-lg hover:ring-6 ring-orange-300/80 hover:ring-offset-3.5 text-shadow-sm hover:animate-ping animate-pulse-alt",
blurred: "filter !blur-sm",
focused: "filter !blur-[0px]",
fullscreen: "!rounded-none",
"tab-hidden": "",
...
CSS
[un-cloak] {
opacity: 0;
transition: opacity 0.5s ease-in;
}
html,body{
@apply bg-slate-800;
font-family: sans-serif;
font-size:0.875rem;
}
/* .dark {
backgrounds (dark)
--bg-1: #111518bb;
--bg-2: #111827b0;
--bg-3: #20262eb0;
--bg-4: #234;
--bg-alpha-1: #ffffff05;
--text-1: #fff;
--text-2: #ddd;
--text-3: #fffa;
--text-4: #789;
--border-1: #000a;
--border-2: #fff3;
--shadow-1: #0004;
--shadow-2: #0003;
--shadow-3: #0002;
--shadow-4: #000f;
--btn-shadow: #fff3;
--line-numbers-bg: #111;
--line-numbers-text: #ddd;
--border-title: #0009;
}
.light {
--bg-1: #f0f0f0;
--bg-2: #ddddea;
--bg-3: #ececec;
--bg-4: #9ab;
--bg-alpha-1: #fff6;
--text-1: #000;
--text-2: #123;
--text-3: #000a;
--text-4: #456;
--border-1: #0004;
--border-2: #0003;
--border-title: #0003;
--shadow-1: #0004;
--shadow-2: #0003;
--shadow-3: #0002;
--shadow-4: #0004;
--btn-shadow: #fff6;
--line-numbers-bg: #e0e0e966;
--line-numbers-text: #444;
} */
:root {
--animate-duration: 400ms;
--animate-delay: 400ms;
--animate-repeat: 1;
--base00: #232a2f;
--base01: #444267;
--base02: #32374d;
--base03: #707a84;
--base04: #8796b0;
--base05: #939da5;
--base06: #939da5;
--base07: #ffffff;
--base08: #ff6a80;
--base09: #ffa763;
--base0A: #ffea6b;
--base0B: #5bec95;
--base0C: #89ddff;
--base0D: #82aaff;
--base0E: #ba8ef7;
--base0F: #ff8ad1;
--base0G: #1a2023;
--font-mono:'OperatorMonoSSmLig','OperatorMono SSm','Operator Mono','Dank Mono','MonoLisa','IBM Plex Mono',monospace;
--font-sans:'Gotham Narrow', 'IBM Plex Sans', ui-sans-serif, sans-serif;
}
.syntax-dark {
--snippet-bg: #204062;
/* --snippet-bg: ; */
--base00: var(--override-base00, #232a2f);
--base01: var(--override-base01, #444267);
--base02: var(--override-base02, #32374d);
--base03: var(--override-base03, #707a84);
--base04:...
JavaScript
class BaseWindowButtonElement extends HTMLElement {
#window = null;
#type = null;
#types = ["close", "minimize", "maximize"];
constructor(type = "close", window = null) {
super();
if (new.target === BaseWindowButtonElement) {
throw new TypeError("Illegal constructor");
}
this.$shadowRoot = this.attachShadow({ mode: 'closed' });
this.$subtype = new.target || this.constructor;
if (type != null) this.type = type;
if (window != null) this.window = window;
}
get type() {
return this.#type ??= this.#types[0];
}
set type(value) {
if (this.#types.includes(value)) this.#type = value;
}
get window() {
if (!this.#window) {
if (this.hasAttribute("window_id")) {
this.window = this.getAttribute("window_id");
}
}
return typeof this.#window === "string" ? document.querySelector(this.#window) : this.#window;
}
set window(value) {
if (typeof value === "string") {
value = value.startsWith("#") ? `[id='${value.slice(1)}']` : value.startsWith(".") ? `[class*='${value.slice(1)}']` : value.startsWith("[") && value.endsWith("]") ? value : `[id="${value}"]`;
this.#window = document.querySelector(value);
} else if (value instanceof HTMLElement) {
this.#window = value;
}
}
set window_id(value) {
this.window = value;
}
clickHandler = (e) => {
switch (this.type) {
case "close": {
this.window?.close();
} break;
case "minimize": {
this.window?.minimize();
/* if (document.exitFullscreen) document.exitFullscreen();
const bounding = this.window.getBoundingClientRect();
const panelsBBox = this.window.panelContainer.getBoundingClientRect();
if (panels) {
if (this.$isMinimized) {
panels.animate?.({ height: (this.$panelsBBox ??= panelsBBox).height, opacity: 1 }, { duration: 600, easing: "ease-in-out", fill: "both", delay: 50 });
...