fix indexing causing main thread freeze, various polish
3 files added
30 files modified
| | |
| | | "mdast-util-find-and-replace": "^2.2.2", |
| | | "mdast-util-to-string": "^3.2.0", |
| | | "micromorph": "^0.4.5", |
| | | "plausible-tracker": "^0.3.8", |
| | | "preact": "^10.14.1", |
| | | "preact-render-to-string": "^6.0.3", |
| | | "pretty-time": "^1.1.0", |
| | |
| | | "url": "https://github.com/sponsors/jonschlinkert" |
| | | } |
| | | }, |
| | | "node_modules/plausible-tracker": { |
| | | "version": "0.3.8", |
| | | "resolved": "https://registry.npmjs.org/plausible-tracker/-/plausible-tracker-0.3.8.tgz", |
| | | "integrity": "sha512-lmOWYQ7s9KOUJ1R+YTOR3HrjdbxIS2Z4de0P/Jx2dQPteznJl2eX3tXxKClpvbfyGP59B5bbhW8ftN59HbbFSg==", |
| | | "engines": { |
| | | "node": ">=10" |
| | | } |
| | | }, |
| | | "node_modules/preact": { |
| | | "version": "10.15.1", |
| | | "resolved": "https://registry.npmjs.org/preact/-/preact-10.15.1.tgz", |
| | |
| | | "mdast-util-find-and-replace": "^2.2.2", |
| | | "mdast-util-to-string": "^3.2.0", |
| | | "micromorph": "^0.4.5", |
| | | "plausible-tracker": "^0.3.8", |
| | | "preact": "^10.14.1", |
| | | "preact-render-to-string": "^6.0.3", |
| | | "pretty-time": "^1.1.0", |
| | |
| | | left: [ |
| | | Component.PageTitle(), |
| | | Component.Search(), |
| | | Component.TableOfContents(), |
| | | Component.Darkmode() |
| | | Component.Darkmode(), |
| | | Component.DesktopOnly(Component.TableOfContents()), |
| | | ], |
| | | right: [ |
| | | Component.Graph(), |
| New file |
| | |
| | | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" |
| | | |
| | | export default ((component?: QuartzComponent) => { |
| | | if (component) { |
| | | const Component = component |
| | | function DesktopOnly(props: QuartzComponentProps) { |
| | | return <div class="desktop-only"> |
| | | <Component {...props} /> |
| | | </div> |
| | | } |
| | | |
| | | DesktopOnly.displayName = component.displayName |
| | | DesktopOnly.afterDOMLoaded = component?.afterDOMLoaded |
| | | DesktopOnly.beforeDOMLoaded = component?.beforeDOMLoaded |
| | | DesktopOnly.css = component?.css |
| | | return DesktopOnly |
| | | } else { |
| | | return () => <></> |
| | | } |
| | | }) satisfies QuartzComponentConstructor |
| | |
| | | drag: true, |
| | | zoom: true, |
| | | depth: 1, |
| | | scale: 1.2, |
| | | repelForce: 2, |
| | | centerForce: 1, |
| | | scale: 1.1, |
| | | repelForce: 0.5, |
| | | centerForce: 0.3, |
| | | linkDistance: 30, |
| | | fontSize: 0.6, |
| | | opacityScale: 3 |
| | | opacityScale: 1 |
| | | }, |
| | | globalGraph: { |
| | | drag: true, |
| | | zoom: true, |
| | | depth: -1, |
| | | scale: 1.2, |
| | | repelForce: 1, |
| | | centerForce: 1, |
| | | scale: 0.9, |
| | | repelForce: 0.5, |
| | | centerForce: 0.3, |
| | | linkDistance: 30, |
| | | fontSize: 0.5, |
| | | opacityScale: 3 |
| | | fontSize: 0.6, |
| | | opacityScale: 1 |
| | | } |
| | | } |
| | | |
| | |
| | | import { resolveToRoot } from "../path" |
| | | import { clientSideSlug, resolveToRoot } from "../path" |
| | | import { JSResourceToScriptElement } from "../resources" |
| | | import { QuartzComponentConstructor, QuartzComponentProps } from "./types" |
| | | |
| | | export default (() => { |
| | | function Head({ fileData, externalResources }: QuartzComponentProps) { |
| | | const slug = fileData.slug! |
| | | const slug = clientSideSlug(fileData.slug!) |
| | | const title = fileData.frontmatter?.title ?? "Untitled" |
| | | const description = fileData.description ?? "No description provided" |
| | | const { css, js } = externalResources |
| New file |
| | |
| | | import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types" |
| | | |
| | | export default ((component?: QuartzComponent) => { |
| | | if (component) { |
| | | const Component = component |
| | | function MobileOnly(props: QuartzComponentProps) { |
| | | return <div class="mobile-only"> |
| | | <Component {...props} /> |
| | | </div> |
| | | } |
| | | |
| | | MobileOnly.displayName = component.displayName |
| | | MobileOnly.afterDOMLoaded = component?.afterDOMLoaded |
| | | MobileOnly.beforeDOMLoaded = component?.beforeDOMLoaded |
| | | MobileOnly.css = component?.css |
| | | return MobileOnly |
| | | } else { |
| | | return () => <></> |
| | | } |
| | | }) satisfies QuartzComponentConstructor |
| | |
| | | |
| | | export function PageList({ fileData, allFiles }: QuartzComponentProps) { |
| | | const slug = fileData.slug! |
| | | return <ul class="section-ul popover-hint"> |
| | | return <ul class="section-ul"> |
| | | {allFiles.sort(byDateAndAlphabetical).map(page => { |
| | | const title = page.frontmatter?.title |
| | | const pageSlug = page.slug! |
| | |
| | | |
| | | function ReadingTime({ fileData }: QuartzComponentProps) { |
| | | const text = fileData.text |
| | | const isHomePage = fileData.slug === "index" |
| | | if (text && !isHomePage) { |
| | | if (text) { |
| | | const { text: timeTaken, words } = readingTime(text) |
| | | return <p class="reading-time">{words} words, {timeTaken}</p> |
| | | } else { |
| | |
| | | return null |
| | | } |
| | | |
| | | return <div> |
| | | return <div class="desktop-only"> |
| | | <button type="button" id="toc"> |
| | | <h3>Table of Contents</h3> |
| | | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="fold"> |
| | |
| | | |
| | | .tags > li { |
| | | display: inline-block; |
| | | white-space: nowrap; |
| | | margin: 0; |
| | | overflow-wrap: normal; |
| | | } |
| | |
| | | import Backlinks from "./Backlinks" |
| | | import Search from "./Search" |
| | | import Footer from "./Footer" |
| | | import DesktopOnly from "./DesktopOnly" |
| | | import MobileOnly from "./MobileOnly" |
| | | |
| | | export { |
| | | ArticleTitle, |
| | |
| | | Graph, |
| | | Backlinks, |
| | | Search, |
| | | Footer |
| | | Footer, |
| | | DesktopOnly, |
| | | MobileOnly |
| | | } |
| | |
| | | import style from '../styles/listPage.scss' |
| | | import { PageList } from "../PageList" |
| | | |
| | | function TagContent(props: QuartzComponentProps) { |
| | | function FolderContent(props: QuartzComponentProps) { |
| | | const { tree, fileData, allFiles } = props |
| | | const folderSlug = fileData.slug! |
| | | const allPagesInFolder = allFiles.filter(file => { |
| | |
| | | |
| | | // @ts-ignore |
| | | const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) |
| | | return <div> |
| | | return <div class="popover-hint"> |
| | | <article>{content}</article> |
| | | <hr/> |
| | | <p>{allPagesInFolder.length} items under this folder.</p> |
| | | <div> |
| | | <PageList {...listProps} /> |
| | | </div> |
| | | </div> |
| | | } |
| | | |
| | | TagContent.css = style + PageList.css |
| | | export default (() => TagContent) satisfies QuartzComponentConstructor |
| | | FolderContent.css = style + PageList.css |
| | | export default (() => FolderContent) satisfies QuartzComponentConstructor |
| | |
| | | import { toJsxRuntime } from "hast-util-to-jsx-runtime" |
| | | import style from '../styles/listPage.scss' |
| | | import { PageList } from "../PageList" |
| | | import { clientSideSlug } from "../../path" |
| | | |
| | | function TagContent(props: QuartzComponentProps) { |
| | | const { tree, fileData, allFiles } = props |
| | | const slug = fileData.slug |
| | | if (slug?.startsWith("tags/")) { |
| | | const tag = slug.slice("tags/".length) |
| | | |
| | | if (slug?.startsWith("tags/")) { |
| | | const tag = clientSideSlug(slug.slice("tags/".length)) |
| | | const allPagesWithTag = allFiles.filter(file => (file.frontmatter?.tags ?? []).includes(tag)) |
| | | const listProps = { |
| | | ...props, |
| | |
| | | |
| | | // @ts-ignore |
| | | const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' }) |
| | | return <div> |
| | | return <div class="popover-hint"> |
| | | <article>{content}</article> |
| | | <hr/> |
| | | <p>{allPagesWithTag.length} items with this tag.</p> |
| | | <div> |
| | | <PageList {...listProps} /> |
| | | </div> |
| | |
| | | css: [baseDir + "/index.css", ...staticResources.css], |
| | | js: [ |
| | | { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" }, |
| | | { loadTime: "afterDOMReady", contentType: "inline", spaPreserve: true, script: contentIndexScript }, |
| | | { loadTime: "beforeDOMReady", contentType: "inline", spaPreserve: true, script: contentIndexScript }, |
| | | ...staticResources.js, |
| | | { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" } |
| | | ] |
| | |
| | | .join("line") |
| | | .attr("class", "link") |
| | | .attr("stroke", "var(--lightgray)") |
| | | .attr("stroke-width", 2) |
| | | .attr("stroke-width", 1) |
| | | |
| | | // svg groups |
| | | const graphNode = svg.append("g").selectAll("g").data(graphData.nodes).enter().append("g") |
| | | |
| | | // calculate radius |
| | | // calculate color |
| | | const color = (d: NodeData) => { |
| | | const isCurrent = d.id === slug |
| | | if (isCurrent) { |
| | |
| | | neighbourNodes.transition().duration(200).attr("fill", color) |
| | | |
| | | // highlight links |
| | | linkNodes.transition().duration(200).attr("stroke", "var(--gray)") |
| | | linkNodes |
| | | .transition() |
| | | .duration(200) |
| | | .attr("stroke", "var(--gray)") |
| | | .attr("stroke-width", 1) |
| | | |
| | | |
| | | const bigFont = fontSize * 1.5 |
| | | |
| | |
| | | const labels = graphNode |
| | | .append("text") |
| | | .attr("dx", 0) |
| | | .attr("dy", (d) => nodeRadius(d) + 8 + "px") |
| | | .attr("dy", (d) => nodeRadius(d) - 8 + "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) |
| | |
| | | }) |
| | | } |
| | | |
| | | async function renderGlobalGraph() { |
| | | function renderGlobalGraph() { |
| | | const slug = document.body.dataset["slug"]! |
| | | await renderGraph("global-graph-container", slug) |
| | | const container = document.getElementById("global-graph-outer") |
| | | container?.classList.add("active") |
| | | |
| | | renderGraph("global-graph-container", slug) |
| | | |
| | | function hideGlobalGraph() { |
| | | container?.classList.remove("active") |
| | |
| | | ) |
| | | } |
| | | |
| | | document.addEventListener("nav", () => { |
| | | const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[] |
| | | const p = new DOMParser() |
| | | for (const link of links) { |
| | | link.addEventListener("mouseenter", async ({ clientX, clientY }) => { |
| | | async function mouseEnterHandler(this: HTMLLinkElement, { clientX, clientY }: { clientX: number, clientY: number }) { |
| | | const link = this |
| | | async function setPosition(popoverElement: HTMLElement) { |
| | | const { x, y } = await computePosition(link, popoverElement, { |
| | | middleware: [ |
| | |
| | | }) |
| | | } |
| | | |
| | | if (link.dataset.fetchedPopover === "true") { |
| | | // dont refetch if there's already a popover |
| | | if ([...link.children].some(child => child.classList.contains("popover"))) { |
| | | return setPosition(link.lastChild as HTMLElement) |
| | | } |
| | | |
| | |
| | | |
| | | setPosition(popoverElement) |
| | | link.appendChild(popoverElement) |
| | | link.dataset.fetchedPopover = "true" |
| | | |
| | | if (hash !== "") { |
| | | const heading = popoverInner.querySelector(hash) as HTMLElement | null |
| | |
| | | popoverInner.scroll({ top: heading.offsetTop - 12, behavior: 'instant' }) |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | |
| | | document.addEventListener("nav", () => { |
| | | const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[] |
| | | for (const link of links) { |
| | | link.removeEventListener("mouseenter", mouseEnterHandler) |
| | | link.addEventListener("mouseenter", mouseEnterHandler) |
| | | } |
| | | }) |
| | |
| | | |
| | | const contextWindowWords = 30 |
| | | function highlight(searchTerm: string, text: string, trim?: boolean) { |
| | | const tokenizedTerms = searchTerm.split(/\s+/).filter(t => t !== "") |
| | | // try to highlight longest tokens first |
| | | const tokenizedTerms = searchTerm.split(/\s+/).filter(t => t !== "").sort((a, b) => b.length - a.length) |
| | | let tokenizedText = text |
| | | .split(/\s+/) |
| | | .filter(t => t !== "") |
| | |
| | | // see if this tok is prefixed by any search terms |
| | | for (const searchTok of tokenizedTerms) { |
| | | if (tok.toLowerCase().includes(searchTok.toLowerCase())) { |
| | | const regex = new RegExp(searchTok, "gi") |
| | | const regex = new RegExp(searchTok.toLowerCase(), "gi") |
| | | return tok.replace(regex, `<span class="highlight">$&</span>`) |
| | | } |
| | | } |
| | |
| | | }) |
| | | |
| | | for (const [slug, fileData] of Object.entries<ContentDetails>(data)) { |
| | | index.add({ |
| | | await index.addAsync(slug, { |
| | | slug, |
| | | title: fileData.title, |
| | | content: fileData.content |
| | |
| | | displayResults(finalResults) |
| | | } |
| | | |
| | | |
| | | document.removeEventListener("keydown", shortcutHandler) |
| | | document.addEventListener("keydown", shortcutHandler) |
| | | searchIcon?.removeEventListener("click", showSearch) |
| | |
| | | position: relative; |
| | | width: 20px; |
| | | height: 20px; |
| | | margin: 1rem; |
| | | |
| | | & > .toggle { |
| | | display: none; |
| | |
| | | top: 0; |
| | | width: 100vw; |
| | | height: 100%; |
| | | overflow: scroll; |
| | | backdrop-filter: blur(4px); |
| | | display: none; |
| | | overflow: hidden; |
| | | |
| | | &.active { |
| | | display: inline-block; |
| | |
| | | @use "../../styles/variables.scss" as *; |
| | | |
| | | ul.section-ul { |
| | | list-style: none; |
| | | margin-top: 2em; |
| | |
| | | display: grid; |
| | | grid-template-columns: 6em 3fr 1fr; |
| | | |
| | | @media all and (max-width: 600px) { |
| | | @media all and (max-width: $mobileBreakpoint) { |
| | | & > .tags { |
| | | display: none; |
| | | } |
| | |
| | | margin-left: 1rem; |
| | | } |
| | | |
| | | & > .desc a { |
| | | & > .desc > h3 > a { |
| | | background-color: transparent; |
| | | } |
| | | |
| | |
| | | @use "../../styles/variables.scss" as *; |
| | | |
| | | @keyframes dropin { |
| | | 0% { |
| | | opacity: 0; |
| | |
| | | opacity: 0; |
| | | transition: opacity 0.3s ease, visibility 0.3s ease; |
| | | |
| | | @media all and (max-width: 600px) { |
| | | @media all and (max-width: $mobileBreakpoint) { |
| | | display: none !important; |
| | | } |
| | | } |
| | |
| | | @use "../../styles/variables.scss" as *; |
| | | |
| | | .search { |
| | | min-width: 5rem; |
| | | max-width: 14rem; |
| | |
| | | margin-left: auto; |
| | | margin-right: auto; |
| | | |
| | | @media all and (max-width: 1200px) { |
| | | @media all and (max-width: $tabletBreakpoint) { |
| | | width: 90%; |
| | | } |
| | | |
| | |
| | | |
| | | // on the client, 'index' isn't ever rendered so we should clean it up |
| | | export function clientSideSlug(fp: string): string { |
| | | // remove index |
| | | if (fp.endsWith("index")) { |
| | | fp = fp.slice(0, -"index".length) |
| | | } |
| | | |
| | | // remove trailing slash |
| | | if (fp.endsWith("/")) { |
| | | fp = fp.slice(0, -1) |
| | | } |
| | | |
| | | return fp |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | export function slugify(s: string): string { |
| | | const [fp, anchor] = s.split("#", 2) |
| | | let [fp, anchor] = s.split("#", 2) |
| | | const sluggedAnchor = anchor === undefined ? "" : "#" + slugAnchor(anchor) |
| | | const withoutFileExt = fp.replace(new RegExp(path.extname(fp) + '$'), '') |
| | | const rawSlugSegments = withoutFileExt.split(path.sep) |
| | |
| | | interface Options { |
| | | enableSiteMap: boolean |
| | | enableRSS: boolean |
| | | includeEmptyFiles: boolean |
| | | } |
| | | |
| | | const defaultOptions: Options = { |
| | | enableSiteMap: true, |
| | | enableRSS: true, |
| | | includeEmptyFiles: false, |
| | | } |
| | | |
| | | function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string { |
| | |
| | | </rss>` |
| | | } |
| | | |
| | | export const ContentIndex: QuartzEmitterPlugin<Options> = (opts) => { |
| | | export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => { |
| | | opts = { ...defaultOptions, ...opts } |
| | | return { |
| | | name: "ContentIndex", |
| | |
| | | for (const [_tree, file] of content) { |
| | | const slug = file.data.slug! |
| | | const date = file.data.dates?.modified ?? new Date() |
| | | if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) { |
| | | linkIndex.set(slug, { |
| | | title: file.data.frontmatter?.title!, |
| | | links: file.data.links ?? [], |
| | |
| | | description: file.data.description ?? "" |
| | | }) |
| | | } |
| | | } |
| | | |
| | | if (opts?.enableSiteMap) { |
| | | await emit({ |
| | |
| | | return [slug, content] |
| | | }) |
| | | ) |
| | | |
| | | await emit({ |
| | | content: JSON.stringify(simplifiedIndex), |
| | | slug: fp, |
| | |
| | | import { ProcessedContent, defaultProcessedContent } from "../vfile" |
| | | import { FullPageLayout } from "../../cfg" |
| | | import path from "path" |
| | | import { clientSideSlug } from "../../path" |
| | | |
| | | export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { |
| | | if (!opts) { |
| | |
| | | ]))) |
| | | |
| | | for (const [tree, file] of content) { |
| | | const slug = file.data.slug! |
| | | const slug = clientSideSlug(file.data.slug!) |
| | | if (folders.has(slug)) { |
| | | folderDescriptions[slug] = [tree, file] |
| | | } |
| | |
| | | import { pageResources, renderPage } from "../../components/renderPage" |
| | | import { ProcessedContent, defaultProcessedContent } from "../vfile" |
| | | import { FullPageLayout } from "../../cfg" |
| | | import { clientSideSlug } from "../../path" |
| | | |
| | | export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => { |
| | | if (!opts) { |
| | |
| | | ]))) |
| | | |
| | | for (const [tree, file] of content) { |
| | | const slug = file.data.slug! |
| | | const slug = clientSideSlug(file.data.slug!) |
| | | if (slug.startsWith("tags/")) { |
| | | const tag = slug.slice("tags/".length) |
| | | if (tags.has(tag)) { |
| | |
| | | } |
| | | } |
| | | |
| | | const componentResources: ComponentResources = { |
| | | css: [], |
| | | beforeDOMLoaded: [], |
| | | afterDOMLoaded: [] |
| | | const componentResources = { |
| | | css: new Set<string>(), |
| | | beforeDOMLoaded: new Set<string>(), |
| | | afterDOMLoaded: new Set<string>() |
| | | } |
| | | |
| | | for (const component of allComponents) { |
| | | const { css, beforeDOMLoaded, afterDOMLoaded } = component |
| | | if (css) { |
| | | componentResources.css.push(css) |
| | | componentResources.css.add(css) |
| | | } |
| | | if (beforeDOMLoaded) { |
| | | componentResources.beforeDOMLoaded.push(beforeDOMLoaded) |
| | | componentResources.beforeDOMLoaded.add(beforeDOMLoaded) |
| | | } |
| | | if (afterDOMLoaded) { |
| | | componentResources.afterDOMLoaded.push(afterDOMLoaded) |
| | | componentResources.afterDOMLoaded.add(afterDOMLoaded) |
| | | } |
| | | } |
| | | |
| | | return componentResources |
| | | return { |
| | | css: [...componentResources.css], |
| | | beforeDOMLoaded: [...componentResources.beforeDOMLoaded], |
| | | afterDOMLoaded: [...componentResources.afterDOMLoaded] |
| | | } |
| | | } |
| | | |
| | | function joinScripts(scripts: string[]): string { |
| | |
| | | for (const transformer of plugins.transformers) { |
| | | const res = transformer.externalResources ? transformer.externalResources() : {} |
| | | if (res?.js) { |
| | | staticResources.js = staticResources.js.concat(res.js) |
| | | staticResources.js.push(...res.js) |
| | | } |
| | | if (res?.css) { |
| | | staticResources.css = staticResources.css.concat(res.css) |
| | | staticResources.css.push(...res.css) |
| | | } |
| | | } |
| | | |
| | |
| | | import { QUARTZ, slugify } from "../path" |
| | | import { globbyStream } from "globby" |
| | | import chalk from "chalk" |
| | | import { googleFontHref } from '../theme' |
| | | |
| | | // @ts-ignore |
| | | import spaRouterScript from '../components/scripts/spa.inline' |
| | |
| | | import popoverScript from '../components/scripts/popover.inline' |
| | | import popoverStyle from '../components/styles/popover.scss' |
| | | import { StaticResources } from "../resources" |
| | | import { QuartzLogger } from "../log" |
| | | import { googleFontHref } from "../theme" |
| | | |
| | | function addGlobalPageResources(cfg: GlobalConfiguration, staticResources: StaticResources, componentResources: ComponentResources) { |
| | | // font and other resources |
| | | staticResources.css.push(googleFontHref(cfg.theme)) |
| | | |
| | | // popovers |
| | |
| | | |
| | | export async function emitContent(contentFolder: string, output: string, cfg: QuartzConfig, content: ProcessedContent[], verbose: boolean) { |
| | | const perf = new PerfTimer() |
| | | const log = new QuartzLogger(verbose) |
| | | |
| | | log.start(`Emitting output files`) |
| | | const emit: EmitCallback = async ({ slug, ext, content }) => { |
| | | const pathToPage = path.join(output, slug + ext) |
| | | const dir = path.dirname(pathToPage) |
| | |
| | | |
| | | // component specific scripts and styles |
| | | const componentResources = getComponentResources(cfg.plugins) |
| | | |
| | | // important that this goes *after* component scripts |
| | | // as the "nav" event gets triggered here and we should make sure |
| | | // that everyone else had the chance to register a listener for it |
| | |
| | | } |
| | | } |
| | | |
| | | console.log(`Emitted ${emittedFiles} files to \`${output}\` in ${perf.timeSince()}`) |
| | | log.success(`Emitted ${emittedFiles} files to \`${output}\` in ${perf.timeSince()}`) |
| | | } |
| | |
| | | @import "./syntax.scss"; |
| | | @import "./callouts.scss"; |
| | | @use "./syntax.scss"; |
| | | @use "./callouts.scss"; |
| | | @use "./variables.scss" as *; |
| | | |
| | | html { |
| | | scroll-behavior: smooth; |
| | |
| | | box-sizing: border-box; |
| | | background-color: var(--light); |
| | | font-family: var(--bodyFont); |
| | | --pageWidth: 800px; |
| | | --sidePanelWidth: 400px; |
| | | --topSpacing: 6rem; |
| | | } |
| | | |
| | | .text-highlight { |
| | |
| | | |
| | | .page { |
| | | & > .page-header { |
| | | max-width: var(--pageWidth); |
| | | margin: var(--topSpacing) auto 0 auto; |
| | | max-width: $pageWidth; |
| | | margin: $topSpacing auto 0 auto; |
| | | } |
| | | |
| | | & > #quartz-body { |
| | |
| | | |
| | | & .left, & .right { |
| | | flex: 1; |
| | | width: calc(calc(100vw - var(--pageWidth)) / 2); |
| | | width: calc(calc(100vw - $pageWidth) / 2); |
| | | } |
| | | |
| | | & .left-inner, & .right-inner { |
| | |
| | | flex-direction: column; |
| | | gap: 2rem; |
| | | top: 0; |
| | | width: var(--sidePanelWidth); |
| | | margin-top: calc(var(--topSpacing)); |
| | | width: $sidePanelWidth; |
| | | margin-top: $topSpacing; |
| | | box-sizing: border-box; |
| | | padding: 0 4rem; |
| | | position: fixed; |
| | | } |
| | | |
| | | & .left-inner { |
| | | left: calc(calc(100vw - var(--pageWidth)) / 2 - var(--sidePanelWidth)); |
| | | left: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth); |
| | | } |
| | | |
| | | & .right-inner { |
| | | right: calc(calc(100vw - var(--pageWidth)) / 2 - var(--sidePanelWidth)); |
| | | right: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth); |
| | | } |
| | | |
| | | & .center { |
| | | width: var(--pageWidth); |
| | | width: $pageWidth; |
| | | margin: 0 auto; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .desktop-only { |
| | | display: initial; |
| | | @media all and (max-width: ($pageWidth + 2 * $sidePanelWidth)) { |
| | | display: none; |
| | | } |
| | | } |
| | | |
| | | .mobile-only { |
| | | display: none; |
| | | @media all and (max-width: ($pageWidth + 2 * $sidePanelWidth)) { |
| | | display: initial; |
| | | } |
| | | } |
| | | |
| | | .page { |
| | | @media all and (max-width: 1200px) { |
| | | @media all and (max-width: $tabletBreakpoint) { |
| | | margin: 25px 5vw; |
| | | & .left, & .right { |
| | | padding: 0; |
| New file |
| | |
| | | $pageWidth: 800px; |
| | | $mobileBreakpoint: 600px; |
| | | $tabletBreakpoint: 1200px; |
| | | $sidePanelWidth: 400px; |
| | | $topSpacing: 6rem; |
| | |
| | | } |
| | | } |
| | | |
| | | const DEFAULT_SANS_SERIF = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Helvetica, Arial, sans-serif" |
| | | const DEFAULT_MONO = "ui-monospace, SFMono-Regular, SF Mono, Menlo, monospace" |
| | | export function googleFontHref(theme: Theme) { |
| | | const { code, header, body } = theme.typography |
| | | return `https://fonts.googleapis.com/css2?family=${code}&family=${header}:wght@400;700&family=${body}:ital,wght@0,400;0,600;1,400;1,600&display=swap` |
| | |
| | | --tertiary: ${theme.colors.lightMode.tertiary}; |
| | | --highlight: ${theme.colors.lightMode.highlight}; |
| | | |
| | | --headerFont: ${theme.typography.header}; |
| | | --bodyFont: ${theme.typography.body}; |
| | | --codeFont: ${theme.typography.code}; |
| | | --headerFont: ${theme.typography.header}, ${DEFAULT_SANS_SERIF}; |
| | | --bodyFont: ${theme.typography.body}, ${DEFAULT_SANS_SERIF}; |
| | | --codeFont: ${theme.typography.code}, ${DEFAULT_MONO}; |
| | | } |
| | | |
| | | :root[saved-theme="dark"] { |