mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-24 15:05:42 -05:00
Merge branch 'v4' of https://github.com/jackyzha0/quartz
This commit is contained in:
commit
0acf449503
@ -29,6 +29,7 @@ Some common frontmatter fields that are natively supported by Quartz:
|
|||||||
|
|
||||||
- `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title.
|
- `title`: Title of the page. If it isn't provided, Quartz will use the name of the file as the title.
|
||||||
- `description`: Description of the page used for link previews.
|
- `description`: Description of the page used for link previews.
|
||||||
|
- `permalink`: A custom URL for the page that will remain constant even if the path to the file changes.
|
||||||
- `aliases`: Other names for this note. This is a list of strings.
|
- `aliases`: Other names for this note. This is a list of strings.
|
||||||
- `tags`: Tags for this note.
|
- `tags`: Tags for this note.
|
||||||
- `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz.
|
- `draft`: Whether to publish the page or not. This is one way to make [[private pages|pages private]] in Quartz.
|
||||||
|
|||||||
@ -6,7 +6,6 @@ draft: true
|
|||||||
|
|
||||||
- static dead link detection
|
- static dead link detection
|
||||||
- cursor chat extension
|
- cursor chat extension
|
||||||
- https://giscus.app/ extension
|
|
||||||
- sidenotes? https://github.com/capnfabs/paperesque
|
- sidenotes? https://github.com/capnfabs/paperesque
|
||||||
- direct match in search using double quotes
|
- direct match in search using double quotes
|
||||||
- https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI
|
- https://help.obsidian.md/Advanced+topics/Using+Obsidian+URI
|
||||||
|
|||||||
@ -201,7 +201,7 @@ build:
|
|||||||
paths:
|
paths:
|
||||||
- public
|
- public
|
||||||
tags:
|
tags:
|
||||||
- docker
|
- gitlab-org-docker
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
|
|||||||
@ -26,5 +26,7 @@ Want to see what Quartz can do? Here are some cool community gardens:
|
|||||||
- [Gatekeeper Wiki](https://www.gatekeeper.wiki)
|
- [Gatekeeper Wiki](https://www.gatekeeper.wiki)
|
||||||
- [Ellie's Notes](https://ellie.wtf)
|
- [Ellie's Notes](https://ellie.wtf)
|
||||||
- [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja)
|
- [🥷🏻🌳🍃 Computer Science & Thinkering Garden](https://notes.yxy.ninja)
|
||||||
|
- [Eledah's Crystalline](https://blog.eledah.ir/)
|
||||||
|
- [🌓 Projects & Privacy - FOSS, tech, law](https://be-far.com)
|
||||||
|
|
||||||
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!
|
If you want to see your own on here, submit a [Pull Request adding yourself to this file](https://github.com/jackyzha0/quartz/blob/v4/docs/showcase.md)!
|
||||||
|
|||||||
6900
package-lock.json
generated
Normal file
6900
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@
|
|||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"description": "🌱 publish your digital garden and notes as a website",
|
"description": "🌱 publish your digital garden and notes as a website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "4.3.0",
|
"version": "4.3.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -68,6 +68,7 @@
|
|||||||
"mdast-util-to-hast": "^13.2.0",
|
"mdast-util-to-hast": "^13.2.0",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"micromorph": "^0.4.5",
|
"micromorph": "^0.4.5",
|
||||||
|
"pixi.js": "^8.3.3",
|
||||||
"preact": "^10.23.2",
|
"preact": "^10.23.2",
|
||||||
"preact-render-to-string": "^6.5.8",
|
"preact-render-to-string": "^6.5.8",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
|
|||||||
@ -52,11 +52,13 @@ export const PageList: QuartzComponent = ({
|
|||||||
return (
|
return (
|
||||||
<li class="section-li">
|
<li class="section-li">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
<div>
|
||||||
{page.dates && (
|
{page.dates && (
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
<Date date={getDate(cfg, page)!} locale={cfg.locale} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
<h3>
|
<h3>
|
||||||
<a
|
<a
|
||||||
|
|||||||
@ -51,7 +51,7 @@ const TableOfContents: QuartzComponent = ({
|
|||||||
<polyline points="6 9 12 15 18 9"></polyline>
|
<polyline points="6 9 12 15 18 9"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div id="toc-content">
|
<div id="toc-content" class={fileData.collapseToc ? "collapsed" : ""}>
|
||||||
<ul class="overflow">
|
<ul class="overflow">
|
||||||
{fileData.toc.map((tocEntry) => (
|
{fileData.toc.map((tocEntry) => (
|
||||||
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
|
||||||
|
|||||||
@ -13,7 +13,8 @@ const emitThemeChangeEvent = (theme: "light" | "dark") => {
|
|||||||
|
|
||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
const switchTheme = (e: Event) => {
|
const switchTheme = (e: Event) => {
|
||||||
const newTheme = (e.target as HTMLInputElement)?.checked ? "dark" : "light"
|
const newTheme =
|
||||||
|
document.documentElement.getAttribute("saved-theme") === "dark" ? "light" : "dark"
|
||||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
document.documentElement.setAttribute("saved-theme", newTheme)
|
||||||
localStorage.setItem("theme", newTheme)
|
localStorage.setItem("theme", newTheme)
|
||||||
emitThemeChangeEvent(newTheme)
|
emitThemeChangeEvent(newTheme)
|
||||||
@ -23,7 +24,6 @@ document.addEventListener("nav", () => {
|
|||||||
const newTheme = e.matches ? "dark" : "light"
|
const newTheme = e.matches ? "dark" : "light"
|
||||||
document.documentElement.setAttribute("saved-theme", newTheme)
|
document.documentElement.setAttribute("saved-theme", newTheme)
|
||||||
localStorage.setItem("theme", newTheme)
|
localStorage.setItem("theme", newTheme)
|
||||||
toggleSwitch.checked = e.matches
|
|
||||||
emitThemeChangeEvent(newTheme)
|
emitThemeChangeEvent(newTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,13 +13,27 @@ type NodeData = {
|
|||||||
id: SimpleSlug
|
id: SimpleSlug
|
||||||
text: string
|
text: string
|
||||||
tags: string[]
|
tags: string[]
|
||||||
} & d3.SimulationNodeDatum
|
} & SimulationNodeDatum
|
||||||
|
|
||||||
type LinkData = {
|
type SimpleLinkData = {
|
||||||
source: SimpleSlug
|
source: SimpleSlug
|
||||||
target: SimpleSlug
|
target: SimpleSlug
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LinkData = {
|
||||||
|
source: NodeData
|
||||||
|
target: NodeData
|
||||||
|
} & SimulationLinkDatum<NodeData>
|
||||||
|
|
||||||
|
type LinkRenderData = GraphicsInfo & {
|
||||||
|
simulationData: LinkData
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeRenderData = GraphicsInfo & {
|
||||||
|
simulationData: NodeData
|
||||||
|
label: Text
|
||||||
|
}
|
||||||
|
|
||||||
const localStorageKey = "graph-visited"
|
const localStorageKey = "graph-visited"
|
||||||
function getVisited(): Set<SimpleSlug> {
|
function getVisited(): Set<SimpleSlug> {
|
||||||
return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]"))
|
return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]"))
|
||||||
@ -31,6 +45,11 @@ function addToVisited(slug: SimpleSlug) {
|
|||||||
localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
|
localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TweenNode = {
|
||||||
|
update: (time: number) => void
|
||||||
|
stop: () => void
|
||||||
|
}
|
||||||
|
|
||||||
async function renderGraph(container: string, fullSlug: FullSlug) {
|
async function renderGraph(container: string, fullSlug: FullSlug) {
|
||||||
const slug = simplifySlug(fullSlug)
|
const slug = simplifySlug(fullSlug)
|
||||||
const visited = getVisited()
|
const visited = getVisited()
|
||||||
@ -51,7 +70,7 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
removeTags,
|
removeTags,
|
||||||
showTags,
|
showTags,
|
||||||
focusOnHover,
|
focusOnHover,
|
||||||
} = JSON.parse(graph.dataset["cfg"]!)
|
} = JSON.parse(graph.dataset["cfg"]!) as D3Config
|
||||||
|
|
||||||
const data: Map<SimpleSlug, ContentDetails> = new Map(
|
const data: Map<SimpleSlug, ContentDetails> = new Map(
|
||||||
Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
|
Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
|
||||||
@ -59,10 +78,11 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
v,
|
v,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
const links: LinkData[] = []
|
const links: SimpleLinkData[] = []
|
||||||
const tags: SimpleSlug[] = []
|
const tags: SimpleSlug[] = []
|
||||||
|
|
||||||
const validLinks = new Set(data.keys())
|
const validLinks = new Set(data.keys())
|
||||||
|
|
||||||
|
const tweens = new Map<string, TweenNode>()
|
||||||
for (const [source, details] of data.entries()) {
|
for (const [source, details] of data.entries()) {
|
||||||
const outgoing = details.links ?? []
|
const outgoing = details.links ?? []
|
||||||
|
|
||||||
@ -125,20 +145,15 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
const simulation: d3.Simulation<NodeData, LinkData> = d3
|
// we virtualize the simulation and use pixi to actually render it
|
||||||
.forceSimulation(graphData.nodes)
|
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
|
||||||
.force("charge", d3.forceManyBody().strength(-100 * repelForce))
|
.force("charge", forceManyBody().strength(-100 * repelForce))
|
||||||
.force(
|
.force("center", forceCenter().strength(centerForce))
|
||||||
"link",
|
.force("link", forceLink(graphData.links).distance(linkDistance))
|
||||||
d3
|
.force("collide", forceCollide<NodeData>((n) => nodeRadius(n)).iterations(3))
|
||||||
.forceLink(graphData.links)
|
|
||||||
.id((d: any) => d.id)
|
|
||||||
.distance(linkDistance),
|
|
||||||
)
|
|
||||||
.force("center", d3.forceCenter().strength(centerForce))
|
|
||||||
|
|
||||||
const height = Math.max(graph.offsetHeight, 250)
|
|
||||||
const width = graph.offsetWidth
|
const width = graph.offsetWidth
|
||||||
|
const height = Math.max(graph.offsetHeight, 250)
|
||||||
|
|
||||||
const svg = d3
|
const svg = d3
|
||||||
.select<HTMLElement, NodeData>("#" + container)
|
.select<HTMLElement, NodeData>("#" + container)
|
||||||
@ -174,38 +189,82 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
const color = (d: NodeData) => {
|
const color = (d: NodeData) => {
|
||||||
const isCurrent = d.id === slug
|
const isCurrent = d.id === slug
|
||||||
if (isCurrent) {
|
if (isCurrent) {
|
||||||
return "var(--secondary)"
|
return computedStyleMap["--secondary"]
|
||||||
} else if (visited.has(d.id) || d.id.startsWith("tags/")) {
|
} else if (visited.has(d.id) || d.id.startsWith("tags/")) {
|
||||||
return "var(--tertiary)"
|
return computedStyleMap["--tertiary"]
|
||||||
} else {
|
} else {
|
||||||
return "var(--gray)"
|
return computedStyleMap["--gray"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const drag = (simulation: d3.Simulation<NodeData, LinkData>) => {
|
function nodeRadius(d: NodeData) {
|
||||||
function dragstarted(event: any, d: NodeData) {
|
const numLinks = graphData.links.filter(
|
||||||
if (!event.active) simulation.alphaTarget(1).restart()
|
(l) => l.source.id === d.id || l.target.id === d.id,
|
||||||
d.fx = d.x
|
).length
|
||||||
d.fy = d.y
|
return 2 + Math.sqrt(numLinks)
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragged(event: any, d: NodeData) {
|
let hoveredNodeId: string | null = null
|
||||||
d.fx = event.x
|
let hoveredNeighbours: Set<string> = new Set()
|
||||||
d.fy = event.y
|
const linkRenderData: LinkRenderData[] = []
|
||||||
|
const nodeRenderData: NodeRenderData[] = []
|
||||||
|
function updateHoverInfo(newHoveredId: string | null) {
|
||||||
|
hoveredNodeId = newHoveredId
|
||||||
|
|
||||||
|
if (newHoveredId === null) {
|
||||||
|
hoveredNeighbours = new Set()
|
||||||
|
for (const n of nodeRenderData) {
|
||||||
|
n.active = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragended(event: any, d: NodeData) {
|
for (const l of linkRenderData) {
|
||||||
if (!event.active) simulation.alphaTarget(0)
|
l.active = false
|
||||||
d.fx = null
|
}
|
||||||
d.fy = null
|
} else {
|
||||||
|
hoveredNeighbours = new Set()
|
||||||
|
for (const l of linkRenderData) {
|
||||||
|
const linkData = l.simulationData
|
||||||
|
if (linkData.source.id === newHoveredId || linkData.target.id === newHoveredId) {
|
||||||
|
hoveredNeighbours.add(linkData.source.id)
|
||||||
|
hoveredNeighbours.add(linkData.target.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const noop = () => {}
|
l.active = linkData.source.id === newHoveredId || linkData.target.id === newHoveredId
|
||||||
return d3
|
}
|
||||||
.drag<Element, NodeData>()
|
|
||||||
.on("start", enableDrag ? dragstarted : noop)
|
for (const n of nodeRenderData) {
|
||||||
.on("drag", enableDrag ? dragged : noop)
|
n.active = hoveredNeighbours.has(n.simulationData.id)
|
||||||
.on("end", enableDrag ? dragended : noop)
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dragStartTime = 0
|
||||||
|
let dragging = false
|
||||||
|
|
||||||
|
function renderLinks() {
|
||||||
|
tweens.get("link")?.stop()
|
||||||
|
const tweenGroup = new TweenGroup()
|
||||||
|
|
||||||
|
for (const l of linkRenderData) {
|
||||||
|
let alpha = 1
|
||||||
|
|
||||||
|
// if we are hovering over a node, we want to highlight the immediate neighbours
|
||||||
|
// with full alpha and the rest with default alpha
|
||||||
|
if (hoveredNodeId) {
|
||||||
|
alpha = l.active ? 1 : 0.2
|
||||||
|
}
|
||||||
|
|
||||||
|
l.color = l.active ? computedStyleMap["--gray"] : computedStyleMap["--lightgray"]
|
||||||
|
tweenGroup.add(new Tweened<LinkRenderData>(l).to({ alpha }, 200))
|
||||||
|
}
|
||||||
|
|
||||||
|
tweenGroup.getAll().forEach((tw) => tw.start())
|
||||||
|
tweens.set("link", {
|
||||||
|
update: tweenGroup.update.bind(tweenGroup),
|
||||||
|
stop() {
|
||||||
|
tweenGroup.getAll().forEach((tw) => tw.stop())
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeRadius(d: NodeData) {
|
function nodeRadius(d: NodeData) {
|
||||||
@ -215,19 +274,12 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
return 2 + Math.sqrt(numLinks)
|
return 2 + Math.sqrt(numLinks)
|
||||||
}
|
}
|
||||||
|
|
||||||
let connectedNodes: SimpleSlug[] = []
|
tweenGroup.getAll().forEach((tw) => tw.start())
|
||||||
|
tweens.set("label", {
|
||||||
// draw individual nodes
|
update: tweenGroup.update.bind(tweenGroup),
|
||||||
const node = graphNode
|
stop() {
|
||||||
.append("circle")
|
tweenGroup.getAll().forEach((tw) => tw.stop())
|
||||||
.attr("class", "node")
|
},
|
||||||
.attr("id", (d) => d.id)
|
|
||||||
.attr("r", nodeRadius)
|
|
||||||
.attr("fill", color)
|
|
||||||
.style("cursor", "pointer")
|
|
||||||
.on("click", (_, d) => {
|
|
||||||
const targ = resolveRelative(fullSlug, d.id)
|
|
||||||
window.spaNavigate(new URL(targ, window.location.toString()))
|
|
||||||
})
|
})
|
||||||
.on("mouseover", function (_, d) {
|
.on("mouseover", function (_, d) {
|
||||||
const currentId = d.id
|
const currentId = d.id
|
||||||
@ -325,35 +377,104 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
.style("opacity", d3.select(parent).select("text").attr("opacityOld"))
|
.style("opacity", d3.select(parent).select("text").attr("opacityOld"))
|
||||||
.style("font-size", fontSize + "em")
|
.style("font-size", fontSize + "em")
|
||||||
})
|
})
|
||||||
// @ts-ignore
|
.circle(0, 0, nodeRadius(n))
|
||||||
.call(drag(simulation))
|
.fill({ color: isTagNode ? computedStyleMap["--light"] : color(n) })
|
||||||
|
.stroke({ width: isTagNode ? 2 : 0, color: color(n) })
|
||||||
|
.on("pointerover", (e) => {
|
||||||
|
updateHoverInfo(e.target.label)
|
||||||
|
oldLabelOpacity = label.alpha
|
||||||
|
if (!dragging) {
|
||||||
|
renderPixiFromD3()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("pointerleave", () => {
|
||||||
|
updateHoverInfo(null)
|
||||||
|
label.alpha = oldLabelOpacity
|
||||||
|
if (!dragging) {
|
||||||
|
renderPixiFromD3()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// make tags hollow circles
|
nodesContainer.addChild(gfx)
|
||||||
node
|
labelsContainer.addChild(label)
|
||||||
.filter((d) => d.id.startsWith("tags/"))
|
|
||||||
.attr("stroke", color)
|
|
||||||
.attr("stroke-width", 2)
|
|
||||||
.attr("fill", "var(--light)")
|
|
||||||
|
|
||||||
// draw labels
|
const nodeRenderDatum: NodeRenderData = {
|
||||||
const labels = graphNode
|
simulationData: n,
|
||||||
.append("text")
|
gfx,
|
||||||
.attr("dx", 0)
|
label,
|
||||||
.attr("dy", (d) => -nodeRadius(d) + "px")
|
color: color(n),
|
||||||
.attr("text-anchor", "middle")
|
alpha: 1,
|
||||||
.text((d) => d.text)
|
active: false,
|
||||||
.style("opacity", (opacityScale - 1) / 3.75)
|
}
|
||||||
.style("pointer-events", "none")
|
|
||||||
.style("font-size", fontSize + "em")
|
nodeRenderData.push(nodeRenderDatum)
|
||||||
.raise()
|
}
|
||||||
// @ts-ignore
|
|
||||||
.call(drag(simulation))
|
for (const l of graphData.links) {
|
||||||
|
const gfx = new Graphics({ interactive: false, eventMode: "none" })
|
||||||
|
linkContainer.addChild(gfx)
|
||||||
|
|
||||||
|
const linkRenderDatum: LinkRenderData = {
|
||||||
|
simulationData: l,
|
||||||
|
gfx,
|
||||||
|
color: computedStyleMap["--lightgray"],
|
||||||
|
alpha: 1,
|
||||||
|
active: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
linkRenderData.push(linkRenderDatum)
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentTransform = zoomIdentity
|
||||||
|
if (enableDrag) {
|
||||||
|
select<HTMLCanvasElement, NodeData | undefined>(app.canvas).call(
|
||||||
|
drag<HTMLCanvasElement, NodeData | undefined>()
|
||||||
|
.container(() => app.canvas)
|
||||||
|
.subject(() => graphData.nodes.find((n) => n.id === hoveredNodeId))
|
||||||
|
.on("start", function dragstarted(event) {
|
||||||
|
if (!event.active) simulation.alphaTarget(1).restart()
|
||||||
|
event.subject.fx = event.subject.x
|
||||||
|
event.subject.fy = event.subject.y
|
||||||
|
event.subject.__initialDragPos = {
|
||||||
|
x: event.subject.x,
|
||||||
|
y: event.subject.y,
|
||||||
|
fx: event.subject.fx,
|
||||||
|
fy: event.subject.fy,
|
||||||
|
}
|
||||||
|
dragStartTime = Date.now()
|
||||||
|
dragging = true
|
||||||
|
})
|
||||||
|
.on("drag", function dragged(event) {
|
||||||
|
const initPos = event.subject.__initialDragPos
|
||||||
|
event.subject.fx = initPos.x + (event.x - initPos.x) / currentTransform.k
|
||||||
|
event.subject.fy = initPos.y + (event.y - initPos.y) / currentTransform.k
|
||||||
|
})
|
||||||
|
.on("end", function dragended(event) {
|
||||||
|
if (!event.active) simulation.alphaTarget(0)
|
||||||
|
event.subject.fx = null
|
||||||
|
event.subject.fy = null
|
||||||
|
dragging = false
|
||||||
|
|
||||||
|
// if the time between mousedown and mouseup is short, we consider it a click
|
||||||
|
if (Date.now() - dragStartTime < 500) {
|
||||||
|
const node = graphData.nodes.find((n) => n.id === event.subject.id) as NodeData
|
||||||
|
const targ = resolveRelative(fullSlug, node.id)
|
||||||
|
window.spaNavigate(new URL(targ, window.location.toString()))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
for (const node of nodeRenderData) {
|
||||||
|
node.gfx.on("click", () => {
|
||||||
|
const targ = resolveRelative(fullSlug, node.simulationData.id)
|
||||||
|
window.spaNavigate(new URL(targ, window.location.toString()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// set panning
|
|
||||||
if (enableZoom) {
|
if (enableZoom) {
|
||||||
svg.call(
|
select<HTMLCanvasElement, NodeData>(app.canvas).call(
|
||||||
d3
|
zoom<HTMLCanvasElement, NodeData>()
|
||||||
.zoom<SVGSVGElement, NodeData>()
|
|
||||||
.extent([
|
.extent([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[width, height],
|
[width, height],
|
||||||
@ -363,46 +484,44 @@ async function renderGraph(container: string, fullSlug: FullSlug) {
|
|||||||
link.attr("transform", transform)
|
link.attr("transform", transform)
|
||||||
node.attr("transform", transform)
|
node.attr("transform", transform)
|
||||||
const scale = transform.k * opacityScale
|
const scale = transform.k * opacityScale
|
||||||
const scaledOpacity = Math.max((scale - 1) / 3.75, 0)
|
let scaleOpacity = Math.max((scale - 1) / 3.75, 0)
|
||||||
labels.attr("transform", transform).style("opacity", scaledOpacity)
|
const activeNodes = nodeRenderData.filter((n) => n.active).flatMap((n) => n.label)
|
||||||
|
|
||||||
|
for (const label of labelsContainer.children) {
|
||||||
|
if (!activeNodes.includes(label)) {
|
||||||
|
label.alpha = scaleOpacity
|
||||||
|
}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// progress the simulation
|
function animate(time: number) {
|
||||||
simulation.on("tick", () => {
|
for (const n of nodeRenderData) {
|
||||||
link
|
const { x, y } = n.simulationData
|
||||||
.attr("x1", (d: any) => d.source.x)
|
if (!x || !y) continue
|
||||||
.attr("y1", (d: any) => d.source.y)
|
n.gfx.position.set(x + width / 2, y + height / 2)
|
||||||
.attr("x2", (d: any) => d.target.x)
|
if (n.label) {
|
||||||
.attr("y2", (d: any) => d.target.y)
|
n.label.position.set(x + width / 2, y + height / 2)
|
||||||
node.attr("cx", (d: any) => d.x).attr("cy", (d: any) => d.y)
|
}
|
||||||
labels.attr("x", (d: any) => d.x).attr("y", (d: any) => d.y)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGlobalGraph() {
|
|
||||||
const slug = getFullSlug(window)
|
|
||||||
const container = document.getElementById("global-graph-outer")
|
|
||||||
const sidebar = container?.closest(".sidebar") as HTMLElement
|
|
||||||
container?.classList.add("active")
|
|
||||||
if (sidebar) {
|
|
||||||
sidebar.style.zIndex = "1"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderGraph("global-graph-container", slug)
|
for (const l of linkRenderData) {
|
||||||
|
const linkData = l.simulationData
|
||||||
function hideGlobalGraph() {
|
l.gfx.clear()
|
||||||
container?.classList.remove("active")
|
l.gfx.moveTo(linkData.source.x! + width / 2, linkData.source.y! + height / 2)
|
||||||
const graph = document.getElementById("global-graph-container")
|
l.gfx
|
||||||
if (sidebar) {
|
.lineTo(linkData.target.x! + width / 2, linkData.target.y! + height / 2)
|
||||||
sidebar.style.zIndex = "unset"
|
.stroke({ alpha: l.alpha, width: 1, color: l.color })
|
||||||
}
|
|
||||||
if (!graph) return
|
|
||||||
removeAllChildren(graph)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerEscapeHandler(container, hideGlobalGraph)
|
tweens.forEach((t) => t.update(time))
|
||||||
|
app.renderer.render(stage)
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
}
|
||||||
|
|
||||||
|
const graphAnimationFrameHandle = requestAnimationFrame(animate)
|
||||||
|
window.addCleanup(() => cancelAnimationFrame(graphAnimationFrameHandle))
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
@ -410,6 +529,48 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
addToVisited(simplifySlug(slug))
|
addToVisited(simplifySlug(slug))
|
||||||
await renderGraph("graph-container", slug)
|
await renderGraph("graph-container", slug)
|
||||||
|
|
||||||
|
// Function to re-render the graph when the theme changes
|
||||||
|
const handleThemeChange = () => {
|
||||||
|
renderGraph("graph-container", slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
// event listener for theme change
|
||||||
|
document.addEventListener("themechange", handleThemeChange)
|
||||||
|
|
||||||
|
// cleanup for the event listener
|
||||||
|
window.addCleanup(() => {
|
||||||
|
document.removeEventListener("themechange", handleThemeChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
const container = document.getElementById("global-graph-outer")
|
||||||
|
const sidebar = container?.closest(".sidebar") as HTMLElement
|
||||||
|
|
||||||
|
function renderGlobalGraph() {
|
||||||
|
const slug = getFullSlug(window)
|
||||||
|
container?.classList.add("active")
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.style.zIndex = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGraph("global-graph-container", slug)
|
||||||
|
registerEscapeHandler(container, hideGlobalGraph)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideGlobalGraph() {
|
||||||
|
container?.classList.remove("active")
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.style.zIndex = "unset"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
||||||
|
if (e.key === "g" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
const globalGraphOpen = container?.classList.contains("active")
|
||||||
|
globalGraphOpen ? hideGlobalGraph() : renderGlobalGraph()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const containerIcon = document.getElementById("global-graph-icon")
|
const containerIcon = document.getElementById("global-graph-icon")
|
||||||
containerIcon?.addEventListener("click", renderGlobalGraph)
|
containerIcon?.addEventListener("click", renderGlobalGraph)
|
||||||
window.addCleanup(() =>
|
window.addCleanup(() =>
|
||||||
|
|||||||
@ -1,17 +1,15 @@
|
|||||||
.darkmode {
|
.darkmode {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
|
text-align: inherit;
|
||||||
& > .toggle {
|
|
||||||
display: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
& svg {
|
& svg {
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
@ -29,20 +27,20 @@
|
|||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[saved-theme="dark"] .toggle ~ label {
|
:root[saved-theme="dark"] .darkmode {
|
||||||
& > #dayIcon {
|
& > #dayIcon {
|
||||||
opacity: 0;
|
display: none;
|
||||||
}
|
}
|
||||||
& > #nightIcon {
|
& > #nightIcon {
|
||||||
opacity: 1;
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:root .toggle ~ label {
|
:root .darkmode {
|
||||||
& > #dayIcon {
|
& > #dayIcon {
|
||||||
opacity: 1;
|
display: inline;
|
||||||
}
|
}
|
||||||
& > #nightIcon {
|
& > #nightIcon {
|
||||||
opacity: 0;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,10 +16,13 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
& > #global-graph-icon {
|
& > #global-graph-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
width: 18px;
|
width: 24px;
|
||||||
height: 18px;
|
height: 24px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
margin: 0.3rem;
|
margin: 0.3rem;
|
||||||
@ -59,8 +62,8 @@
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
height: 60vh;
|
height: 80vh;
|
||||||
width: 50vw;
|
width: 80vw;
|
||||||
|
|
||||||
@media all and (max-width: $fullPageWidth) {
|
@media all and (max-width: $fullPageWidth) {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
|
|||||||
@ -23,7 +23,7 @@ li.section-li {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .meta {
|
& .meta {
|
||||||
margin: 0 1em 0 0;
|
margin: 0 1em 0 0;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -161,6 +161,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
& .sidebar.left {
|
& .sidebar.left {
|
||||||
|
z-index: 1;
|
||||||
left: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth);
|
left: calc(calc(100vw - $pageWidth) / 2 - $sidePanelWidth);
|
||||||
@media all and (max-width: $fullPageWidth) {
|
@media all and (max-width: $fullPageWidth) {
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user