diff --git a/content/treehouse/cmd.tree b/content/treehouse/cmd.tree new file mode 100644 index 0000000..e214de9 --- /dev/null +++ b/content/treehouse/cmd.tree @@ -0,0 +1,20 @@ +%% title = "command line" + +% id = "01JEK4XKK26T6W603FTPQHQ7C8" +- press `:`{=html} to open the command line. + + % id = "01JEK4XKK27KXP01EK8K890SPK" + - type in your command, then press `Enter`{=html} to run it. + + - `Esc`{=html} closes the command line. + + - `Tab`{=html} cycles through suggestions. + + - you may also use the mouse to close the command line or pick a suggestion from the list. + +% id = "01JEK4XKK2EDTVCNZQRV9XDZXJ" +- unknown commands do not do anything. +known commands usually result in immediate feedback. + +% id = "01JEK4XKK2S4W0TPT4JY8AH143" +- the command line is currently not accessible on mobile devices. diff --git a/content/treehouse/dev/tools.tree b/content/treehouse/dev/tools.tree index c3624e5..df9e41a 100644 --- a/content/treehouse/dev/tools.tree +++ b/content/treehouse/dev/tools.tree @@ -1,5 +1,5 @@ %% title = "developer tools" -styles = ["page/treehouse/dev/tools.css"] +styles = ["dev.css"] scripts = ["treehouse/dev/picture-upload.js"] % id = "01JEHDJSJP282VCTRKYHNFM4N7" diff --git a/static/css/page/treehouse/dev/tools.css b/static/css/dev.css similarity index 74% rename from static/css/page/treehouse/dev/tools.css rename to static/css/dev.css index 8c70868..addcde9 100644 --- a/static/css/page/treehouse/dev/tools.css +++ b/static/css/dev.css @@ -1,18 +1,14 @@ +/* Styles for developer tools. + This stylesheet MUST NOT be used for modifying the appearance of elements globally. + If you notice that it is for whatever reason, please bonk liquidex on the head. */ + th-picture-upload { display: block; - border: 1px solid var(--border-1); - padding: 8px 12px; - margin-right: 8px; - border-radius: 8px; - cursor: default; - &:focus { - border-color: var(--liquidex-brand-blue); - } - & > .nothing-pasted { + border: 1px solid var(--border-1); text-align: center; opacity: 50%; padding: 16px; diff --git a/static/css/main.css b/static/css/main.css index 648c50c..e0d9c23 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -108,6 +108,11 @@ body { scrollbar-gutter: stable; } +:focus-visible { + outline: 1px solid var(--liquidex-brand-blue); + outline-offset: 2px; +} + /* Set up typography */ @font-face { @@ -131,8 +136,11 @@ pre, code, kbd, button, -select { +select, +input, +dfn { font-family: "RecVar", sans-serif; + font-style: normal; line-height: 1.5; } @@ -144,8 +152,9 @@ pre, code, kbd, button, -select { - font-size: 100%; +select, +input { + font-size: inherit; } :root { @@ -499,28 +508,7 @@ h1.page-title { } } -/* Style the `new` link on the homepage */ -a[data-cast~="new"] { - flex-shrink: 0; - color: var(--text-color); - opacity: 50%; - - &.has-news { - opacity: 100%; - text-decoration: none; - - & .new-text { - text-decoration: underline; - } - } - - & .badge { - margin-left: var(--8px); - text-decoration: none; - } -} - -/* Style new badges */ +/* Style badges */ span.badge { --recursive-wght: 800; --recursive-mono: 1; @@ -641,6 +629,20 @@ footer { } } +/* Style dialogues */ + +dialog[open] { + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + + color: var(--text-color); + background-color: var(--background-color); + border: 1px solid var(--border-1); + border-radius: 12px; +} + /* Style emojis to be readable */ img[data-cast~="emoji"] { @@ -702,35 +704,78 @@ th-emoji-tooltip p { cursor: help; } -/* Funny joke */ +/* Command line */ -@keyframes hello-there { - 0% { - opacity: 0%; - } +th-command-line { + --recursive-mono: 1; + --recursive-casl: 0; - 70% { - opacity: 0%; - } - - 100% { - opacity: 70%; - } -} - -.oops-you-seem-to-have-gotten-stuck { - margin-top: 16px; display: none; - position: absolute; - opacity: 0%; -} + flex-direction: column; -#index\:treehouse - > details:not([open]) - > summary - .oops-you-seem-to-have-gotten-stuck { - display: inline; - animation: 4s hello-there forwards; + background-color: var(--background-color-tooltip); + font-size: 87.5%; + + &.visible { + display: flex; + position: fixed; + left: 0; + bottom: 0; + width: 100%; + } + + & > .input-wrapper { + display: flex; + flex-direction: row; + + padding: 2px 4px; + width: 100%; + + &::before { + content: ":"; + padding-right: 2px; + opacity: 50%; + } + + & > input { + background: none; + color: var(--text-color); + border: none; + flex-grow: 1; + + &:focus { + outline: none; + } + } + } + + & > ul.suggestions { + list-style: none; + + display: flex; + flex-direction: column; + + margin: 0; + padding: 0; + + & > li { + padding: 2px 8px; + + cursor: default; + + & > dfn { + --recursive-crsv: 0; + --recursive-wght: 700; + margin-right: 2ch; + } + + &:hover, + &.tabbed { + background-color: var(--liquidex-brand-blue); + color: white; + } + } + } } /* Literate programming support */ diff --git a/static/js/command-line.js b/static/js/command-line.js new file mode 100644 index 0000000..b509688 --- /dev/null +++ b/static/js/command-line.js @@ -0,0 +1,174 @@ +export class CommandLine extends HTMLElement { + static commands = new Map(); + + constructor() { + super(); + } + + connectedCallback() { + this.suggestions = this.appendChild(document.createElement("ul")); + this.suggestions.classList.add("suggestions"); + + let inputWrapper = this.appendChild(document.createElement("div")); + inputWrapper.classList.add("input-wrapper"); + this.input = inputWrapper.appendChild(document.createElement("input")); + this.input.type = "text"; + + window.addEventListener("keydown", (event) => { + if (event.key == ":" && !this.visible) { + event.stopPropagation(); + event.preventDefault(); + this.show(); + } + + if (event.key == "Escape") { + this.hide(); + } + }); + + window.addEventListener("click", () => { + this.hide(); + }); + + this.addEventListener("click", (event) => { + event.stopPropagation(); + }); + + this.input.addEventListener("keydown", (event) => { + if (event.key == "Enter") { + event.preventDefault(); + this.hide(); + this.runCommand(this.input.value); + } + + if (event.key == "Tab") { + event.preventDefault(); + if (event.shiftKey) this.tabToPreviousSuggestion(); + else this.tabToNextSuggestion(); + } + }); + + this.input.addEventListener("input", () => { + this.updateSuggestions(); + }); + } + + get visible() { + return this.classList.contains("visible"); + } + + show() { + this.classList.add("visible"); + this.input.focus(); + this.input.value = ""; + this.updateSuggestions(); + } + + hide() { + this.classList.remove("visible"); + } + + tab(current, next) { + current?.classList?.remove("tabbed"); + next.classList.add("tabbed"); + + this.input.value = next.name; + // NOTE: Do NOT update suggestions here. + // This would cause the tabbing to break. + } + + tabToNextSuggestion() { + let current = this.suggestions.querySelector(".tabbed"); + let next = current?.nextSibling ?? this.suggestions.childNodes[0]; + this.tab(current, next); + } + + tabToPreviousSuggestion() { + let current = this.suggestions.querySelector(".tabbed"); + let previous = + current?.previousSibling ?? + this.suggestions.childNodes[this.suggestions.childNodes.length - 1]; + this.tab(current, previous); + } + + updateSuggestions() { + let search = parseCommand(this.input.value)?.command ?? ""; + let suggestions = Array.from(CommandLine.commands.entries()).filter( + ([name, def]) => !def.isAlias && fuzzyMatch(search, name), + ); + suggestions.sort(); + + this.suggestions.replaceChildren(); + for (let [name, def] of suggestions) { + let suggestion = this.suggestions.appendChild(document.createElement("li")); + let commandName = suggestion.appendChild(document.createElement("dfn")); + commandName.textContent = name; + let commandDescription = suggestion.appendChild(document.createElement("span")); + commandDescription.classList.add("description"); + commandDescription.textContent = def.description; + + suggestion.name = name; + suggestion.def = def; + + suggestion.addEventListener("click", () => { + this.input.value = name; + this.updateSuggestions(); + this.input.focus(); + }); + } + } + + runCommand(commandLine) { + let { command, args } = parseCommand(commandLine); + let commandDef = CommandLine.commands.get(command); + if (CommandLine.commands.has(command)) { + commandDef.run(args); + } else { + console.log(`unknown command`); + } + } + + static registerCommand({ aliases, description, run }) { + for (let i = 0; i < aliases.length; ++i) { + CommandLine.commands.set(aliases[i], { + isAlias: i != 0, + description, + run, + }); + } + } +} + +customElements.define("th-command-line", CommandLine); + +function parseCommand(commandLine) { + let result = /^([^ ]+) *(.*)$/.exec(commandLine); + if (result == null) return null; + + let [_, command, args] = result; + return { command, args }; +} + +// https://www.forrestthewoods.com/blog/reverse_engineering_sublime_texts_fuzzy_match/ +function fuzzyMatch(pattern, string) { + let iPattern = 0; + let iString = 0; + + while (iPattern < pattern.length && iString < string.length) { + if (pattern.charAt(iPattern).toLowerCase() == string.charAt(iString).toLowerCase()) { + iPattern += 1; + } + + iString += 1; + } + + return iPattern == pattern.length; +} + +CommandLine.registerCommand({ + aliases: ["help", "h"], + description: '"OwO, what is this?"', + run() { + window.location = `${TREEHOUSE_SITE}/treehouse/cmd`; + }, +}); diff --git a/static/js/live-reload.js b/static/js/dev/live-reload.js similarity index 100% rename from static/js/live-reload.js rename to static/js/dev/live-reload.js diff --git a/static/js/dev/picture-upload.js b/static/js/dev/picture-upload.js index 179b65f..6791eeb 100644 --- a/static/js/dev/picture-upload.js +++ b/static/js/dev/picture-upload.js @@ -1,10 +1,11 @@ +import { CommandLine } from "treehouse/command-line.js"; + class PictureUpload extends HTMLElement { constructor() { super(); } connectedCallback() { - this.tabIndex = 0; this.gotoInit(); this.preview = this.querySelector("img[name='preview']"); @@ -30,16 +31,17 @@ class PictureUpload extends HTMLElement { gotoInit() { this.setState("init"); this.innerHTML = ` -