The Trans Directory
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: support non-singleton explorer

+168 -146
+4 -7
quartz/build.ts
··· 19 19 import { Mutex } from "async-mutex" 20 20 import DepGraph from "./depgraph" 21 21 import { getStaticResourcesFromPlugins } from "./plugins" 22 + import { randomIdNonSecure } from "./util/random" 22 23 23 24 type Dependencies = Record<string, DepGraph<FilePath> | null> 24 25 ··· 38 39 39 40 type FileEvent = "add" | "change" | "delete" 40 41 41 - function newBuildId() { 42 - return Math.random().toString(36).substring(2, 8) 43 - } 44 - 45 42 async function buildQuartz(argv: Argv, mut: Mutex, clientRefresh: () => void) { 46 43 const ctx: BuildCtx = { 47 - buildId: newBuildId(), 44 + buildId: randomIdNonSecure(), 48 45 argv, 49 46 cfg, 50 47 allSlugs: [], ··· 162 159 return 163 160 } 164 161 165 - const buildId = newBuildId() 162 + const buildId = randomIdNonSecure() 166 163 ctx.buildId = buildId 167 164 buildData.lastBuildMs = new Date().getTime() 168 165 const release = await mut.acquire() ··· 359 356 toRemove.add(filePath) 360 357 } 361 358 362 - const buildId = newBuildId() 359 + const buildId = randomIdNonSecure() 363 360 ctx.buildId = buildId 364 361 buildData.lastBuildMs = new Date().getTime() 365 362 const release = await mut.acquire()
+4 -3
quartz/components/Backlinks.tsx
··· 3 3 import { resolveRelative, simplifySlug } from "../util/path" 4 4 import { i18n } from "../i18n" 5 5 import { classNames } from "../util/lang" 6 - import OverflowList from "./OverflowList" 6 + import OverflowListFactory from "./OverflowList" 7 7 8 8 interface BacklinksOptions { 9 9 hideWhenEmpty: boolean ··· 15 15 16 16 export default ((opts?: Partial<BacklinksOptions>) => { 17 17 const options: BacklinksOptions = { ...defaultOptions, ...opts } 18 + const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory() 18 19 19 20 const Backlinks: QuartzComponent = ({ 20 21 fileData, ··· 30 31 return ( 31 32 <div class={classNames(displayClass, "backlinks")}> 32 33 <h3>{i18n(cfg.locale).components.backlinks.title}</h3> 33 - <OverflowList id="backlinks-ul"> 34 + <OverflowList> 34 35 {backlinkFiles.length > 0 ? ( 35 36 backlinkFiles.map((f) => ( 36 37 <li> ··· 48 49 } 49 50 50 51 Backlinks.css = style 51 - Backlinks.afterDOMLoaded = OverflowList.afterDOMLoaded("backlinks-ul") 52 + Backlinks.afterDOMLoaded = overflowListAfterDOMLoaded 52 53 53 54 return Backlinks 54 55 }) satisfies QuartzComponentConstructor
+8 -8
quartz/components/Explorer.tsx
··· 6 6 import { classNames } from "../util/lang" 7 7 import { i18n } from "../i18n" 8 8 import { FileTrieNode } from "../util/fileTrie" 9 - import OverflowList from "./OverflowList" 9 + import OverflowListFactory from "./OverflowList" 10 + import { concatenateResources } from "../util/resources" 10 11 11 12 type OrderEntries = "sort" | "filter" | "map" 12 13 ··· 56 57 57 58 export default ((userOpts?: Partial<Options>) => { 58 59 const opts: Options = { ...defaultOptions, ...userOpts } 60 + const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory() 59 61 60 62 const Explorer: QuartzComponent = ({ cfg, displayClass }: QuartzComponentProps) => { 61 63 return ( ··· 73 75 > 74 76 <button 75 77 type="button" 76 - id="mobile-explorer" 77 - class="explorer-toggle hide-until-loaded" 78 + class="explorer-toggle mobile-explorer hide-until-loaded" 78 79 data-mobile={true} 79 80 aria-controls="explorer-content" 80 81 > ··· 95 96 </button> 96 97 <button 97 98 type="button" 98 - id="desktop-explorer" 99 - class="title-button explorer-toggle" 99 + class="title-button explorer-toggle desktop-explorer" 100 100 data-mobile={false} 101 101 aria-expanded={true} 102 102 > ··· 116 116 <polyline points="6 9 12 15 18 9"></polyline> 117 117 </svg> 118 118 </button> 119 - <div id="explorer-content" aria-expanded={false}> 120 - <OverflowList id="explorer-ul" /> 119 + <div class="explorer-content" aria-expanded={false}> 120 + <OverflowList class="explorer-ul" /> 121 121 </div> 122 122 <template id="template-file"> 123 123 <li> ··· 157 157 } 158 158 159 159 Explorer.css = style 160 - Explorer.afterDOMLoaded = script + OverflowList.afterDOMLoaded("explorer-ul") 160 + Explorer.afterDOMLoaded = concatenateResources(script, overflowListAfterDOMLoaded) 161 161 return Explorer 162 162 }) satisfies QuartzComponentConstructor
+14 -5
quartz/components/OverflowList.tsx
··· 1 1 import { JSX } from "preact" 2 + import { randomIdNonSecure } from "../util/random" 2 3 3 4 const OverflowList = ({ 4 5 children, 5 6 ...props 6 7 }: JSX.HTMLAttributes<HTMLUListElement> & { id: string }) => { 7 8 return ( 8 - <ul class="overflow" {...props}> 9 + <ul {...props} class={[props.class, "overflow"].filter(Boolean).join(" ")} id={props.id}> 9 10 {children} 10 11 <li class="overflow-end" /> 11 12 </ul> 12 13 ) 13 14 } 14 15 15 - OverflowList.afterDOMLoaded = (id: string) => ` 16 + export default () => { 17 + const id = randomIdNonSecure() 18 + 19 + return { 20 + OverflowList: (props: JSX.HTMLAttributes<HTMLUListElement>) => ( 21 + <OverflowList {...props} id={id} /> 22 + ), 23 + overflowListAfterDOMLoaded: ` 16 24 document.addEventListener("nav", (e) => { 17 25 const observer = new IntersectionObserver((entries) => { 18 26 for (const entry of entries) { 19 27 const parentUl = entry.target.parentElement 28 + if (!parentUl) return 20 29 if (entry.isIntersecting) { 21 30 parentUl.classList.remove("gradient-active") 22 31 } else { ··· 34 43 observer.observe(end) 35 44 window.addCleanup(() => observer.disconnect()) 36 45 }) 37 - ` 38 - 39 - export default OverflowList 46 + `, 47 + } 48 + }
+68 -65
quartz/components/TableOfContents.tsx
··· 6 6 // @ts-ignore 7 7 import script from "./scripts/toc.inline" 8 8 import { i18n } from "../i18n" 9 - import OverflowList from "./OverflowList" 9 + import OverflowListFactory from "./OverflowList" 10 + import { concatenateResources } from "../util/resources" 10 11 11 12 interface Options { 12 13 layout: "modern" | "legacy" ··· 16 17 layout: "modern", 17 18 } 18 19 19 - const TableOfContents: QuartzComponent = ({ 20 - fileData, 21 - displayClass, 22 - cfg, 23 - }: QuartzComponentProps) => { 24 - if (!fileData.toc) { 25 - return null 20 + export default ((opts?: Partial<Options>) => { 21 + const layout = opts?.layout ?? defaultOptions.layout 22 + const { OverflowList, overflowListAfterDOMLoaded } = OverflowListFactory() 23 + const TableOfContents: QuartzComponent = ({ 24 + fileData, 25 + displayClass, 26 + cfg, 27 + }: QuartzComponentProps) => { 28 + if (!fileData.toc) { 29 + return null 30 + } 31 + 32 + return ( 33 + <div class={classNames(displayClass, "toc")}> 34 + <button 35 + type="button" 36 + class={fileData.collapseToc ? "collapsed toc-header" : "toc-header"} 37 + aria-controls="toc-content" 38 + aria-expanded={!fileData.collapseToc} 39 + > 40 + <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3> 41 + <svg 42 + xmlns="http://www.w3.org/2000/svg" 43 + width="24" 44 + height="24" 45 + viewBox="0 0 24 24" 46 + fill="none" 47 + stroke="currentColor" 48 + stroke-width="2" 49 + stroke-linecap="round" 50 + stroke-linejoin="round" 51 + class="fold" 52 + > 53 + <polyline points="6 9 12 15 18 9"></polyline> 54 + </svg> 55 + </button> 56 + <div class={fileData.collapseToc ? "collapsed toc-content" : "toc-content"}> 57 + <OverflowList> 58 + {fileData.toc.map((tocEntry) => ( 59 + <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> 60 + <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}> 61 + {tocEntry.text} 62 + </a> 63 + </li> 64 + ))} 65 + </OverflowList> 66 + </div> 67 + </div> 68 + ) 26 69 } 27 70 28 - return ( 29 - <div class={classNames(displayClass, "toc")}> 30 - <button 31 - type="button" 32 - class={fileData.collapseToc ? "collapsed toc-header" : "toc-header"} 33 - aria-controls="toc-content" 34 - aria-expanded={!fileData.collapseToc} 35 - > 36 - <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3> 37 - <svg 38 - xmlns="http://www.w3.org/2000/svg" 39 - width="24" 40 - height="24" 41 - viewBox="0 0 24 24" 42 - fill="none" 43 - stroke="currentColor" 44 - stroke-width="2" 45 - stroke-linecap="round" 46 - stroke-linejoin="round" 47 - class="fold" 48 - > 49 - <polyline points="6 9 12 15 18 9"></polyline> 50 - </svg> 51 - </button> 52 - <div class={fileData.collapseToc ? "collapsed toc-content" : "toc-content"}> 53 - <OverflowList id="toc-ul"> 71 + TableOfContents.css = modernStyle 72 + TableOfContents.afterDOMLoaded = concatenateResources(script, overflowListAfterDOMLoaded) 73 + 74 + const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => { 75 + if (!fileData.toc) { 76 + return null 77 + } 78 + return ( 79 + <details class="toc" open={!fileData.collapseToc}> 80 + <summary> 81 + <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3> 82 + </summary> 83 + <ul> 54 84 {fileData.toc.map((tocEntry) => ( 55 85 <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> 56 86 <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}> ··· 58 88 </a> 59 89 </li> 60 90 ))} 61 - </OverflowList> 62 - </div> 63 - </div> 64 - ) 65 - } 66 - TableOfContents.css = modernStyle 67 - TableOfContents.afterDOMLoaded = script + OverflowList.afterDOMLoaded("toc-ul") 68 - 69 - const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => { 70 - if (!fileData.toc) { 71 - return null 91 + </ul> 92 + </details> 93 + ) 72 94 } 73 - return ( 74 - <details class="toc" open={!fileData.collapseToc}> 75 - <summary> 76 - <h3>{i18n(cfg.locale).components.tableOfContents.title}</h3> 77 - </summary> 78 - <ul> 79 - {fileData.toc.map((tocEntry) => ( 80 - <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> 81 - <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}> 82 - {tocEntry.text} 83 - </a> 84 - </li> 85 - ))} 86 - </ul> 87 - </details> 88 - ) 89 - } 90 - LegacyTableOfContents.css = legacyStyle 95 + LegacyTableOfContents.css = legacyStyle 91 96 92 - export default ((opts?: Partial<Options>) => { 93 - const layout = opts?.layout ?? defaultOptions.layout 94 97 return layout === "modern" ? TableOfContents : LegacyTableOfContents 95 98 }) satisfies QuartzComponentConstructor
+2 -1
quartz/components/pages/FolderContent.tsx
··· 9 9 import { i18n } from "../../i18n" 10 10 import { QuartzPluginData } from "../../plugins/vfile" 11 11 import { ComponentChildren } from "preact" 12 + import { concatenateResources } from "../../util/resources" 12 13 13 14 interface FolderContentOptions { 14 15 /** ··· 104 105 ) 105 106 } 106 107 107 - FolderContent.css = style + PageList.css 108 + FolderContent.css = concatenateResources(style, PageList.css) 108 109 return FolderContent 109 110 }) satisfies QuartzComponentConstructor
+2 -1
quartz/components/pages/TagContent.tsx
··· 7 7 import { htmlToJsx } from "../../util/jsx" 8 8 import { i18n } from "../../i18n" 9 9 import { ComponentChildren } from "preact" 10 + import { concatenateResources } from "../../util/resources" 10 11 11 12 interface TagContentOptions { 12 13 sort?: SortFn ··· 124 125 } 125 126 } 126 127 127 - TagContent.css = style + PageList.css 128 + TagContent.css = concatenateResources(style, PageList.css) 128 129 return TagContent 129 130 }) satisfies QuartzComponentConstructor
+21 -25
quartz/components/scripts/explorer.inline.ts
··· 21 21 22 22 let currentExplorerState: Array<FolderState> 23 23 function toggleExplorer(this: HTMLElement) { 24 - const explorers = document.querySelectorAll(".explorer") 25 - for (const explorer of explorers) { 26 - explorer.classList.toggle("collapsed") 27 - explorer.setAttribute( 28 - "aria-expanded", 29 - explorer.getAttribute("aria-expanded") === "true" ? "false" : "true", 30 - ) 31 - } 24 + const nearestExplorer = this.closest(".explorer") as HTMLElement 25 + if (!nearestExplorer) return 26 + nearestExplorer.classList.toggle("collapsed") 27 + nearestExplorer.setAttribute( 28 + "aria-expanded", 29 + nearestExplorer.getAttribute("aria-expanded") === "true" ? "false" : "true", 30 + ) 32 31 } 33 32 34 33 function toggleFolder(evt: MouseEvent) { ··· 145 144 } 146 145 147 146 async function setupExplorer(currentSlug: FullSlug) { 148 - const allExplorers = document.querySelectorAll(".explorer") as NodeListOf<HTMLElement> 147 + const allExplorers = document.querySelectorAll("div.explorer") as NodeListOf<HTMLElement> 149 148 150 149 for (const explorer of allExplorers) { 151 150 const dataFns = JSON.parse(explorer.dataset.dataFns || "{}") ··· 192 191 collapsed: oldIndex.get(path) === true, 193 192 })) 194 193 195 - const explorerUl = document.getElementById("explorer-ul") 194 + const explorerUl = explorer.querySelector(".explorer-ul") 196 195 if (!explorerUl) continue 197 196 198 197 // Create and insert new content ··· 219 218 } 220 219 221 220 // Set up event handlers 222 - const explorerButtons = explorer.querySelectorAll( 223 - "button.explorer-toggle", 224 - ) as NodeListOf<HTMLElement> 225 - if (explorerButtons) { 226 - window.addCleanup(() => 227 - explorerButtons.forEach((button) => button.removeEventListener("click", toggleExplorer)), 228 - ) 229 - explorerButtons.forEach((button) => button.addEventListener("click", toggleExplorer)) 221 + const explorerButtons = explorer.getElementsByClassName( 222 + "explorer-toggle", 223 + ) as HTMLCollectionOf<HTMLElement> 224 + for (const button of explorerButtons) { 225 + button.addEventListener("click", toggleExplorer) 226 + window.addCleanup(() => button.removeEventListener("click", toggleExplorer)) 230 227 } 231 228 232 229 // Set up folder click handlers ··· 235 232 "folder-button", 236 233 ) as HTMLCollectionOf<HTMLElement> 237 234 for (const button of folderButtons) { 238 - window.addCleanup(() => button.removeEventListener("click", toggleFolder)) 239 235 button.addEventListener("click", toggleFolder) 236 + window.addCleanup(() => button.removeEventListener("click", toggleFolder)) 240 237 } 241 238 } 242 239 ··· 244 241 "folder-icon", 245 242 ) as HTMLCollectionOf<HTMLElement> 246 243 for (const icon of folderIcons) { 247 - window.addCleanup(() => icon.removeEventListener("click", toggleFolder)) 248 244 icon.addEventListener("click", toggleFolder) 245 + window.addCleanup(() => icon.removeEventListener("click", toggleFolder)) 249 246 } 250 247 } 251 248 } 252 249 253 - document.addEventListener("prenav", async (e: CustomEventMap["prenav"]) => { 250 + document.addEventListener("prenav", async () => { 254 251 // save explorer scrollTop position 255 - const explorer = document.getElementById("explorer-ul") 252 + const explorer = document.querySelector(".explorer-ul") 256 253 if (!explorer) return 257 254 sessionStorage.setItem("explorerScrollTop", explorer.scrollTop.toString()) 258 255 }) ··· 262 259 await setupExplorer(currentSlug) 263 260 264 261 // if mobile hamburger is visible, collapse by default 265 - const mobileExplorer = document.getElementById("mobile-explorer") 266 - if (mobileExplorer && mobileExplorer.checkVisibility()) { 267 - for (const explorer of document.querySelectorAll(".explorer")) { 262 + for (const explorer of document.getElementsByClassName("mobile-explorer")) { 263 + if (explorer.checkVisibility()) { 268 264 explorer.classList.add("collapsed") 269 265 explorer.setAttribute("aria-expanded", "false") 270 266 }
+14 -12
quartz/components/styles/explorer.scss
··· 20 20 margin: 0; 21 21 } 22 22 23 - .hide-until-loaded ~ #explorer-content { 23 + .hide-until-loaded ~ .explorer-content { 24 24 display: none; 25 25 } 26 26 } ··· 30 30 display: flex; 31 31 flex-direction: column; 32 32 overflow-y: hidden; 33 + 34 + min-height: 1.2rem; 33 35 flex: 0 1 auto; 34 36 &.collapsed { 35 37 flex: 0 1 1.2rem; ··· 52 54 align-self: flex-start; 53 55 } 54 56 55 - button#mobile-explorer { 57 + button.mobile-explorer { 56 58 display: none; 57 59 } 58 60 59 - button#desktop-explorer { 61 + button.desktop-explorer { 60 62 display: flex; 61 63 } 62 64 63 65 @media all and ($mobile) { 64 - button#mobile-explorer { 66 + button.mobile-explorer { 65 67 display: flex; 66 68 } 67 69 68 - button#desktop-explorer { 70 + button.desktop-explorer { 69 71 display: none; 70 72 } 71 73 } ··· 86 88 } 87 89 } 88 90 89 - button#mobile-explorer, 90 - button#desktop-explorer { 91 + button.mobile-explorer, 92 + button.desktop-explorer { 91 93 background-color: transparent; 92 94 border: none; 93 95 text-align: left; ··· 104 106 } 105 107 } 106 108 107 - #explorer-content { 109 + .explorer-content { 108 110 list-style: none; 109 111 overflow: hidden; 110 112 overflow-y: auto; ··· 209 211 &.collapsed { 210 212 flex: 0 0 34px; 211 213 212 - & > #explorer-content { 214 + & > .explorer-content { 213 215 transform: translateX(-100vw); 214 216 visibility: hidden; 215 217 } ··· 218 220 &:not(.collapsed) { 219 221 flex: 0 0 34px; 220 222 221 - & > #explorer-content { 223 + & > .explorer-content { 222 224 transform: translateX(0); 223 225 visibility: visible; 224 226 } 225 227 } 226 228 227 - #explorer-content { 229 + .explorer-content { 228 230 box-sizing: border-box; 229 231 z-index: 100; 230 232 position: absolute; ··· 245 247 visibility: hidden; 246 248 } 247 249 248 - #mobile-explorer { 250 + .mobile-explorer { 249 251 margin: 0; 250 252 padding: 5px; 251 253 z-index: 101;
+1
quartz/components/styles/toc.scss
··· 5 5 flex-direction: column; 6 6 7 7 overflow-y: hidden; 8 + min-height: 4rem; 8 9 flex: 0 1 auto; 9 10 &:has(button.toc-header.collapsed) { 10 11 flex: 0 1 1.2rem;
+4 -4
quartz/components/types.ts
··· 1 1 import { ComponentType, JSX } from "preact" 2 - import { StaticResources } from "../util/resources" 2 + import { StaticResources, StringResource } from "../util/resources" 3 3 import { QuartzPluginData } from "../plugins/vfile" 4 4 import { GlobalConfiguration } from "../cfg" 5 5 import { Node } from "hast" ··· 19 19 } 20 20 21 21 export type QuartzComponent = ComponentType<QuartzComponentProps> & { 22 - css?: string 23 - beforeDOMLoaded?: string 24 - afterDOMLoaded?: string 22 + css?: StringResource 23 + beforeDOMLoaded?: StringResource 24 + afterDOMLoaded?: StringResource 25 25 } 26 26 27 27 export type QuartzComponentConstructor<Options extends object | undefined = undefined> = (
+13 -9
quartz/plugins/emitters/componentResources.ts
··· 36 36 afterDOMLoaded: new Set<string>(), 37 37 } 38 38 39 + function normalizeResource(resource: string | string[] | undefined): string[] { 40 + if (!resource) return [] 41 + if (Array.isArray(resource)) return resource 42 + return [resource] 43 + } 44 + 39 45 for (const component of allComponents) { 40 46 const { css, beforeDOMLoaded, afterDOMLoaded } = component 41 - if (css) { 42 - componentResources.css.add(css) 43 - } 44 - if (beforeDOMLoaded) { 45 - componentResources.beforeDOMLoaded.add(beforeDOMLoaded) 46 - } 47 - if (afterDOMLoaded) { 48 - componentResources.afterDOMLoaded.add(afterDOMLoaded) 49 - } 47 + const normalizedCss = normalizeResource(css) 48 + const normalizedBeforeDOMLoaded = normalizeResource(beforeDOMLoaded) 49 + const normalizedAfterDOMLoaded = normalizeResource(afterDOMLoaded) 50 + 51 + normalizedCss.forEach((c) => componentResources.css.add(c)) 52 + normalizedBeforeDOMLoaded.forEach((b) => componentResources.beforeDOMLoaded.add(b)) 53 + normalizedAfterDOMLoaded.forEach((a) => componentResources.afterDOMLoaded.add(a)) 50 54 } 51 55 52 56 return {
+3 -6
quartz/styles/base.scss
··· 542 542 } 543 543 544 544 .spacer { 545 - flex: 1 1 auto; 545 + flex: 2 1 auto; 546 546 } 547 547 548 548 div:has(> .overflow) { ··· 555 555 max-height: 100%; 556 556 overflow-y: auto; 557 557 width: 100%; 558 + margin-bottom: 0; 558 559 559 560 // clearfix 560 561 content: ""; 561 562 clear: both; 562 563 563 - & > li:last-of-type { 564 - margin-bottom: 30px; 565 - } 566 - 567 564 & > li.overflow-end { 568 - height: 4px; 565 + height: 1rem; 569 566 margin: 0; 570 567 } 571 568
+3
quartz/util/random.ts
··· 1 + export function randomIdNonSecure() { 2 + return Math.random().toString(36).substring(2, 8) 3 + }
+7
quartz/util/resources.tsx
··· 65 65 js: JSResource[] 66 66 additionalHead: (JSX.Element | ((pageData: QuartzPluginData) => JSX.Element))[] 67 67 } 68 + 69 + export type StringResource = string | string[] | undefined 70 + export function concatenateResources(...resources: StringResource[]): StringResource { 71 + return resources 72 + .filter((resource): resource is string | string[] => resource !== undefined) 73 + .flat() 74 + }