From f4879721ac1d4b1347d58445fd00d4c90a4f2e1a Mon Sep 17 00:00:00 2001
From: YuviPanda <yuvipanda@gmail.com>
Date: Sat, 3 Feb 2024 10:16:03 -0800
Subject: [PATCH 01/14] Cleanup JS just a bit

- Remove ability to connect to arbitrary hosts and ports
  via query params. We already required websockify anyway, so
  this never worked. Plus, we only care about using the viewer for
  our own purposes, not as a general purpose novnc thing
- Get rid of the ctrlAltDelete function. It's not particularly useful
  in this context. I will work on adding more UI here, but this
  location is a good place for other buttons
---
 js/index.js                                   | 79 +------------------
 .../static/index.html                         |  2 -
 2 files changed, 4 insertions(+), 77 deletions(-)

diff --git a/js/index.js b/js/index.js
index 3a3df481..42911b5c 100644
--- a/js/index.js
+++ b/js/index.js
@@ -24,100 +24,29 @@ function disconnectedFromServer(e) {
   }
 }
 
-// When this function is called, the server requires
-// credentials to authenticate
-function credentialsAreRequired(e) {
-  const password = prompt("Password Required:");
-  rfb.sendCredentials({ password: password });
-}
-
 // When this function is called we have received
 // a desktop name from the server
 function updateDesktopName(e) {
   desktopName = e.detail.name;
 }
 
-// Since most operating systems will catch Ctrl+Alt+Del
-// before they get a chance to be intercepted by the browser,
-// we provide a way to emulate this key sequence.
-function sendCtrlAltDel() {
-  rfb.sendCtrlAltDel();
-  return false;
-}
-
 // Show a status text in the top bar
 function status(text) {
   document.getElementById("status").textContent = text;
 }
 
-// This function extracts the value of one variable from the
-// query string. If the variable isn't defined in the URL
-// it returns the default value instead.
-function readQueryVariable(name, defaultValue) {
-  // A URL with a query parameter can look like this:
-  // https://www.example.com?myqueryparam=myvalue
-  //
-  // Note that we use location.href instead of location.search
-  // because Firefox < 53 has a bug w.r.t location.search
-  const re = new RegExp(".*[?&]" + name + "=([^&#]*)"),
-    match = document.location.href.match(re);
-
-  if (match) {
-    // We have to decode the URL since want the cleartext value
-    return decodeURIComponent(match[1]);
-  }
-
-  return defaultValue;
-}
-
-document.getElementById("sendCtrlAltDelButton").onclick = sendCtrlAltDel;
-
-// Read parameters specified in the URL query string
-// By default, use the host and port of server that served this file
-const host = readQueryVariable("host", window.location.hostname);
-let port = readQueryVariable("port", window.location.port);
-const password = readQueryVariable("password");
-
-const path = readQueryVariable(
-  "path",
-  window.location.pathname.replace(/[^/]*$/, "").substring(1) + "websockify",
-);
-
-// | | |         | | |
-// | | | Connect | | |
-// v v v         v v v
-
-status("Connecting");
-
-// Build the websocket URL used to connect
-let url;
-if (window.location.protocol === "https:") {
-  url = "wss";
-} else {
-  url = "ws";
-}
-url += "://" + host;
-if (port) {
-  url += ":" + port;
-}
-url += "/" + path;
+// Construct the websockify websocket URL we want to connect to
+let websockifyUrl = new URL("websockify", window.location);
+websockifyUrl.protocol = window.location.protocol === "https" ? "wss" : "ws";
 
 // Creating a new RFB object will start a new connection
-rfb = new RFB(document.getElementById("screen"), url, {
-  credentials: { password: password },
-});
+rfb = new RFB(document.getElementById("screen"), websockifyUrl.toString(), {});
 
 // Add listeners to important events from the RFB module
 rfb.addEventListener("connect", connectedToServer);
 rfb.addEventListener("disconnect", disconnectedFromServer);
-rfb.addEventListener("credentialsrequired", credentialsAreRequired);
 rfb.addEventListener("desktopname", updateDesktopName);
 
-// Set parameters that can be changed on an active connection
-rfb.viewOnly = readQueryVariable("view_only", false);
-
-rfb.scaleViewport = readQueryVariable("scale", true);
-
 // Clipboard
 function toggleClipboardPanel() {
   document
diff --git a/jupyter_remote_desktop_proxy/static/index.html b/jupyter_remote_desktop_proxy/static/index.html
index 376f261e..d8d5021d 100644
--- a/jupyter_remote_desktop_proxy/static/index.html
+++ b/jupyter_remote_desktop_proxy/static/index.html
@@ -30,8 +30,6 @@
           <textarea id="noVNC_clipboard_text" rows="5"></textarea>
         </div>
       </div>
-
-      <div id="sendCtrlAltDelButton">Send CtrlAltDel</div>
     </div>
     <div id="screen">
       <!-- This is where the remote screen will appear -->

From 21efc204cd48416b4699516a353576a2d42dd707 Mon Sep 17 00:00:00 2001
From: YuviPanda <yuvipanda@gmail.com>
Date: Sat, 3 Feb 2024 10:21:24 -0800
Subject: [PATCH 02/14] Add eslint & fix the one issue it found

---
 .pre-commit-config.yaml | 9 +++++++++
 js/index.js             | 2 +-
 package.json            | 1 +
 3 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9a0c50bd..cf374582 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -68,6 +68,15 @@ repos:
     hooks:
       - id: flake8
 
+  # Lint: JS code
+  - repo: https://github.com/pre-commit/mirrors-eslint
+    rev: "v8.56.0" # Use the sha / tag you want to point at
+    hooks:
+      - id: eslint
+        files: \.jsx?$
+        types: [file]
+        exclude: jupyter_remote_desktop_proxy/static/dist
+
 # Content here is mostly copied from other locations, so lets not make
 # formatting changes in it.
 exclude: share
diff --git a/js/index.js b/js/index.js
index 42911b5c..ff35c679 100644
--- a/js/index.js
+++ b/js/index.js
@@ -11,7 +11,7 @@ let desktopName;
 
 // When this function is called we have
 // successfully connected to a server
-function connectedToServer(e) {
+function connectedToServer() {
   status("Connected to " + desktopName);
 }
 
diff --git a/package.json b/package.json
index 11c795b8..115ff4e3 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
   },
   "devDependencies": {
     "babel-loader": "^9.1.3",
+    "eslint": "^8.56.0",
     "webpack": "^5.90.1",
     "webpack-cli": "^5.1.4"
   }

From 389f265b4336ad8c2a506abdc52ce5a542ab316a Mon Sep 17 00:00:00 2001
From: YuviPanda <yuvipanda@gmail.com>
Date: Sat, 3 Feb 2024 10:22:19 -0800
Subject: [PATCH 03/14] Add missing eslintrc file

---
 .eslintrc.js | 30 ++++++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)
 create mode 100644 .eslintrc.js

diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 00000000..fdb4321a
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,30 @@
+module.exports = {
+  env: {
+    browser: true,
+    es2021: true,
+  },
+  extends: ["eslint:recommended"],
+  overrides: [
+    {
+      env: {
+        node: true,
+      },
+      files: [".eslintrc.{js,cjs}"],
+      parserOptions: {
+        sourceType: "script",
+      },
+    },
+  ],
+  parserOptions: {
+    ecmaVersion: "latest",
+    sourceType: "module",
+  },
+  plugins: [],
+  rules: {
+    "no-unused-vars": ["error", { args: "after-used" }],
+  },
+  ignorePatterns: [
+    "jupyter_remote_desktop_proxy/static/dist/**",
+    "webpack.config.js",
+  ],
+};

