redesign almost done

This commit is contained in:
riceset 2026-03-18 19:27:05 +09:00
parent d4317685b3
commit 9cd90ae22a
9 changed files with 209 additions and 48 deletions

1
Logo_tufs-cropped.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -5,7 +5,17 @@ import * as Component from "./quartz/components"
export const sharedPageComponents: SharedLayout = { export const sharedPageComponents: SharedLayout = {
head: Component.Head(), head: Component.Head(),
header: [], header: [],
afterBody: [], afterBody: [
Component.ConditionalRender({
component: Component.Graph({
globalOnly: true,
globalGraph: {
removeSlugs: ["index"],
},
}),
condition: (props) => props.fileData.slug === "index",
}),
],
footer: Component.Footer({ footer: Component.Footer({
links: {}, links: {},
}), }),

View File

@ -15,6 +15,7 @@ export interface D3Config {
linkDistance: number linkDistance: number
fontSize: number fontSize: number
opacityScale: number opacityScale: number
removeSlugs: string[]
removeTags: string[] removeTags: string[]
showTags: boolean showTags: boolean
focusOnHover?: boolean focusOnHover?: boolean
@ -22,11 +23,13 @@ export interface D3Config {
} }
interface GraphOptions { interface GraphOptions {
globalOnly: boolean
localGraph: Partial<D3Config> | undefined localGraph: Partial<D3Config> | undefined
globalGraph: Partial<D3Config> | undefined globalGraph: Partial<D3Config> | undefined
} }
const defaultOptions: GraphOptions = { const defaultOptions: GraphOptions = {
globalOnly: false,
localGraph: { localGraph: {
drag: true, drag: true,
zoom: true, zoom: true,
@ -37,6 +40,7 @@ const defaultOptions: GraphOptions = {
linkDistance: 30, linkDistance: 30,
fontSize: 0.6, fontSize: 0.6,
opacityScale: 1, opacityScale: 1,
removeSlugs: [],
showTags: true, showTags: true,
removeTags: [], removeTags: [],
focusOnHover: false, focusOnHover: false,
@ -52,6 +56,7 @@ const defaultOptions: GraphOptions = {
linkDistance: 30, linkDistance: 30,
fontSize: 0.6, fontSize: 0.6,
opacityScale: 1, opacityScale: 1,
removeSlugs: [],
showTags: true, showTags: true,
removeTags: [], removeTags: [],
focusOnHover: true, focusOnHover: true,
@ -59,15 +64,46 @@ const defaultOptions: GraphOptions = {
}, },
} }
const GraphIcon = () => (
<svg
class="section-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="5" cy="6" r="2" />
<circle cx="19" cy="5" r="2" />
<circle cx="18" cy="19" r="2" />
<circle cx="6" cy="18" r="2" />
<path d="M7 7.5 11 10" />
<path d="M17 6.5 13 10" />
<path d="M7.5 16.5 11 13" />
<path d="M16.5 17 13 13" />
</svg>
)
export default ((opts?: Partial<GraphOptions>) => { export default ((opts?: Partial<GraphOptions>) => {
const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => { const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph } const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph } const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
const graphConfig = opts?.globalOnly ? globalGraph : localGraph
const title = opts?.globalOnly ? "Graph" : i18n(cfg.locale).components.graph.title
return ( return (
<div class={classNames(displayClass, "graph")}> <div class={classNames(displayClass, "graph", ...(opts?.globalOnly ? ["global-only"] : []))}>
<h3>{i18n(cfg.locale).components.graph.title}</h3> {opts?.globalOnly ? (
<h2 class="graph-section-heading">
<GraphIcon />
{title}
</h2>
) : (
<h3>{title}</h3>
)}
<div class="graph-outer"> <div class="graph-outer">
<div class="graph-container" data-cfg={JSON.stringify(localGraph)}></div> <div class="graph-container" data-cfg={JSON.stringify(graphConfig)}></div>
{!opts?.globalOnly && (
<button class="global-graph-icon" aria-label="Global Graph"> <button class="global-graph-icon" aria-label="Global Graph">
<svg <svg
version="1.1" version="1.1"
@ -94,10 +130,13 @@ export default ((opts?: Partial<GraphOptions>) => {
/> />
</svg> </svg>
</button> </button>
)}
</div> </div>
{!opts?.globalOnly && (
<div class="global-graph-outer"> <div class="global-graph-outer">
<div class="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div> <div class="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
</div> </div>
)}
</div> </div>
) )
} }

View File

@ -31,7 +31,7 @@ const HomeHero: QuartzComponent = () => {
<p class="home-bio"> <p class="home-bio">
Software engineer and linguist. Interning at MIXI, Inc building iOS features for Software engineer and linguist. Interning at MIXI, Inc building iOS features for
FamilyAlbum. MEXT Scholar at Tokyo University of Foreign Studies. 42 Network alumnus. FamilyAlbum. MEXT Scholar at Tokyo University of Foreign Studies. 42 Network alumnus.
Native in English, Japanese, and Portuguese also speak Spanish and Mandarin. Native in Japanese and Portuguese, bilingual in English, and also speak Spanish and Mandarin.
</p> </p>
<div class="home-links"> <div class="home-links">
<a href="mailto:riceset@icloud.com" class="home-link"> <a href="mailto:riceset@icloud.com" class="home-link">

View File

@ -84,7 +84,7 @@ const education: EducationItem[] = [
degree: "B.A. Language and Area Studies", degree: "B.A. Language and Area Studies",
institution: "Tokyo University of Foreign Studies", institution: "Tokyo University of Foreign Studies",
institutionUrl: "https://www.tufs.ac.jp/english/", institutionUrl: "https://www.tufs.ac.jp/english/",
logo: "/static/logos/tufs.svg", logo: "/static/logos/Logo_tufs-cropped.svg",
period: "2024 2028", period: "2024 2028",
}, },
{ {
@ -98,10 +98,10 @@ const education: EducationItem[] = [
const languages: Language[] = [ const languages: Language[] = [
{ flag: "🇧🇷", name: "Portuguese", level: "Native" }, { flag: "🇧🇷", name: "Portuguese", level: "Native" },
{ flag: "🇺🇸", name: "English", level: "Native" }, { flag: "🇺🇸", name: "English", level: "Bilingual · TOEIC 945" },
{ flag: "🇯🇵", name: "Japanese", level: "Native · JLPT N1" }, { flag: "🇯🇵", name: "Japanese", level: "Native · JLPT N1" },
{ flag: "🇪🇸", name: "Spanish", level: "Professional · TOEIC 945" }, { flag: "🇪🇸", name: "Spanish", level: "Professional" },
{ flag: "🇨🇳", name: "Mandarin", level: "Working · HSK 3 · TOCFL 4" }, { flag: "🇨🇳", name: "Mandarin", level: "Working · HSK 3 · TBCL 4" },
] ]
// ── Component ────────────────────────────────────────────────────────────── // ── Component ──────────────────────────────────────────────────────────────

View File

@ -68,6 +68,17 @@ type TweenNode = {
stop: () => void stop: () => void
} }
function getGraphDimensions(graph: HTMLElement) {
const parent = graph.parentElement
const graphRect = graph.getBoundingClientRect()
const parentRect = parent?.getBoundingClientRect()
return {
width: Math.max(Math.round(graphRect.width), Math.round(parentRect?.width ?? 0)),
height: Math.max(Math.round(graphRect.height), Math.round(parentRect?.height ?? 0), 250),
}
}
async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) { async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
const slug = simplifySlug(fullSlug) const slug = simplifySlug(fullSlug)
const visited = getVisited() const visited = getVisited()
@ -83,17 +94,18 @@ async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
linkDistance, linkDistance,
fontSize, fontSize,
opacityScale, opacityScale,
removeSlugs,
removeTags, removeTags,
showTags, showTags,
focusOnHover, focusOnHover,
enableRadial, enableRadial,
} = JSON.parse(graph.dataset["cfg"]!) as D3Config } = JSON.parse(graph.dataset["cfg"]!) as D3Config
const hiddenSlugs = new Set(removeSlugs.map((slug) => simplifySlug(slug as FullSlug)))
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)
simplifySlug(k as FullSlug), .map(([k, v]) => [simplifySlug(k as FullSlug), v] as const)
v, .filter(([slug]) => !hiddenSlugs.has(slug)),
]),
) )
const links: SimpleLinkData[] = [] const links: SimpleLinkData[] = []
const tags: SimpleSlug[] = [] const tags: SimpleSlug[] = []
@ -123,7 +135,7 @@ async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
} }
const neighbourhood = new Set<SimpleSlug>() const neighbourhood = new Set<SimpleSlug>()
const wl: (SimpleSlug | "__SENTINEL")[] = [slug, "__SENTINEL"] const wl: (SimpleSlug | "__SENTINEL")[] = validLinks.has(slug) ? [slug, "__SENTINEL"] : []
if (depth >= 0) { if (depth >= 0) {
while (depth >= 0 && wl.length > 0) { while (depth >= 0 && wl.length > 0) {
// compute neighbours // compute neighbours
@ -161,8 +173,7 @@ async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
})), })),
} }
const width = graph.offsetWidth const { width, height } = getGraphDimensions(graph)
const height = Math.max(graph.offsetHeight, 250)
// we virtualize the simulation and use pixi to actually render it // we virtualize the simulation and use pixi to actually render it
const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes) const simulation: Simulation<NodeData, LinkData> = forceSimulation<NodeData>(graphData.nodes)
@ -556,6 +567,57 @@ async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
} }
} }
async function mountGraph(graph: HTMLElement, fullSlug: FullSlug) {
let cleanup = () => {}
let isRendering = false
let pendingRender = false
let lastWidth = 0
let lastHeight = 0
const render = async (force = false) => {
if (isRendering) {
pendingRender = true
return
}
const { width, height } = getGraphDimensions(graph)
if (!force && width === lastWidth && height === lastHeight) {
return
}
isRendering = true
cleanup()
try {
cleanup = await renderGraph(graph, fullSlug)
lastWidth = width
lastHeight = height
} finally {
isRendering = false
if (pendingRender) {
pendingRender = false
void render()
}
}
}
await render(true)
const resizeObserver = new ResizeObserver(() => {
void render()
})
resizeObserver.observe(graph)
if (graph.parentElement) {
resizeObserver.observe(graph.parentElement)
}
return () => {
resizeObserver.disconnect()
cleanup()
}
}
let localGraphCleanups: (() => void)[] = [] let localGraphCleanups: (() => void)[] = []
let globalGraphCleanups: (() => void)[] = [] let globalGraphCleanups: (() => void)[] = []
@ -581,7 +643,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
cleanupLocalGraphs() cleanupLocalGraphs()
const localGraphContainers = document.getElementsByClassName("graph-container") const localGraphContainers = document.getElementsByClassName("graph-container")
for (const container of localGraphContainers) { for (const container of localGraphContainers) {
localGraphCleanups.push(await renderGraph(container as HTMLElement, slug)) localGraphCleanups.push(await mountGraph(container as HTMLElement, slug))
} }
} }
@ -608,7 +670,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
const graphContainer = container.querySelector(".global-graph-container") as HTMLElement const graphContainer = container.querySelector(".global-graph-container") as HTMLElement
registerEscapeHandler(container, hideGlobalGraph) registerEscapeHandler(container, hideGlobalGraph)
if (graphContainer) { if (graphContainer) {
globalGraphCleanups.push(await renderGraph(graphContainer, slug)) globalGraphCleanups.push(await mountGraph(graphContainer, slug))
} }
} }
} }

