diff --git a/builder/page.ts b/builder/page.ts index 7080601..ff5dffd 100644 --- a/builder/page.ts +++ b/builder/page.ts @@ -80,12 +80,14 @@ export async function CreateFolderPage(toolbar: string, path: string) { function RenderPage(path: string, data: string) { - const pathFrag = path.split("/"); + const pathFrag = path.split("/").slice(2); const { summary, details } = IngestPage(data); const html = `
` - + `${pathFrag.slice(2, -1).join("/")}` - + `🔗` + + `${pathFrag.slice(0, -1).join("/")}` + + `🔗` + + `📂` + + `
` + `
Close
` + `
` + `
${summary.text}
` @@ -97,7 +99,7 @@ function RenderPage(path: string, data: string) { + summary.params.map(p => `
` +`${p.name}` +`: ${p.type}` - + `${p.description}` + + `${p.description}` +`
`).join("") + `` + (summary.type == "function" @@ -105,7 +107,7 @@ function RenderPage(path: string, data: string) { + `
${summary.returns.map(p => `
` +`${p.name}` +`: ${p.type}` - + ` ${p.description}` + + ` ${p.description}` +`
`).join("")}
` ) : "}") + `` diff --git a/client/index.ts b/client/index.ts index c24d604..0165a6d 100644 --- a/client/index.ts +++ b/client/index.ts @@ -28,7 +28,7 @@ function AnyClick(ev: MouseEvent) { } } -async function OpenEntry(href: string, caller?: HTMLElement) { +async function OpenEntry(href: string, caller?: HTMLElement, pushEnd: boolean = false) { const stash = document.querySelector(".stash"); if (!stash) throw new Error("Missing stash element"); @@ -43,10 +43,12 @@ async function OpenEntry(href: string, caller?: HTMLElement) { if (!entry) throw new Error("Route is missing div.entry"); if (!existing && caller) caller.style.setProperty('view-transition-name', href.replaceAll("/", "_")); - await TransitionStart(); + if (!pushEnd) await TransitionStart(); if (existing) existing.remove(); if (caller) caller.style.removeProperty('view-transition-name'); - stash.insertBefore(entry, stash.firstChild); + + if (pushEnd) stash.appendChild(entry); + else stash.insertBefore(entry, stash.firstChild); stash.scrollTo({top: 0}); const title = doc.querySelector("title")?.innerText || document.title; @@ -55,6 +57,8 @@ async function OpenEntry(href: string, caller?: HTMLElement) { Save(); } +(window as any).OpenEntry = OpenEntry; + function FindOpenEntry(href: string) { for (const div of document.body.querySelectorAll(".entry")) { @@ -89,8 +93,7 @@ async function OpenFolder(href: string) { function Save() { const pages = [ ...document.body.querySelectorAll(".entry") ] .map(x => x.getAttribute("data-src")) - .filter(x => x) - .reverse(); + .filter(x => x); localStorage.setItem("open-pages", pages.join("\n")); } @@ -137,7 +140,7 @@ async function Startup() { const pages = (localStorage.getItem('open-pages') || "").split("\n"); for (const page of pages) { - await OpenEntry(page); + await OpenEntry(page, undefined, true); } Search.Bind(); diff --git a/client/search.ts b/client/search.ts index 2cc38a5..733a0d8 100644 --- a/client/search.ts +++ b/client/search.ts @@ -5,6 +5,7 @@ function Focus(ev: FocusEvent) { ev.target.value = ""; } +let resultsElm: HTMLDivElement; let searchElm: HTMLInputElement; let loading = false; let timer: NodeJS.Timeout; @@ -19,7 +20,7 @@ async function PreloadIndex() { if (!req.ok) throw new Error(req.statusText); const json = await req.json(); - console.log("Loaded search index", json); + console.info("Loaded search index", json); index = lunr(function () { this.ref('href'); @@ -37,6 +38,25 @@ async function PreloadIndex() { function Keypress(ev: KeyboardEvent) { if (!(ev.target instanceof HTMLInputElement)) return; + if (ev.key === "Enter") { + ev.stopImmediatePropagation(); + ev.stopPropagation(); + ev.preventDefault(); + OpenSelection(); + return; + } + + let move = 0; + if (ev.key === "ArrowDown") move = -1; + if (ev.key === "ArrowUp") move = 1; + if (move !== 0) { + ev.stopImmediatePropagation(); + ev.stopPropagation(); + ev.preventDefault(); + MoveSelection(move); + return; + } + PreloadIndex(); if (timer) clearTimeout(timer); @@ -50,11 +70,9 @@ function Search() { }; const res = index.search(searchElm.value+"*"); - console.log(res); - const results = document.querySelector("#search .results"); - if (!results) return; - results.innerHTML = ""; + if (!resultsElm) return; + resultsElm.innerHTML = ""; for (const opt of res) { const elm = document.createElement("a"); @@ -68,14 +86,37 @@ function Search() { ctx.className = "comment"; elm.appendChild(ctx); - results.appendChild(elm); + resultsElm.appendChild(elm); } } +function MoveSelection(delta: number) { + const i = [...resultsElm.children].findIndex(v => v.hasAttribute("selected")); + if (0 <= i) resultsElm.children[i].removeAttribute("selected"); + + const j = Math.min(Math.max(0, i-delta), resultsElm.children.length-1); + resultsElm.children[j].setAttribute("selected", "true"); +} + +function OpenSelection() { + const i = Math.max(0, [...resultsElm.children].findIndex(v => v.hasAttribute("selected"))); + + const item = resultsElm.children[i]; + if (!item) return; + + item.removeAttribute("selected"); + (window as any).OpenEntry(item.getAttribute("href") || "", item); + searchElm.blur(); +} + export function Bind() { - const elm = document.getElementById("search-input"); - if (!(elm instanceof HTMLInputElement)) throw new Error("Missing search box"); - searchElm = elm; + const a = document.querySelector("#search .results"); + if (!(a instanceof HTMLDivElement)) throw new Error("Missing search results div"); + resultsElm = a; + + const b = document.getElementById("search-input"); + if (!(b instanceof HTMLInputElement)) throw new Error("Missing search box"); + searchElm = b; searchElm.addEventListener("keyup", Keypress); searchElm.addEventListener("focus", Focus); diff --git a/public/main.css b/public/main.css index c615a08..59f88bb 100644 --- a/public/main.css +++ b/public/main.css @@ -10,9 +10,22 @@ body { min-height: 100vh; font-size: 0.8rem; - background-color: #272822; + background-color: var(--bg); font-family: Fira Code; - color: #f8f8f2; + color: var(--fg); + + --bg: #272822; + --bg_h: #3e3d32; + --bg_c: #75715e; + --fg: #f8f8f2; + --yellow: #e6db74; + --orange: #fd971f; + --red: #f92672; + --magenta: #fd5ff0; + --violet: #ae81ff; + --blue: #66d9ef; + --cyan: #a1efe4; + --green: #a6e22e; } a, a:visited { @@ -43,7 +56,7 @@ a, a:visited { margin-top: 0; margin-left: 0; - background-color: #3e3d32; + background-color: var(--bg_h); padding: 3px 10px; } .toolbar a[folder][parent]::before { @@ -90,14 +103,14 @@ a[folder][parent] + a[folder]:not([parent]) { .entry .expander { display: flex; align-items: center; - gap: 5px; + gap: 10px; user-select: none; } .entry .expander::before { content: "+"; font-size: 1.3rem; - margin-right: 0.2rem; + margin-right: 0rem; cursor: pointer; } .entry[open] .expander::before { @@ -106,7 +119,6 @@ a[folder][parent] + a[folder]:not([parent]) { .entry .expander a { text-decoration: none; - flex-grow: 1; } .entry .close { @@ -121,6 +133,12 @@ a[folder][parent] + a[folder]:not([parent]) { .entry[open] .details { display: block; } +.entry .inline-details { + display: none; +} +.entry[open] .inline-details { + display: inline; +} .entry .context { font-style: italic; @@ -134,9 +152,9 @@ a[folder][parent] + a[folder]:not([parent]) { font-family: Fira Code; font-size: 0.8rem; - color: #f8f8f2; + color: var(--fg); - background-color: #3e3d32; + background-color: var(--bg_h); border-radius: 5px; } @@ -146,13 +164,6 @@ a[folder][parent] + a[folder]:not([parent]) { gap: 0.6rem; } -.entry .comment { - display: none; -} -.entry[open] .comment { - display: inline; -} - .entry[open] .cluster { display: flex; flex-direction: column; @@ -165,21 +176,21 @@ a[folder][parent] + a[folder]:not([parent]) { .name { - color: #a6e22e; + color: var(--green); } .argument { - color: #fd971f; + color: var(--orange); font-style: italic; } .type { - color: #66d9ef !important; + color: var(--blue) !important; font-style: italic; } .comment { - color: #75715e; + color: var(--bg_c); font-style: italic; } @@ -288,4 +299,7 @@ a[folder][parent] + a[folder]:not([parent]) { #search .result .comment { font-style: normal; margin-left: 0.5rem; +} +#search .result[selected] { + background-color: var(--bg_h); } \ No newline at end of file