From 7674f14870606c15615ad5c1c46f29d10a6b5d26 Mon Sep 17 00:00:00 2001
From: YuviPanda <yuvipanda@gmail.com>
Date: Sat, 3 Feb 2024 10:30:47 -0800
Subject: [PATCH 04/14] Stop showing the desktop name

This is almost always a random sequence of strings and is
not useful.
---
 js/index.js | 21 +++++++--------------
 1 file changed, 7 insertions(+), 14 deletions(-)

diff --git a/js/index.js b/js/index.js
index ff35c679..4cbd5a20 100644
--- a/js/index.js
+++ b/js/index.js
@@ -6,13 +6,9 @@
 // RFB holds the API to connect and communicate with a VNC server
 import RFB from "@novnc/novnc/core/rfb";
 
-let rfb;
-let desktopName;
-
-// When this function is called we have
-// successfully connected to a server
+// When this function is called we have successfully connected to a server
 function connectedToServer() {
-  status("Connected to " + desktopName);
+  status("Connected!");
 }
 
 // This function is called when we are disconnected
@@ -24,12 +20,6 @@ function disconnectedFromServer(e) {
   }
 }
 
-// When this function is called we have received
-// a desktop name from the server
-function updateDesktopName(e) {
-  desktopName = e.detail.name;
-}
-
 // Show a status text in the top bar
 function status(text) {
   document.getElementById("status").textContent = text;
@@ -40,12 +30,15 @@ let websockifyUrl = new URL("websockify", window.location);
 websockifyUrl.protocol = window.location.protocol === "https" ? "wss" : "ws";
 
 // Creating a new RFB object will start a new connection
-rfb = new RFB(document.getElementById("screen"), websockifyUrl.toString(), {});
+const rfb = new RFB(
+  document.getElementById("screen"),
+  websockifyUrl.toString(),
+  {},
+);
 
 // Add listeners to important events from the RFB module
 rfb.addEventListener("connect", connectedToServer);
 rfb.addEventListener("disconnect", disconnectedFromServer);
-rfb.addEventListener("desktopname", updateDesktopName);
 
 // Clipboard
 function toggleClipboardPanel() {

From b411bf2e7c638e836f163e2e0ff9b2a67dbd60a9 Mon Sep 17 00:00:00 2001
From: YuviPanda <yuvipanda@gmail.com>
Date: Sat, 3 Feb 2024 12:19:00 -0800
Subject: [PATCH 05/14] Set scaleViewPort to true

Was missed when I undid all the queryparam stuff
---
 js/index.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/js/index.js b/js/index.js
index 4cbd5a20..bf262251 100644
--- a/js/index.js
+++ b/js/index.js
@@ -40,6 +40,9 @@ const rfb = new RFB(
 rfb.addEventListener("connect", connectedToServer);
 rfb.addEventListener("disconnect", disconnectedFromServer);
 
+// Scale our viewport so the user doesn't have to scroll
+rfb.scaleViewport = true;
+
 // Clipboard
 function toggleClipboardPanel() {
   document

From f932075b52ace07f0b0e117284d02ededcdc8c5f Mon Sep 17 00:00:00 2001
From: YuviPanda <yuvipanda@gmail.com>
Date: Sat, 3 Feb 2024 13:36:04 -0800
Subject: [PATCH 06/14] Cleanup styles

- Include a CSS reset
- Use Jupyter Brand Colors
- Add a helpful bit of info for clipboard popout
---
 js/index.css                                  | 86 +++++++++++++++++++
 js/index.js                                   | 15 ++--
 jupyter_remote_desktop_proxy/static/index.css | 79 -----------------
 .../static/index.html                         | 28 +++---
 package.json                                  |  6 +-
 webpack.config.js                             |  8 +-
 6 files changed, 121 insertions(+), 101 deletions(-)
 create mode 100644 js/index.css
 delete mode 100644 jupyter_remote_desktop_proxy/static/index.css

diff --git a/js/index.css b/js/index.css
new file mode 100644
index 00000000..a0e10f78
--- /dev/null
+++ b/js/index.css
@@ -0,0 +1,86 @@
+/**
+* Derived from https://github.com/novnc/noVNC/blob/v1.4.0/vnc_lite.html, which was licensed
+* under the 2-clause BSD license
+*/
+
+html {
+  /**
+  Colors from https://github.com/jupyter/design/blob/main/brandguide/brand_guide.pdf
+  **/
+  --jupyter-main-brand-color: #f37626;
+  --jupyter-dark-grey: #4d4d4d;
+  --jupyter-medium-dark-grey: #616161;
+  --jupyter-medium-grey: #757575;
+  --jupyter-grey: #9e9e9e;
+
+  --topbar-height: 32px;
+}
+
+body {
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background-color: var(--jupyter-medium-dark-grey);
+}
+
+#top-bar {
+  background-color: var(--jupyter-main-brand-color);
+  color: white;
+  font: bold 12px Helvetica;
+  border-bottom: 1px white;
+  display: flex;
+  align-items: center;
+  height: var(--topbar-height);
+}
+
+#status {
+  text-align: center;
+  flex-grow: 1;
+}
+
+#screen {
+  flex: 1;
+  /* fill remaining space */
+  overflow: hidden;
+}
+
+.hidden {
+  display: none !important;
+}
+
+/* Clipboard */
+#clipboard-area {
+  position: fixed;
+  top: var(--topbar-height);
+  left: 0px;
+  padding: 4px;
+}
+
+#clipboard-button {
+  border: 1px outset;
+  cursor: pointer;
+}
+
+#clipboard-button img {
+  height: 24px;
+  vertical-align: middle;
+}
+
+#clipboard-button .label {
+  padding: 5px 5px 4px 0px;
+}
+
+#clipboard-area {
+  /* Full screen, minus padding and left and right margins */
+  display: flex;
+  flex-direction: column;
+  background-color: white;
+  border-bottom-right-radius: 4px;
+  border: 1px solid var(--jupyter-medium-grey);
+}
+
+#clipboard-text {
+  min-width: 500px;
+  max-width: 100%;
+  border: 1px solid var(--jupyter-medium-grey);
+}
diff --git a/js/index.js b/js/index.js
index bf262251..185645a9 100644
--- a/js/index.js
+++ b/js/index.js
@@ -3,6 +3,9 @@
  * under the 2-clause BSD license
  */
 
+import "reset-css";
+import "./index.css";
+
 // RFB holds the API to connect and communicate with a VNC server
 import RFB from "@novnc/novnc/core/rfb";
 
@@ -45,23 +48,21 @@ rfb.scaleViewport = true;
 
 // Clipboard
 function toggleClipboardPanel() {
-  document
-    .getElementById("noVNC_clipboard_area")
-    .classList.toggle("noVNC_clipboard_closed");
+  document.getElementById("clipboard-area").classList.toggle("hidden");
 }
 document
-  .getElementById("noVNC_clipboard_button")
+  .getElementById("clipboard-button")
   .addEventListener("click", toggleClipboardPanel);
 
 function clipboardReceive(e) {
-  document.getElementById("noVNC_clipboard_text").value = e.detail.text;
+  document.getElementById("clipboard-text").value = e.detail.text;
 }
 rfb.addEventListener("clipboard", clipboardReceive);
 
 function clipboardSend() {
-  const text = document.getElementById("noVNC_clipboard_text").value;
+  const text = document.getElementById("clipboard-text").value;
   rfb.clipboardPasteFrom(text);
 }
 document
