summaryrefslogtreecommitdiff
path: root/public
diff options
context:
space:
mode:
Diffstat (limited to 'public')
-rw-r--r--public/shell.js73
-rw-r--r--public/styles/main.css13
-rw-r--r--public/styles/style.css48
3 files changed, 134 insertions, 0 deletions
diff --git a/public/shell.js b/public/shell.js
new file mode 100644
index 0000000..9a70846
--- /dev/null
+++ b/public/shell.js
@@ -0,0 +1,73 @@
+const shell = document.querySelector("[data-shell]");
+
+if (shell) {
+ const area = shell.closest("[data-scroll-area]");
+ const history = shell.querySelector("#shell-history");
+ const input = shell.querySelector("[data-shell-input]");
+ const caret = shell.querySelector("[data-shell-caret]");
+ let busy = false;
+
+ const updateCaret = () => {
+ const pos = input.selectionStart ?? input.value.length;
+ caret.style.left = `calc(${pos}ch - ${input.scrollLeft}px)`;
+ caret.textContent = input.value[pos] ?? "\u00a0";
+ caret.style.animation = "none"; // restart the blink so the caret is solid while typing
+ void caret.offsetWidth;
+ caret.style.animation = "";
+ };
+
+ const snapToBottom = () => {
+ area.scrollTop = area.scrollHeight;
+ };
+
+ const run = async (cmd) => {
+ busy = true;
+ try {
+ const res = await fetch("/shell/run", {
+ method: "POST",
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
+ body: new URLSearchParams({ cmd }),
+ });
+ const data = await res.json();
+ if (data.clear) history.replaceChildren();
+ if (data.html) history.insertAdjacentHTML("beforeend", data.html);
+ if (data.vars) {
+ for (const [key, value] of Object.entries(data.vars)) {
+ document.documentElement.style.setProperty(`--${key}`, value);
+ }
+ }
+ } catch {
+ const el = document.createElement("p");
+ el.textContent = "request failed";
+ history.append(el);
+ } finally {
+ busy = false;
+ snapToBottom();
+ }
+ };
+
+ input.addEventListener("keydown", (e) => {
+ if (e.key !== "Enter") return;
+ e.preventDefault();
+ const cmd = input.value.trim();
+ if (!cmd || busy) return;
+ input.value = "";
+ updateCaret();
+ run(cmd);
+ });
+
+ for (const event of ["input", "keyup", "click", "focus"]) {
+ input.addEventListener(event, updateCaret);
+ }
+ document.addEventListener("selectionchange", () => {
+ if (document.activeElement === input) updateCaret();
+ });
+
+ area.addEventListener("click", () => {
+ if (!getSelection().toString()) input.focus({ preventScroll: true });
+ });
+
+ input.focus({ preventScroll: true });
+ updateCaret();
+ snapToBottom();
+}
diff --git a/public/styles/main.css b/public/styles/main.css
index 41d7fa2..f7539ee 100644
--- a/public/styles/main.css
+++ b/public/styles/main.css
@@ -261,3 +261,16 @@ body {
transform: scale(1.03);
}
}
+
+@keyframes blink {
+ 50% {
+ opacity: 0;
+ }
+}
+
+[data-shell-caret] {
+ animation: blink 1.1s steps(1) infinite;
+}
+[data-shell-prompt]:not(:focus-within) [data-shell-caret] {
+ visibility: hidden;
+}
diff --git a/public/styles/style.css b/public/styles/style.css
index 872086f..5c4e34e 100644
--- a/public/styles/style.css
+++ b/public/styles/style.css
@@ -820,6 +820,9 @@
}
}
@layer utilities {
+ .pointer-events-none {
+ pointer-events: none;
+ }
.invisible {
visibility: hidden;
}
@@ -841,6 +844,9 @@
.inset-x-\[0\.3rem\] {
inset-inline: 0.3rem;
}
+ .inset-y-0 {
+ inset-block: 0px;
+ }
.top-1\/2 {
top: calc(1 / 2 * 100%);
}
@@ -1451,6 +1457,9 @@
.w-8 {
width: calc(var(--spacing) * 8);
}
+ .w-\[1ch\] {
+ width: 1ch;
+ }
.w-\[30\%\] {
width: 30%;
}
@@ -1566,6 +1575,9 @@
.gap-\[1ch\] {
gap: 1ch;
}
+ .gap-\[1rem\] {
+ gap: 1rem;
+ }
.gap-x-4 {
column-gap: calc(var(--spacing) * 4);
}
@@ -1708,6 +1720,9 @@
.bg-\(--secondary\) {
background-color: var(--secondary);
}
+ .bg-\(--text\) {
+ background-color: var(--text);
+ }
.bg-\[var\(--background\)\] {
background-color: var(--background);
}
@@ -1726,6 +1741,9 @@
background-color: color-mix(in oklab, var(--color-black) 30%, transparent);
}
}
+ .bg-transparent {
+ background-color: transparent;
+ }
.bg-white\/2 {
background-color: color-mix(in srgb, #fff 2%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -1942,6 +1960,9 @@
.whitespace-nowrap {
white-space: nowrap;
}
+ .whitespace-pre {
+ white-space: pre;
+ }
.text-\(--accent\) {
color: var(--accent);
}
@@ -2020,6 +2041,9 @@
.text-\[\#a34eeb\] {
color: #a34eeb;
}
+ .text-\[\#ffffff\] {
+ color: #ffffff;
+ }
.text-\[\%v\] {
color: %v;
}
@@ -2035,6 +2059,9 @@
.no-underline {
text-decoration-line: none;
}
+ .caret-transparent {
+ caret-color: transparent;
+ }
.opacity-0 {
opacity: 0%;
}
@@ -2054,6 +2081,10 @@
--tw-duration: 150ms;
transition-duration: 150ms;
}
+ .outline-none {
+ --tw-outline-style: none;
+ outline-style: none;
+ }
.select-text {
-webkit-user-select: text;
user-select: text;
@@ -2099,6 +2130,12 @@
background-color: color-mix(in oklab, var(--primary) 25%, transparent) !important;
}
}
+ .group-data-\[copied\]\/copy\:block:is(:where(.group\/copy)[data-copied] *) {
+ display: block;
+ }
+ .group-data-\[copied\]\/copy\:hidden:is(:where(.group\/copy)[data-copied] *) {
+ display: none;
+ }
.after\:absolute::after {
content: var(--tw-content);
position: absolute;
@@ -2515,6 +2552,17 @@ body {
transform: scale(1.03);
}
}
+@keyframes blink {
+ 50% {
+ opacity: 0;
+ }
+}
+[data-shell-caret] {
+ animation: blink 1.1s steps(1) infinite;
+}
+[data-shell-prompt]:not(:focus-within) [data-shell-caret] {
+ visibility: hidden;
+}
@property --tw-translate-x {
syntax: "*";
inherits: false;