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 });
           ...