-  .getElementById("noVNC_clipboard_text")
+  .getElementById("clipboard-text")
   .addEventListener("change", clipboardSend);
diff --git a/jupyter_remote_desktop_proxy/static/index.css b/jupyter_remote_desktop_proxy/static/index.css
deleted file mode 100644
index 226934b8..00000000
--- a/jupyter_remote_desktop_proxy/static/index.css
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
-* Derived from https://github.com/novnc/noVNC/blob/v1.4.0/vnc_lite.html, which was licensed
-* under the 2-clause BSD license
-*/
-
-body {
-  margin: 0;
-  background-color: dimgrey;
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-}
-
-html {
-  height: 100%;
-}
-
-#top_bar {
-  background-color: #6e84a3;
-  color: white;
-  font: bold 12px Helvetica;
-  padding: 6px 5px 4px 5px;
-  border-bottom: 1px outset;
-}
-
-#status {
-  text-align: center;
-}
-
-#sendCtrlAltDelButton {
-  position: fixed;
-  top: 0px;
-  right: 0px;
-  border: 1px outset;
-  padding: 5px 5px 4px 5px;
-  cursor: pointer;
-}
-
-#screen {
-  flex: 1;
-  /* fill remaining space */
-  overflow: hidden;
-}
-
-/* Clipboard */
-#noVNC_clipboard_area {
-  position: fixed;
-  top: 0px;
-  left: 0px;
-}
-
-#noVNC_clipboard_button {
-  border: 1px outset;
-  cursor: pointer;
-}
-
-#noVNC_clipboard_button img {
-  height: 24px;
-  vertical-align: middle;
-}
-
-#noVNC_clipboard_button .label {
-  padding: 5px 5px 4px 0px;
-}
-
-#noVNC_clipboard {
-  /* Full screen, minus padding and left and right margins */
-  max-width: calc(100vw - 2 * 15px - 75px - 25px);
-  background-color: #6e84a3;
-}
-
-.noVNC_clipboard_closed #noVNC_clipboard {
-  display: none;
-}
-
-#noVNC_clipboard_text {
-  width: 500px;
-  max-width: 100%;
-}
diff --git a/jupyter_remote_desktop_proxy/static/index.html b/jupyter_remote_desktop_proxy/static/index.html
index d8d5021d..e0c49625 100644
--- a/jupyter_remote_desktop_proxy/static/index.html
+++ b/jupyter_remote_desktop_proxy/static/index.html
@@ -13,28 +13,30 @@
                 Chrome Frame. -->
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 
-    <link href="./index.css" rel="stylesheet" />
+    <link href="./dist/index.css" rel="stylesheet" />
   </head>
 
   <body>
-    <div id="top_bar">
-      <div id="status">Loading</div>
-
-      <!-- Clipboard -->
-      <div id="noVNC_clipboard_area" class="noVNC_clipboard_closed">
-        <div id="noVNC_clipboard_button">
-          <img src="./clipboard.svg" />
-          <span class="label">Clipboard</span>
-        </div>
-        <div id="noVNC_clipboard">
-          <textarea id="noVNC_clipboard_text" rows="5"></textarea>
-        </div>
+    <div id="top-bar">
+      <div id="clipboard-button">
+        <img src="./clipboard.svg" />
+        <span class="label">Clipboard</span>
       </div>
+      <div id="status">Loading...</div>
     </div>
     <div id="screen">
       <!-- This is where the remote screen will appear -->
     </div>
 
+    <!-- Clipboard -->
+    <div id="clipboard-area" class="hidden">
+      <span
+        >Contents of this text box are synced with the clipboard on the remote
+        desktop</span
+      >
+      <textarea id="clipboard-text" rows="5"></textarea>
+    </div>
+
     <script src="./dist/viewer.js"></script>
   </body>
 </html>
diff --git a/package.json b/package.json
index 115ff4e3..4551affb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,7 @@
 {
   "dependencies": {
-    "@novnc/novnc": "^1.4.0"
+    "@novnc/novnc": "^1.4.0",
+    "reset-css": "^5.0.2"
   },
   "scripts": {
     "webpack": "webpack",
@@ -8,7 +9,10 @@
   },
   "devDependencies": {
     "babel-loader": "^9.1.3",
+    "css-loader": "^6.10.0",
     "eslint": "^8.56.0",
+    "mini-css-extract-plugin": "^2.8.0",
+    "style-loader": "^3.3.4",
     "webpack": "^5.90.1",
     "webpack-cli": "^5.1.4"
   }
diff --git a/webpack.config.js b/webpack.config.js
index e86067f6..6ec22bc8 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,8 +1,14 @@
 const webpack = require("webpack");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 const path = require("path");
 
 module.exports = {
   entry: path.resolve(__dirname, "js/index.js"),
+  plugins: [
+    new MiniCssExtractPlugin({
+      filename: "index.css",
+    }),
+  ],
   devtool: "source-map",
   mode: "development",
   module: {
@@ -14,7 +20,7 @@ module.exports = {
       },
       {
         test: /\.(css)/,
-        use: ["style-loader", "css-loader"],
+        use: [MiniCssExtractPlugin.loader, "css-loader"],
       },
     ],
   },

From 3d47e0ab54986670f2387dd02cdfc28fb2818701 Mon Sep 17 00:00:00 2001
From: YuviPanda <yuvipanda@gmail.com>
Date: Sat, 3 Feb 2024 13:54:50 -0800
Subject: [PATCH 07/14] Add a Jupyter Logo that takes you back home

---
 js/index.css                                  | 21 ++++-
 js/index.js                                   |  3 +
 .../static/index.html                         | 14 +--
 .../static/jupyter-logo.svg                   | 88 +++++++++++++++++++
 4 files changed, 118 insertions(+), 8 deletions(-)
 create mode 100644 jupyter_remote_desktop_proxy/static/jupyter-logo.svg

diff --git a/js/index.css b/js/index.css
index a0e10f78..366eee7c 100644
--- a/js/index.css
+++ b/js/index.css
@@ -24,15 +24,31 @@ body {
 }
 
 #top-bar {
-  background-color: var(--jupyter-main-brand-color);
+  background-color: var(--jupyter-dark-grey);
   color: white;
-  font: bold 12px Helvetica;
+  font: 12px Helvetica;
   border-bottom: 1px white;
   display: flex;
   align-items: center;
   height: var(--topbar-height);
 }
 
+#logo {
+  padding: 0 32px;
+}
+
+#logo img {
+  height: 24px;
+}
+
+#menu {
+  display: flex;
+  font-weight: bold;
+}
+
+#menu li {
+}
+
 #status {
   text-align: center;
   flex-grow: 1;
@@ -57,7 +73,6 @@ body {
 }
 
 #clipboard-button {
-  border: 1px outset;
   cursor: pointer;
 }
 
diff --git a/js/index.js b/js/index.js
index 185645a9..cc2e6728 100644
--- a/js/index.js
+++ b/js/index.js
@@ -46,6 +46,9 @@ rfb.addEventListener("disconnect", disconnectedFromServer);
 // Scale our viewport so the user doesn't have to scroll
 rfb.scaleViewport = true;
 
+// Use a CSS variable to set background color
+rfb.background = "var(--jupyter-medium-dark-grey)";
+
 // Clipboard
 function toggleClipboardPanel() {
   document.getElementById("clipboard-area").classList.toggle("hidden");
diff --git a/jupyter_remote_desktop_proxy/static/index.html b/jupyter_remote_desktop_proxy/static/index.html
index e0c49625..aa3fe8ee 100644
--- a/jupyter_remote_desktop_proxy/static/index.html
+++ b/jupyter_remote_desktop_proxy/static/index.html
@@ -18,11 +18,15 @@
 
   <body>
     <div id="top-bar">
-      <div id="clipboard-button">
-        <img src="./clipboard.svg" />
-        <span class="label">Clipboard</span>
-      </div>
-      <div id="status">Loading...</div>
+      <a href=".." id="logo">
+        <img src="./jupyter-logo.svg" />
+      </a>
+      <span id="status">Connecting...</span>
+      <ul id="menu">
+        <li id="clipboard-button">
+          <span class="label">Clipboard</span>
+        </li>
+      </ul>
     </div>
     <div id="screen">
       <!-- This is where the remote screen will appear -->
diff --git a/jupyter_remote_desktop_proxy/static/jupyter-logo.svg b/jupyter_remote_desktop_proxy/static/jupyter-logo.svg
new file mode 100644
index 00000000..fde0d6e3
--- /dev/null
+++ b/jupyter_remote_desktop_proxy/static/jupyter-logo.svg
@@ -0,0 +1,88 @@
+<svg width="189" height="51" viewBox="0 0 189 51" version="2.0" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:figma="http://www.figma.com/figma/ns">
+<title>logo-1.svg</title>
+<desc>Created using Figma 0.90</desc>
+<g id="Canvas" transform="translate(-1893 -2093)" figma:type="canvas">
+<g id="logo" style="mix-blend-mode:normal;" figma:type="group">
+<g id="Group" style="mix-blend-mode:normal;" figma:type="group">
+<g id="g" style="mix-blend-mode:normal;" figma:type="group">
+<g id="path" style="mix-blend-mode:normal;" figma:type="group">
+<g id="path0 fill" style="mix-blend-mode:normal;" figma:type="vector">
+<use xlink:href="#path0_fill" transform="translate(1943.87 2106.23)" fill="#FFFFFF" style="mix-blend-mode:normal;"/>
+</g>
+</g>
+<g id="path" style="mix-blend-mode:normal;" figma:type="group">
+<g id="path1 fill" style="mix-blend-mode:normal;" figma:type="vector">
+<use xlink:href="#path1_fill" transform="translate(1960.38 2106.19)" fill="#FFFFFF" style="mix-blend-mode:normal;"/>
+</g>
+</g>
+<g id="path" style="mix-blend-mode:normal;" figma:type="group">
+<g id="path2 fill" style="mix-blend-mode:normal;" figma:type="vector">
+<use xlink:href="#path2_fill" transform="translate(1985.18 2105.67)" fill="#FFFFFF" style="mix-blend-mode:normal;"/>
+</g>
+</g>
+<g id="path" style="mix-blend-mode:normal;" figma:type="group">
+<g id="path3 fill" style="mix-blend-mode:normal;" figma:type="vector">
+<use xlink:href="#path3_fill" transform="translate(2007.94 2106.21)" fill="#FFFFFF" style="mix-blend-mode:normal;"/>
+</g>
+</g>
+<g id="path" style="mix-blend-mode:normal;" figma:type="group">
+<g id="path4 fill" style="mix-blend-mode:normal;" figma:type="vector">
+<use xlink:href="#path4_fill" transform="translate(2030.8 2100.04)" fill="#FFFFFF" style="mix-blend-mode:normal;"/>
+</g>
+</g>
+<g id="path" style="mix-blend-mode:normal;" figma:type="group">
+<g id="path5 fill" style="mix-blend-mode:normal;" figma:type="vector">
+<use xlink:href="#path5_fill" transform="translate(2046.75 2105.71)" fill="#FFFFFF" style="mix-blend-mode:normal;"/>
+</g>
+</g>
+<g id="path" style="mix-blend-mode:normal;" figma:type="group">
+<g id="path6 fill" style="mix-blend-mode:normal;" figma:type="vector">
+<use xlink:href="#path6_fill" transform="translate(2070.77 2105.72)" fill="#FFFFFF" style="mix-blend-mode:normal;"/>
+</g>
+</g>
+</g>
+</g>
+<g id="g" style="mix-blend-mode:normal;" figma:type="group">
+<g id="path" style="mix-blend-mode:normal;" figma:type="group">
+<g id="path7 fill" style="mix-blend-mode:normal;" figma:type="vector">
+<use xlink:href="#path7_fill" transform="translate(1924.3 2093.31)" fill="#FFFFFF" style="mix-blend-mode:normal;"/>
+</g>
+</g>
+<g id="path" style="mix-blend-mode:normal;" figma:type="group">
+<g id="path8 fill" style="mix-blend-mode:normal;" figma:type="vector">
+<use xlink:href="#path8_fill" transform="translate(1894.74 2123.98)" fill="#F37726" style="mix-blend-mode:normal;"/>
+</g>
+</g>
+<g id="path" style="mix-blend-mode:normal;" figma:type="group">
+<g id="path9 fill" style="mix-blend-mode:normal;" figma:type="vector">
+<use xlink:href="#path9_fill" transform="translate(1894.73 2097.48)" fill="#F37726" style="mix-blend-mode:normal;"/>
+</g>
+</g>
+<g id="path" style="mix-blend-mode:normal;" figma:type="group">
+<g id="path10 fill" style="mix-blend-mode:normal;" figma:type="vector">
+<use xlink:href="#path10_fill" transform="translate(1894.8 2135.81)" fill="#FFFFFF" style="mix-blend-mode:normal;"/>
+</g>
+</g>
+<g id="path" style="mix-blend-mode:normal;" figma:type="group">
+<g id="path11 fill" style="mix-blend-mode:normal;" figma:type="vector">
+<use xlink:href="#path11_fill" transform="translate(1893.36 2098.06)" fill="#FFFFFF" style="mix-blend-mode:normal;"/>
+</g>
+</g>
+</g>
+</g>
+</g>
+<defs>
+<path id="path0_fill" d="M 5.62592 17.9276C 5.62592 23.0737 5.23342 24.7539 4.18673 25.9942C 3.04416 27.0501 1.54971 27.6341 0 27.6304L 0.392506 30.6916C 2.79452 30.7249 5.12302 29.8564 6.92556 28.255C 8.80086 26.3021 9.45504 23.6015 9.45504 19.4583L 9.45504 0L 5.6172 0L 5.6172 17.954L 5.62592 17.9276Z"/>
+<path id="path1_fill" d="M 17.7413 15.6229C 17.7413 17.8397 17.7413 19.7925 17.9157 21.4727L 14.5576 21.4727L 14.3396 17.954L 14.2523 17.954C 13.5454 19.1838 12.5262 20.2013 11.2997 20.9017C 10.0732 21.6022 8.68377 21.9602 7.27445 21.9389C 3.95995 21.9389 0 20.074 0 12.5441L 0 0L 3.83784 0L 3.83784 11.9019C 3.83784 15.9836 5.05897 18.7281 8.53919 18.7281C 9.63092 18.708 10.6925 18.3634 11.5908 17.7374C 12.4892 17.1115 13.1844 16.2321 13.5894 15.2095C 13.8222 14.57 13.9404 13.8938 13.9383 13.2126L 13.9383 0.0175934L 17.7762 0.0175934L 17.7762 15.6229L 17.7413 15.6229Z"/>
+<path id="path2_fill" d="M 0.174447 7.53632C 0.174447 4.79175 0.0872236 2.57499 0 0.498968L 3.44533 0.498968L 3.61978 4.17598L 3.707 4.17598C 4.46074 2.85853 5.55705 1.77379 6.87754 1.03893C 8.19802 0.304077 9.69248 -0.0529711 11.1995 0.0063534C 16.2934 0.0063534 20.1312 4.4047 20.1312 10.9142C 20.1312 18.6289 15.5171 22.4379 10.5366 22.4379C 9.25812 22.492 7.98766 22.2098 6.84994 21.6192C 5.71222 21.0285 4.74636 20.1496 4.04718 19.0688L 3.95995 19.0688L 3.95995 30.7244L 0.165725 30.7244L 0.165725 7.50113L 0.174447 7.53632ZM 3.96868 13.2542C 3.97869 13.7891 4.03708 14.3221 4.14312 14.8464C 4.45574 16.1467 5.19222 17.3035 6.23449 18.1313C 7.27677 18.9592 8.56446 19.4101 9.89116 19.4118C 13.9471 19.4118 16.2934 16.0427 16.2934 11.1254C 16.2934 6.82378 14.0692 3.14677 10.022 3.14677C 8.66089 3.18518 7.35158 3.68134 6.30219 4.55638C 5.25279 5.43143 4.52354 6.63513 4.23035 7.97615C 4.07662 8.49357 3.98869 9.02858 3.96868 9.56835L 3.96868 13.2454L 3.96868 13.2542Z"/>
+<path id="path3_fill" d="M 4.16057 0L 8.7747 12.676C 9.25443 14.0923 9.77777 15.7813 10.1267 17.0744L 10.2139 17.0744C 10.6064 15.7901 11.0425 14.1451 11.5659 12.5969L 15.7526 0.00879669L 19.8085 0.00879669L 14.0604 15.3062C 11.3129 22.6603 9.44632 26.434 6.82961 28.7388C 5.50738 29.9791 3.88672 30.8494 2.12826 31.2634L 1.1688 27.9823C 2.39912 27.5689 3.53918 26.9208 4.52691 26.0734C 5.92259 24.8972 7.02752 23.4094 7.75418 21.7278C 7.90932 21.4374 8.01266 21.1218 8.05946 20.7954C 8.02501 20.4436 7.93674 20.0994 7.79779 19.7749L 0 0.00879669L 4.18673 0.00879669L 4.16057 0Z"/>
+<path id="path4_fill" d="M 7.0215 0L 7.0215 6.15768L 12.5079 6.15768L 12.5079 9.13096L 7.0215 9.13096L 7.0215 20.6898C 7.0215 23.3288 7.7629 24.8594 9.89988 24.8594C 10.6496 24.8712 11.3975 24.7824 12.1241 24.5955L 12.2985 27.5248C 11.207 27.9125 10.0534 28.0915 8.89681 28.0526C 8.1298 28.1 7.36177 27.9782 6.64622 27.6956C 5.93068 27.413 5.28484 26.9765 4.75369 26.4164C 3.66339 25.2641 3.27089 23.3552 3.27089 20.8306L 3.27089 9.13096L 0 9.13096L 0 6.15768L 3.27089 6.15768L 3.27089 1.01162L 7.0215 0Z"/>
+<path id="path5_fill" d="M 3.6285 11.9283C 3.71573 17.2063 7.03022 19.3791 10.8593 19.3791C 12.8612 19.4425 14.8527 19.0642 16.6946 18.2707L 17.3488 21.0593C 15.1419 21.994 12.7638 22.4467 10.3709 22.3876C 3.88145 22.3876 0 18.042 0 11.5676C 0 5.09328 3.75062 0 9.89116 0C 16.7731 0 18.6135 6.15768 18.6135 10.1074C 18.607 10.7165 18.5634 11.3246 18.4827 11.9283L 3.65467 11.9283L 3.6285 11.9283ZM 14.8716 9.13976C 14.9152 6.65909 13.8686 2.79735 9.55971 2.79735C 5.67826 2.79735 3.98612 6.43038 3.68084 9.13976L 14.8803 9.13976L 14.8716 9.13976Z"/>
+<path id="path6_fill" d="M 0.174447 7.17854C 0.174447 4.65389 0.130835 2.48111 0 0.484261L 3.35811 0.484261L 3.48894 4.69787L 3.66339 4.69787C 4.62285 1.81256 6.93428 0.00044283 9.49865 0.00044283C 9.8663 -0.0049786 10.233 0.0394012 10.5889 0.132393L 10.5889 3.80941C 10.1593 3.71494 9.72029 3.67067 9.28059 3.67746C 6.57666 3.67746 4.66646 5.76227 4.14312 8.68277C 4.03516 9.28384 3.97681 9.89289 3.96867 10.5037L 3.96867 21.9394L 0.174447 21.9394L 0.174447 7.17854Z"/>
+<path id="path7_fill" d="M 5.89353 2.844C 5.91889 3.43165 5.77085 4.01367 5.46815 4.51645C 5.16545 5.01922 4.72168 5.42015 4.19299 5.66851C 3.6643 5.91688 3.07444 6.00151 2.49805 5.91171C 1.92166 5.8219 1.38463 5.5617 0.954898 5.16401C 0.52517 4.76633 0.222056 4.24903 0.0839037 3.67757C -0.0542483 3.10611 -0.02123 2.50617 0.178781 1.95364C 0.378793 1.4011 0.736809 0.920817 1.20754 0.573538C 1.67826 0.226259 2.24055 0.0275919 2.82326 0.00267229C 3.60389 -0.0307115 4.36573 0.249789 4.94142 0.782551C 5.51711 1.31531 5.85956 2.05676 5.89353 2.844Z"/>
+<path id="path8_fill" d="M 18.2646 7.13411C 10.4145 7.13411 3.55872 4.2576 0 0C 1.32539 3.8204 3.79556 7.13081 7.0686 9.47303C 10.3417 11.8152 14.2557 13.0734 18.269 13.0734C 22.2823 13.0734 26.1963 11.8152 29.4694 9.47303C 32.7424 7.13081 35.2126 3.8204 36.538 0C 32.9705 4.2576 26.1148 7.13411 18.2646 7.13411Z"/>
+<path id="path9_fill" d="M 18.2733 5.93931C 26.1235 5.93931 32.9793 8.81583 36.538 13.0734C 35.2126 9.25303 32.7424 5.94262 29.4694 3.6004C 26.1963 1.25818 22.2823 0 18.269 0C 14.2557 0 10.3417 1.25818 7.0686 3.6004C 3.79556 5.94262 1.32539 9.25303 0 13.0734C 3.56745 8.82463 10.4232 5.93931 18.2733 5.93931Z"/>
+<path id="path10_fill" d="M 7.42789 3.58338C 7.46008 4.3243 7.27355 5.05819 6.89193 5.69213C 6.51031 6.32607 5.95075 6.83156 5.28411 7.1446C 4.61747 7.45763 3.87371 7.56414 3.14702 7.45063C 2.42032 7.33712 1.74336 7.0087 1.20184 6.50695C 0.660328 6.0052 0.27861 5.35268 0.105017 4.63202C -0.0685757 3.91135 -0.0262361 3.15494 0.226675 2.45856C 0.479587 1.76217 0.931697 1.15713 1.52576 0.720033C 2.11983 0.282935 2.82914 0.0334395 3.56389 0.00313344C 4.54667 -0.0374033 5.50529 0.316706 6.22961 0.987835C 6.95393 1.65896 7.38484 2.59235 7.42789 3.58338L 7.42789 3.58338Z"/>
+<path id="path11_fill" d="M 2.27471 4.39629C 1.84363 4.41508 1.41671 4.30445 1.04799 4.07843C 0.679268 3.8524 0.385328 3.52114 0.203371 3.12656C 0.0214136 2.73198 -0.0403798 2.29183 0.0258116 1.86181C 0.0920031 1.4318 0.283204 1.03126 0.575213 0.710883C 0.867222 0.39051 1.24691 0.164708 1.66622 0.0620592C 2.08553 -0.0405897 2.52561 -0.0154714 2.93076 0.134235C 3.33591 0.283941 3.68792 0.551505 3.94222 0.90306C 4.19652 1.25462 4.34169 1.67436 4.35935 2.10916C 4.38299 2.69107 4.17678 3.25869 3.78597 3.68746C 3.39516 4.11624 2.85166 4.37116 2.27471 4.39629L 2.27471 4.39629Z"/>
+</defs>
+</svg>

From e867a7d778be759ec363fbe11b779282621f3cdf Mon Sep 17 00:00:00 2001
From: YuviPanda <yuvipanda@gmail.com>
Date: Sat, 3 Feb 2024 15:10:29 -0800
Subject: [PATCH 08/14] Style everything to be nicer

- Uses floating-ui for a proper popover for the clipboard
- Sort out the menu to look much better
---
 js/index.css                                  | 75 ++++++++++++-------
 js/index.js                                   | 15 ++--
 js/setupTooltip.js                            | 63 ++++++++++++++++
 .../static/index.html                         | 22 ++++--
 package.json                                  |  1 +
 5 files changed, 133 insertions(+), 43 deletions(-)
 create mode 100644 js/setupTooltip.js

diff --git a/js/index.css b/js/index.css
index 366eee7c..cf43fbe0 100644
--- a/js/index.css
+++ b/js/index.css
@@ -14,6 +14,9 @@ html {
   --jupyter-grey: #9e9e9e;
 
   --topbar-height: 32px;
+
+  /* Use Jupyter Brand fonts */
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
 }
 
 body {
@@ -26,11 +29,10 @@ body {
 #top-bar {
   background-color: var(--jupyter-dark-grey);
   color: white;
-  font: 12px Helvetica;
   border-bottom: 1px white;
   display: flex;
   align-items: center;
-  height: var(--topbar-height);
+  padding: 4px 0px;
 }
 
 #logo {
@@ -44,9 +46,26 @@ body {
 #menu {
   display: flex;
   font-weight: bold;
+  margin-left: auto;
+  font-size: 12px;
 }
 
 #menu li {
+  border-right: 1px white solid;
+  padding: 8px 8px;
+}
+
+#menu li:last-child {
+  border-right: 0;
+}
+
+#menu a {
+  color: white;
+  text-decoration: none;
+}
+
+#status-label {
+  font-weight: normal;
 }
 
 #status {
@@ -65,37 +84,37 @@ body {
 }
 
 /* Clipboard */
-#clipboard-area {
-  position: fixed;
-  top: var(--topbar-height);
-  left: 0px;
-  padding: 4px;
-}
-
-#clipboard-button {
-  cursor: pointer;
-}
-
-#clipboard-button img {
-  height: 24px;
-  vertical-align: middle;
-}
-
-#clipboard-button .label {
-  padding: 5px 5px 4px 0px;
-}
-
-#clipboard-area {
-  /* Full screen, minus padding and left and right margins */
+#clipboard-content {
   display: flex;
   flex-direction: column;
-  background-color: white;
-  border-bottom-right-radius: 4px;
-  border: 1px solid var(--jupyter-medium-grey);
+  padding: 4px;
+  gap: 4px;
 }
 
 #clipboard-text {
   min-width: 500px;
   max-width: 100%;
-  border: 1px solid var(--jupyter-medium-grey);
+}
+
+.tooltip-container {
+  display: none;
+  overflow: visible; /* Needed for the arrow to show up */
+  width: max-content;
+  position: absolute;
+  top: 0;
+  left: 0;
+  background: white;
+  color: var(--jupyter-dark-grey);
+  padding: 6px;
+  border-radius: 4px;
+  font-size: 90%;
+  box-shadow: 4px 4px 4px 0px var(--jupyter-grey);
+}
+
+.arrow {
+  position: absolute;
+  background: white;
+  width: 8px;
+  height: 8px;
+  transform: rotate(45deg);
 }
diff --git a/js/index.js b/js/index.js
index cc2e6728..72aac3b5 100644
--- a/js/index.js
+++ b/js/index.js
@@ -9,9 +9,11 @@ import "./index.css";
 // RFB holds the API to connect and communicate with a VNC server
 import RFB from "@novnc/novnc/core/rfb";
 
+import { setupTooltip } from "./setupTooltip";
+
 // When this function is called we have successfully connected to a server
 function connectedToServer() {
-  status("Connected!");
+  status("Connected");
 }
 
 // This function is called when we are disconnected
@@ -50,12 +52,6 @@ rfb.scaleViewport = true;
 rfb.background = "var(--jupyter-medium-dark-grey)";
 
 // Clipboard
-function toggleClipboardPanel() {
-  document.getElementById("clipboard-area").classList.toggle("hidden");
-}
-document
-  .getElementById("clipboard-button")
-  .addEventListener("click", toggleClipboardPanel);
 
 function clipboardReceive(e) {
   document.getElementById("clipboard-text").value = e.detail.text;
@@ -69,3 +65,8 @@ function clipboardSend() {
 document
   .getElementById("clipboard-text")
   .addEventListener("change", clipboardSend);
+
+setupTooltip(
+  document.getElementById("clipboard-button"),
+  document.getElementById("clipboard-container"),
+);
diff --git a/js/setupTooltip.js b/js/setupTooltip.js
new file mode 100644
index 00000000..677af467
--- /dev/null
+++ b/js/setupTooltip.js
@@ -0,0 +1,63 @@
+import { computePosition, flip, shift, offset, arrow } from "@floating-ui/dom";
+
+/**
+ *
+ * @param {Element} button
+ * @param {Element} tooltip
+ */
+export function setupTooltip(button, tooltip) {
+  const arrowElement = document.querySelector(".arrow");
+  function updatePosition() {
+    computePosition(button, tooltip, {
+      placement: "bottom",
+      middleware: [
+        offset(6),
+        flip(),
+        shift({ padding: 5 }),
+        arrow({ element: arrowElement }),
+      ],
+    }).then(({ x, y, placement, middlewareData }) => {
+      Object.assign(tooltip.style, {
+        left: `${x}px`,
+        top: `${y}px`,
+      });
+
+      // Accessing the data
+      const { x: arrowX, y: arrowY } = middlewareData.arrow;
+
+      const staticSide = {
+        top: "bottom",
+        right: "left",
+        bottom: "top",
+        left: "right",
+      }[placement.split("-")[0]];
+
+      Object.assign(arrowElement.style, {
+        left: arrowX != null ? `${arrowX}px` : "",
+        top: arrowY != null ? `${arrowY}px` : "",
+        right: "",
+        bottom: "",
+        [staticSide]: "-4px",
+      });
+    });
+  }
+  function toggleTooltip() {
+    if (tooltip.style.display === "block") {
+      tooltip.style.display = "none";
+    } else {
+      tooltip.style.display = "block";
+    }
+    updatePosition();
+  }
+
+  function hideTooltip() {
+    tooltip.style.display = "";
+  }
+
+  [
+    ["click", toggleTooltip],
+    ["blur", hideTooltip],
+  ].forEach(([event, listener]) => {
+    button.addEventListener(event, listener);
+  });
+}
diff --git a/jupyter_remote_desktop_proxy/static/index.html b/jupyter_remote_desktop_proxy/static/index.html
index aa3fe8ee..24a0cf52 100644
--- a/jupyter_remote_desktop_proxy/static/index.html
+++ b/jupyter_remote_desktop_proxy/static/index.html
@@ -21,10 +21,13 @@
       <a href=".." id="logo">
         <img src="./jupyter-logo.svg" />
       </a>
-      <span id="status">Connecting...</span>
       <ul id="menu">
+        <li>
+          <span id="status-label">Status:</span>
+          <span id="status">Connecting...</span>
+        </li>
         <li id="clipboard-button">
-          <span class="label">Clipboard</span>
+          <a class="label" href="#"> Clipboard </a>
         </li>
       </ul>
     </div>
@@ -33,12 +36,15 @@
     </div>
 
     <!-- Clipboard -->
-    <div id="clipboard-area" class="hidden">
-      <span
-        >Contents of this text box are synced with the clipboard on the remote
-        desktop</span
-      >
-      <textarea id="clipboard-text" rows="5"></textarea>
+    <div id="clipboard-container" class="tooltip-container" popover>
+      <div class="arrow"></div>
+      <div id="clipboard-content">
+        <p>
+          Contents of this text box are synced with the clipboard on the remote
+          desktop
+        </p>
+        <textarea id="clipboard-text" rows="5"></textarea>
+      </div>
     </div>
 
     <script src="./dist/viewer.js"></script>
diff --git a/package.json b/package.json
index 4551affb..00042647 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,6 @@
 {
   "dependencies": {
+    "@floating-ui/dom": "^1.6.1",
     "@novnc/novnc": "^1.4.0",
     "reset-css": "^5.0.2"
   },

From 09f2bc66858d04da2543ff3a10cbdcb52bd95617 Mon Sep 17 00:00:00 2001
From: YuviPanda <yuvipanda@gmail.com>
Date: Sat, 3 Feb 2024 15:16:28 -0800
Subject: [PATCH 09/14] Refactor & document tooltip code

---
 js/index.css                       |  2 +-
 js/index.js                        |  3 +--
 js/{setupTooltip.js => tooltip.js} | 26 +++++++++++---------------
 3 files changed, 13 insertions(+), 18 deletions(-)
 rename js/{setupTooltip.js => tooltip.js} (73%)

diff --git a/js/index.css b/js/index.css
index cf43fbe0..c245810e 100644
--- a/js/index.css
+++ b/js/index.css
@@ -36,7 +36,7 @@ body {
 }
 
 #logo {
-  padding: 0 32px;
+  padding: 0 24px;
 }
 
 #logo img {
diff --git a/js/index.js b/js/index.js
index 72aac3b5..dd96a4e7 100644
--- a/js/index.js
+++ b/js/index.js
@@ -9,7 +9,7 @@ import "./index.css";
 // RFB holds the API to connect and communicate with a VNC server
 import RFB from "@novnc/novnc/core/rfb";
 
-import { setupTooltip } from "./setupTooltip";
+import { setupTooltip } from "./tooltip";
 
 // When this function is called we have successfully connected to a server
 function connectedToServer() {
@@ -52,7 +52,6 @@ rfb.scaleViewport = true;
 rfb.background = "var(--jupyter-medium-dark-grey)";
 
 // Clipboard
-
 function clipboardReceive(e) {
   document.getElementById("clipboard-text").value = e.detail.text;
 }
diff --git a/js/setupTooltip.js b/js/tooltip.js
similarity index 73%
rename from js/setupTooltip.js
rename to js/tooltip.js
index 677af467..b2b779fe 100644
--- a/js/setupTooltip.js
+++ b/js/tooltip.js
@@ -1,14 +1,19 @@
+/**
+ * Setup simplest popover possible to provide popovers.
+ *
+ * Mostly follows https://floating-ui.com/docs/tutorial
+ */
 import { computePosition, flip, shift, offset, arrow } from "@floating-ui/dom";
 
 /**
- *
- * @param {Element} button
+ * Setup trigger element to toggle showing / hiding tooltip element
+ * @param {Element} trigger
  * @param {Element} tooltip
  */
-export function setupTooltip(button, tooltip) {
-  const arrowElement = document.querySelector(".arrow");
+export function setupTooltip(trigger, tooltip) {
+  const arrowElement = tooltip.querySelector(".arrow");
   function updatePosition() {
-    computePosition(button, tooltip, {
+    computePosition(trigger, tooltip, {
       placement: "bottom",
       middleware: [
         offset(6),
@@ -50,14 +55,5 @@ export function setupTooltip(button, tooltip) {
     updatePosition();
   }
 
-  function hideTooltip() {
-    tooltip.style.display = "";
-  }
-
-  [
-    ["click", toggleTooltip],
-    ["blur", hideTooltip],
-  ].forEach(([event, listener]) => {
-    button.addEventListener(event, listener);
-  });
+  trigger.addEventListener("click", toggleTooltip);
 }

From e63436f5283a24f0188e6afb7187bbd14599c068 Mon Sep 17 00:00:00 2001
From: YuviPanda <yuvipanda@gmail.com>
Date: Sat, 3 Feb 2024 15:18:31 -0800
Subject: [PATCH 10/14] Don't navigate to \# when clipboard is clicked

---
 js/tooltip.js | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/js/tooltip.js b/js/tooltip.js
index b2b779fe..c2aec1ad 100644
--- a/js/tooltip.js
+++ b/js/tooltip.js
@@ -46,14 +46,14 @@ export function setupTooltip(trigger, tooltip) {
       });
     });
   }
-  function toggleTooltip() {
+
+  trigger.addEventListener("click", (e) => {
     if (tooltip.style.display === "block") {
       tooltip.style.display = "none";
     } else {
       tooltip.style.display = "block";
     }
     updatePosition();
-  }
-
-  trigger.addEventListener("click", toggleTooltip);
+    e.preventDefault();
+  });
 }

From ee0d216626c38cc8e7f6fd21c9d70b73689220f2 Mon Sep 17 00:00:00 2001
From: YuviPanda <yuvipanda@gmail.com>
Date: Sat, 3 Feb 2024 15:29:15 -0800
Subject: [PATCH 11/14] Remove stray box-shadow

---
 js/index.css | 1 -
 1 file changed, 1 deletion(-)

diff --git a/js/index.css b/js/index.css
index c245810e..9eeb0c78 100644
--- a/js/index.css
+++ b/js/index.css
@@ -108,7 +108,6 @@ body {
   padding: 6px;
   border-radius: 4px;
   font-size: 90%;
-  box-shadow: 4px 4px 4px 0px var(--jupyter-grey);
 }
 
 .arrow {

From c30d0a1078bf5d06c2381289c6baf426d6f01500 Mon Sep 17 00:00:00 2001
From: YuviPanda <yuvipanda@gmail.com>
Date: Sat, 3 Feb 2024 16:21:13 -0800
Subject: [PATCH 12/14] Cleanup the menu UI better

- Has a hover state and an active state
---
 js/index.css                                  | 21 +++++++++++--------
 js/tooltip.js                                 |  7 ++-----
 .../static/index.html                         |  8 +++----
 3 files changed, 18 insertions(+), 18 deletions(-)

diff --git a/js/index.css b/js/index.css
index 9eeb0c78..a01c6726 100644
--- a/js/index.css
+++ b/js/index.css
@@ -32,7 +32,6 @@ body {
   border-bottom: 1px white;
   display: flex;
   align-items: center;
-  padding: 4px 0px;
 }
 
 #logo {
@@ -51,8 +50,8 @@ body {
 }
 
 #menu li {
-  border-right: 1px white solid;
-  padding: 8px 8px;
+  border-right: 1px var(--jupyter-grey) solid;
+  padding: 12px 0px;
 }
 
 #menu li:last-child {
@@ -62,15 +61,20 @@ body {
 #menu a {
   color: white;
   text-decoration: none;
+  padding: 12px 8px;
 }
 
-#status-label {
-  font-weight: normal;
+#menu a:hover,
+#menu a.active {
+  background-color: var(--jupyter-medium-grey);
+}
+
+li#status-container {
+  padding-right: 8px;
 }
 
-#status {
-  text-align: center;
-  flex-grow: 1;
+#status-label {
+  font-weight: normal;
 }
 
 #screen {
@@ -97,7 +101,6 @@ body {
 }
 
 .tooltip-container {
-  display: none;
   overflow: visible; /* Needed for the arrow to show up */
   width: max-content;
   position: absolute;
diff --git a/js/tooltip.js b/js/tooltip.js
index c2aec1ad..b4053adb 100644
--- a/js/tooltip.js
+++ b/js/tooltip.js
@@ -48,11 +48,8 @@ export function setupTooltip(trigger, tooltip) {
   }
 
   trigger.addEventListener("click", (e) => {
-    if (tooltip.style.display === "block") {
-      tooltip.style.display = "none";
-    } else {
-      tooltip.style.display = "block";
-    }
+    tooltip.classList.toggle("hidden");
+    trigger.classList.toggle("active");
     updatePosition();
     e.preventDefault();
   });
diff --git a/jupyter_remote_desktop_proxy/static/index.html b/jupyter_remote_desktop_proxy/static/index.html
index 24a0cf52..688f02a7 100644
--- a/jupyter_remote_desktop_proxy/static/index.html
+++ b/jupyter_remote_desktop_proxy/static/index.html
@@ -22,12 +22,12 @@
         <img src="./jupyter-logo.svg" />
       </a>
       <ul id="menu">
-        <li>
+        <li id="status-container">
           <span id="status-label">Status:</span>
           <span id="status">Connecting...</span>
         </li>
-        <li id="clipboard-button">
-          <a class="label" href="#"> Clipboard </a>
+        <li>
+          <a id="clipboard-button" href="#">Remote Clipboard</a>
         </li>
       </ul>
     </div>
@@ -36,7 +36,7 @@
     </div>
 
     <!-- Clipboard -->
-    <div id="clipboard-container" class="tooltip-container" popover>
+    <div id="clipboard-container" class="tooltip-container hidden">
       <div class="arrow"></div>
       <div id="clipboard-content">
         <p>

From a0184355fe5739b9f6250ed4ab85229b0cd54e6e Mon Sep 17 00:00:00 2001
From: YuviPanda <yuvipanda@gmail.com>
Date: Sat, 3 Feb 2024 16:28:39 -0800
Subject: [PATCH 13/14] Split tooltip CSS into its own file

---
 js/index.css   | 25 -------------------------
 js/index.js    |  2 +-
 js/tooltip.css | 24 ++++++++++++++++++++++++
 js/tooltip.js  |  1 +
 4 files changed, 26 insertions(+), 26 deletions(-)
 create mode 100644 js/tooltip.css

diff --git a/js/index.css b/js/index.css
index a01c6726..3f08653c 100644
--- a/js/index.css
+++ b/js/index.css
@@ -83,10 +83,6 @@ li#status-container {
   overflow: hidden;
 }
 
-.hidden {
-  display: none !important;
-}
-
 /* Clipboard */
 #clipboard-content {
   display: flex;
@@ -99,24 +95,3 @@ li#status-container {
   min-width: 500px;
   max-width: 100%;
 }
-
-.tooltip-container {
-  overflow: visible; /* Needed for the arrow to show up */
-  width: max-content;
-  position: absolute;
-  top: 0;
-  left: 0;
-  background: white;
-  color: var(--jupyter-dark-grey);
-  padding: 6px;
-  border-radius: 4px;
-  font-size: 90%;
-}
-
-.arrow {
-  position: absolute;
-  background: white;
-  width: 8px;
-  height: 8px;
-  transform: rotate(45deg);
-}
diff --git a/js/index.js b/js/index.js
index dd96a4e7..b5bf5d95 100644
--- a/js/index.js
+++ b/js/index.js
@@ -9,7 +9,7 @@ import "./index.css";
 // RFB holds the API to connect and communicate with a VNC server
 import RFB from "@novnc/novnc/core/rfb";
 
-import { setupTooltip } from "./tooltip";
+import { setupTooltip } from "./tooltip.js";
 
 // When this function is called we have successfully connected to a server
 function connectedToServer() {
diff --git a/js/tooltip.css b/js/tooltip.css
new file mode 100644
index 00000000..dfd09771
--- /dev/null
+++ b/js/tooltip.css
@@ -0,0 +1,24 @@
+.hidden {
+  display: none !important;
+}
+
+.tooltip-container {
+  overflow: visible; /* Needed for the arrow to show up */
+  width: max-content;
+  position: absolute;
+  top: 0;
+  left: 0;
+  background: white;
+  color: var(--jupyter-dark-grey);
+  padding: 6px;
+  border-radius: 4px;
+  font-size: 90%;
+}
+
+.arrow {
+  position: absolute;
+  background: white;
+  width: 8px;
+  height: 8px;
+  transform: rotate(45deg);
+}
diff --git a/js/tooltip.js b/js/tooltip.js
index b4053adb..20f89de1 100644
--- a/js/tooltip.js
+++ b/js/tooltip.js
@@ -4,6 +4,7 @@
  * Mostly follows https://floating-ui.com/docs/tutorial
  */
 import { computePosition, flip, shift, offset, arrow } from "@floating-ui/dom";
+import "./tooltip.css";
 
 /**
  * Setup trigger element to toggle showing / hiding tooltip element

From b1a264dcff79523dd74977964455f62385a2276a Mon Sep 17 00:00:00 2001
From: YuviPanda <yuvipanda@gmail.com>
Date: Sat, 3 Feb 2024 20:05:32 -0800
Subject: [PATCH 14/14] Fix value for protocol check

---
 js/index.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/js/index.js b/js/index.js
index b5bf5d95..1c054a29 100644
--- a/js/index.js
+++ b/js/index.js
@@ -32,7 +32,7 @@ function status(text) {
 
 // Construct the websockify websocket URL we want to connect to
 let websockifyUrl = new URL("websockify", window.location);
-websockifyUrl.protocol = window.location.protocol === "https" ? "wss" : "ws";
+websockifyUrl.protocol = window.location.protocol === "https:" ? "wss" : "ws";
 
 // Creating a new RFB object will start a new connection
 const rfb = new RFB(