View File

@ -5,8 +5,8 @@ footer {
opacity: 0.7; opacity: 0.7;
&.home-footer { &.home-footer {
margin-top: 0;
padding-top: 2rem; padding-top: 2rem;
border-top: 1px solid var(--lightgray);
} }
& ul { & ul {

View File

@ -6,6 +6,41 @@
margin: 0; margin: 0;
} }
&.global-only {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid var(--lightgray);
}
& .graph-section-heading {
display: flex;
align-items: center;
gap: 0.4rem;
font-family: var(--headerFont);
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--gray);
margin: 0 0 1.25rem;
}
& .section-icon {
width: 14px;
height: 14px;
flex-shrink: 0;
stroke: var(--gray);
}
&.global-only > .graph-outer {
height: 420px;
margin: 0;
@media all and ($mobile) {
height: 320px;
}
}
& > .graph-outer { & > .graph-outer {
border-radius: 5px; border-radius: 5px;
border: 1px solid var(--lightgray); border: 1px solid var(--lightgray);
@ -15,6 +50,17 @@
position: relative; position: relative;
overflow: hidden; overflow: hidden;
& > .graph-container {
width: 100%;
height: 100%;
}
& canvas {
display: block;
width: 100%;
height: 100%;
}
& > .global-graph-icon { & > .global-graph-icon {
cursor: pointer; cursor: pointer;
background: none; background: none;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB