more visual polish, adjust colours and spacing
| | |
| | | ], |
| | | left: [ |
| | | Component.PageTitle(), |
| | | Component.MobileOnly(Component.Spacer()), |
| | | Component.Search(), |
| | | Component.Darkmode(), |
| | | Component.DesktopOnly(Component.TableOfContents()), |
| | |
| | | ], |
| | | left: [ |
| | | Component.PageTitle(), |
| | | Component.MobileOnly(Component.Spacer()), |
| | | Component.Search(), |
| | | Component.Darkmode() |
| | | ], |
| | |
| | | colors: { |
| | | lightMode: { |
| | | light: '#faf8f8', |
| | | lightgray: '#e8e8e8', |
| | | gray: '#dadada', |
| | | lightgray: '#e5e5e5', |
| | | gray: '#b8b8b8', |
| | | darkgray: '#4e4e4e', |
| | | dark: '#141021', |
| | | secondary: '#284b63', |
| | |
| | | }, |
| | | darkMode: { |
| | | light: '#161618', |
| | | lightgray: '#292629', |
| | | gray: '#343434', |
| | | lightgray: '#393639', |
| | | gray: '#646464', |
| | | darkgray: '#d4d4d4', |
| | | dark: '#fbfffe', |
| | | secondary: '#7b97aa', |
| | |
| | | const backlinkFiles = allFiles.filter(file => file.links?.includes(slug)) |
| | | return <div class="backlinks"> |
| | | <h3>Backlinks</h3> |
| | | <ul> |
| | | <ul class="overflow"> |
| | | {backlinkFiles.length > 0 ? |
| | | backlinkFiles.map(f => <li><a href={stripIndex(relativeToRoot(slug, f.slug!))} class="internal">{f.frontmatter?.title}</a></li>) |
| | | : <li>No backlinks found</li>} |
| | |
| | | if (component) { |
| | | const Component = component |
| | | function DesktopOnly(props: QuartzComponentProps) { |
| | | return <div class="desktop-only"> |
| | | <Component {...props} /> |
| | | </div> |
| | | return <Component displayClass="desktop-only" {...props} /> |
| | | } |
| | | |
| | | DesktopOnly.displayName = component.displayName |
| | |
| | | const year = new Date().getFullYear() |
| | | const name = opts?.authorName ?? "someone" |
| | | const links = opts?.links ?? [] |
| | | return <> |
| | | return <footer> |
| | | <hr /> |
| | | <footer> |
| | | <p>Made by {name} using <a href="https://quartz.jzhao.xyz/">Quartz</a>, © {year}</p> |
| | | <ul>{Object.entries(links).map(([text, link]) => <li> |
| | | <a href={link}>{text}</a> |
| | | </li>)}</ul> |
| | | </footer> |
| | | </> |
| | | <p>Made by {name} using <a href="https://quartz.jzhao.xyz/">Quartz</a>, © {year}</p> |
| | | <ul>{Object.entries(links).map(([text, link]) => <li> |
| | | <a href={link}>{text}</a> |
| | | </li>)}</ul> |
| | | </footer> |
| | | } |
| | | |
| | | Footer.css = style |
| | |
| | | const localGraph = { ...opts?.localGraph, ...defaultOptions.localGraph } |
| | | const globalGraph = { ...opts?.globalGraph, ...defaultOptions.globalGraph } |
| | | return <div class="graph"> |
| | | <h3>Interactive Graph</h3> |
| | | <h3>Site Graph</h3> |
| | | <div class="graph-outer"> |
| | | <div id="graph-container" data-cfg={JSON.stringify(localGraph)}></div> |
| | | <svg version="1.1" id="global-graph-icon" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" x="0px" y="0px" |
| | |
| | | if (component) { |
| | | const Component = component |
| | | function MobileOnly(props: QuartzComponentProps) { |
| | | return <div class="mobile-only"> |
| | | <Component {...props} /> |
| | | </div> |
| | | return <Component displayClass="mobile-only" {...props} /> |
| | | } |
| | | |
| | | MobileOnly.displayName = component.displayName |
| | |
| | | ReadingTime.css = ` |
| | | .reading-time { |
| | | margin-top: 0; |
| | | opacity: 0.5; |
| | | color: var(--gray); |
| | | } |
| | | ` |
| | | |
| | |
| | | import { QuartzComponentConstructor } from "./types" |
| | | import { QuartzComponentConstructor, QuartzComponentProps } from "./types" |
| | | |
| | | function Spacer() { |
| | | return <div class="spacer"></div> |
| | | function Spacer({ displayClass }: QuartzComponentProps) { |
| | | const className = displayClass ? `spacer ${displayClass}` : "spacer" |
| | | return <div class={className}></div> |
| | | } |
| | | |
| | | export default (() => Spacer) satisfies QuartzComponentConstructor |
| | |
| | | </svg> |
| | | </button> |
| | | <div id="toc-content"> |
| | | <ul> |
| | | <ul class="overflow"> |
| | | {fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}> |
| | | <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>{tocEntry.text}</a> |
| | | </li>)} |
| | |
| | | const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) |
| | | return <div class="popover-hint"> |
| | | <article>{content}</article> |
| | | <hr/> |
| | | <p>{allPagesInFolder.length} items under this folder.</p> |
| | | <div> |
| | | <PageList {...listProps} /> |
| | |
| | | const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) |
| | | return <div class="popover-hint"> |
| | | <article>{content}</article> |
| | | <hr/> |
| | | <p>{allPagesWithTag.length} items with this tag.</p> |
| | | <div> |
| | | <PageList {...listProps} /> |
| | |
| | | <Head {...componentData} /> |
| | | <body data-slug={slug}> |
| | | <div id="quartz-root" class="page"> |
| | | <div class="page-header"> |
| | | <Header {...componentData} > |
| | | {header.map(HeaderComponent => <HeaderComponent {...componentData} />)} |
| | | </Header> |
| | | <div class="popover-hint"> |
| | | {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)} |
| | | </div> |
| | | </div> |
| | | <Body {...componentData}> |
| | | {LeftComponent} |
| | | <div class="center"> |
| | | <div class="page-header"> |
| | | <Header {...componentData} > |
| | | {header.map(HeaderComponent => <HeaderComponent {...componentData} />)} |
| | | </Header> |
| | | <div class="popover-hint"> |
| | | {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)} |
| | | </div> |
| | | </div> |
| | | <Content {...componentData} /> |
| | | <Footer {...componentData} /> |
| | | </div> |
| | | {RightComponent} |
| | | </Body> |
| | | <Footer {...componentData} /> |
| | | </div> |
| | | </body> |
| | | {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))} |
| | |
| | | const labels = graphNode |
| | | .append("text") |
| | | .attr("dx", 0) |
| | | .attr("dy", (d) => nodeRadius(d) - 8 + "px") |
| | | .attr("dy", (d) => -nodeRadius(d) + "px") |
| | | .attr("text-anchor", "middle") |
| | | .text((d) => data[d.id]?.title || (d.id.charAt(1).toUpperCase() + d.id.slice(2)).replace("-", " ")) |
| | | .style('opacity', (opacityScale - 1) / 3.75) |
| | |
| | | containerIcon?.addEventListener("click", renderGlobalGraph) |
| | | }) |
| | | |
| | | let resizeEventDebounce: number | undefined = undefined |
| | | window.addEventListener('resize', () => { |
| | | if (resizeEventDebounce) { |
| | | clearTimeout(resizeEventDebounce) |
| | | } |
| | | |
| | | resizeEventDebounce = window.setTimeout(async () => { |
| | | const slug = document.body.dataset["slug"]! |
| | | await renderGraph("graph-container", slug) |
| | | }, 50) |
| | | }) |
| | |
| | | document.addEventListener("nav", async (e: unknown) => { |
| | | const currentSlug = (e as CustomEventMap["nav"]).detail.url |
| | | |
| | | // setup index if it hasn't been already |
| | | const data = await fetchData |
| | | if (!index) { |
| | | index = new Document({ |
| | | cache: true, |
| | | charset: 'latin:extra', |
| | | optimize: true, |
| | | encode: encoder, |
| | | document: { |
| | | id: "slug", |
| | | index: [ |
| | | { |
| | | field: "title", |
| | | tokenize: "forward", |
| | | }, |
| | | { |
| | | field: "content", |
| | | tokenize: "reverse", |
| | | }, |
| | | ] |
| | | }, |
| | | }) |
| | | |
| | | for (const [slug, fileData] of Object.entries<ContentDetails>(data)) { |
| | | await index.addAsync(slug, { |
| | | slug, |
| | | title: fileData.title, |
| | | content: fileData.content |
| | | }) |
| | | } |
| | | } |
| | | |
| | | const container = document.getElementById("search-container") |
| | | const searchIcon = document.getElementById("search-icon") |
| | | const searchBar = document.getElementById("search-bar") as HTMLInputElement | null |
| | |
| | | searchIcon?.addEventListener("click", showSearch) |
| | | searchBar?.removeEventListener("input", onType) |
| | | searchBar?.addEventListener("input", onType) |
| | | |
| | | // setup index if it hasn't been already |
| | | if (!index) { |
| | | index = new Document({ |
| | | cache: true, |
| | | charset: 'latin:extra', |
| | | optimize: true, |
| | | encode: encoder, |
| | | document: { |
| | | id: "slug", |
| | | index: [ |
| | | { |
| | | field: "title", |
| | | tokenize: "forward", |
| | | }, |
| | | { |
| | | field: "content", |
| | | tokenize: "reverse", |
| | | }, |
| | | ] |
| | | }, |
| | | }) |
| | | |
| | | for (const [slug, fileData] of Object.entries<ContentDetails>(data)) { |
| | | await index.addAsync(slug, { |
| | | slug, |
| | | title: fileData.title, |
| | | content: fileData.content |
| | | }) |
| | | } |
| | | } |
| | | |
| | | // register handlers |
| | | registerEscapeHandler(container, hideSearch) |
| | |
| | | position: relative; |
| | | width: 20px; |
| | | height: 20px; |
| | | margin: 1rem; |
| | | margin: 0 10px; |
| | | |
| | | & > .toggle { |
| | | display: none; |
| | |
| | | footer { |
| | | text-align: left; |
| | | opacity: 0.8; |
| | | margin-bottom: 4rem; |
| | | |
| | | & ul { |
| | |
| | | line-height: normal; |
| | | font-size: initial; |
| | | font-family: var(--bodyFont); |
| | | border: 1px solid var(--gray); |
| | | border: 1px solid var(--lightgray); |
| | | background-color: var(--light); |
| | | border-radius: 5px; |
| | | box-shadow: 6px 6px 36px 0 rgba(0,0,0,0.25); |
| | | overflow: scroll; |
| | | overflow: auto; |
| | | } |
| | | |
| | | h1 { |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | cursor: pointer; |
| | | white-space: nowrap; |
| | | |
| | | & > div { |
| | | flex-grow: 1; |
| | |
| | | |
| | | & > #search-container { |
| | | position: fixed; |
| | | contain: layout; |
| | | z-index: 999; |
| | | left: 0; |
| | | top: 0; |
| | | width: 100vw; |
| | | height: 100vh; |
| | | overflow: scroll; |
| | | overflow-y: auto; |
| | | display: none; |
| | | backdrop-filter: blur(4px); |
| | | |
| | |
| | | margin-left: auto; |
| | | margin-right: auto; |
| | | |
| | | @media all and (max-width: $tabletBreakpoint) { |
| | | @media all and (max-width: $fullPageWidth) { |
| | | width: 90%; |
| | | } |
| | | |
| | |
| | | list-style: none; |
| | | overflow: hidden; |
| | | max-height: none; |
| | | transition: max-height 0.3s ease; |
| | | transition: max-height 0.5s ease; |
| | | |
| | | & ul { |
| | | list-style: none; |
| | |
| | | children: (QuartzComponent | JSX.Element)[] |
| | | tree: Node<QuartzPluginData> |
| | | allFiles: QuartzPluginData[] |
| | | displayClass?: 'mobile-only' | 'desktop-only' |
| | | } & JSX.IntrinsicAttributes & { |
| | | [key: string]: any |
| | | } |
| | | |
| | | export type QuartzComponent = ComponentType<QuartzComponentProps> & { |
| | |
| | | } |
| | | } |
| | | |
| | | .page { |
| | | & > .page-header { |
| | | max-width: $pageWidth; |
| | | margin: $topSpacing auto 0 auto; |
| | | } |
| | | |
| | | & > #quartz-body { |
| | | width: 100%; |
| | | display: flex; |
| | | |
| | | & .left, & .right { |
| | | flex: 1; |
| | | width: calc(calc(100vw - $pageWidth) / 2); |
| | | } |
| | | |
| | | & .left-inner, & .right-inner { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 2rem; |
| | | top: 0; |
| | | width: $sidePanelWidth; |
| | | margin-top: $topSpacing; |
| | | box-sizing: border-box; |
| | | padding: 0 4rem; |
| | | position: fixed; |
| | | } |
| | | |
| | | & .left-inner { |
| | | left: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth); |
| | | } |
| | | |
| | | & .right-inner { |
| | | right: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth); |
| | | } |
| | | |
| | | & .center { |
| | | width: $pageWidth; |
| | | margin: 0 auto; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .desktop-only { |
| | | display: initial; |
| | | @media all and (max-width: ($pageWidth + 2 * $sidePanelWidth)) { |
| | | @media all and (max-width: $fullPageWidth) { |
| | | display: none; |
| | | } |
| | | } |
| | | |
| | | .mobile-only { |
| | | display: none; |
| | | @media all and (max-width: ($pageWidth + 2 * $sidePanelWidth)) { |
| | | @media all and (max-width: $fullPageWidth) { |
| | | display: initial; |
| | | } |
| | | } |
| | | |
| | | .page { |
| | | @media all and (max-width: $tabletBreakpoint) { |
| | | margin: 25px 5vw; |
| | | & .left, & .right { |
| | | padding: 0; |
| | | height: initial; |
| | | max-width: none; |
| | | position: initial; |
| | | } |
| | | @media all and (max-width: $fullPageWidth) { |
| | | margin: 0 5vw; |
| | | } |
| | | |
| | | & p { |
| | |
| | | padding-left: 0; |
| | | } |
| | | } |
| | | |
| | | & > #quartz-body { |
| | | width: 100%; |
| | | display: flex; |
| | | @media all and (max-width: $fullPageWidth) { |
| | | flex-direction: column; |
| | | } |
| | | |
| | | & .left, & .right { |
| | | flex: 1; |
| | | width: calc(calc(100vw - $pageWidth) / 2); |
| | | @media all and (max-width: $fullPageWidth) { |
| | | width: initial; |
| | | } |
| | | } |
| | | |
| | | & .left-inner, & .right-inner { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 2rem; |
| | | top: 0; |
| | | width: $sidePanelWidth; |
| | | margin-top: $topSpacing; |
| | | box-sizing: border-box; |
| | | padding: 0 4rem; |
| | | position: fixed; |
| | | @media all and (max-width: $fullPageWidth) { |
| | | position: initial; |
| | | flex-direction: row; |
| | | padding: 0; |
| | | width: initial; |
| | | margin-top: 4rem; |
| | | } |
| | | } |
| | | |
| | | & .left-inner { |
| | | left: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth); |
| | | @media all and (max-width: $fullPageWidth) { |
| | | gap: 1rem; |
| | | align-items: center; |
| | | } |
| | | } |
| | | |
| | | & .right-inner { |
| | | right: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth); |
| | | & > * { |
| | | @media all and (max-width: $fullPageWidth) { |
| | | flex: 1; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | & .page-header { |
| | | width: $pageWidth; |
| | | margin: $topSpacing auto 0 auto; |
| | | @media all and (max-width: $fullPageWidth) { |
| | | width: initial; |
| | | margin-top: 2rem; |
| | | } |
| | | } |
| | | |
| | | & .center, & footer { |
| | | width: $pageWidth; |
| | | margin-left: auto; |
| | | margin-right: auto; |
| | | @media all and (max-width: $fullPageWidth) { |
| | | width: initial; |
| | | margin-left: 0; |
| | | margin-right: 0; |
| | | } |
| | | } |
| | | } |
| | | |
| | | input[type="checkbox"] { |
| | |
| | | font-family: var(--codeFont); |
| | | padding: 0.5rem; |
| | | border-radius: 5px; |
| | | overflow-x: scroll; |
| | | overflow-x: auto; |
| | | border: 1px solid var(--lightgray); |
| | | |
| | | & > code { |
| | |
| | | .spacer { |
| | | flex: 1 1 auto; |
| | | } |
| | | |
| | | ul.overflow, ol.overflow { |
| | | height: 400px; |
| | | overflow-y: scroll; |
| | | |
| | | & > li:last-of-type { |
| | | margin-bottom: 50px; |
| | | } |
| | | |
| | | &:after { |
| | | pointer-events: none; |
| | | content: ''; |
| | | width: 100%; |
| | | height: 50px; |
| | | position: absolute; |
| | | left: 0; |
| | | bottom: 0; |
| | | background: linear-gradient(transparent 0px, var(--light)); |
| | | } |
| | | } |
| | |
| | | $pageWidth: 800px; |
| | | $pageWidth: 750px; |
| | | $mobileBreakpoint: 600px; |
| | | $tabletBreakpoint: 1200px; |
| | | $sidePanelWidth: 400px; |
| | | $topSpacing: 6rem; |
| | | $fullPageWidth: $pageWidth + 2 * $sidePanelWidth |