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 = {
head: Component.Head(),
header: [],
afterBody: [],
afterBody: [
Component.ConditionalRender({
component: Component.Graph({
globalOnly: true,
globalGraph: {
removeSlugs: ["index"],
},
}),
condition: (props) => props.fileData.slug === "index",
}),
],
footer: Component.Footer({
links: {},
}),

View File

@ -15,6 +15,7 @@ export interface D3Config {
linkDistance: number
fontSize: number
opacityScale: number
removeSlugs: string[]
removeTags: string[]
showTags: boolean
focusOnHover?: boolean
@ -22,11 +23,13 @@ export interface D3Config {
}
interface GraphOptions {
globalOnly: boolean
localGraph: Partial<D3Config> | undefined
globalGraph: Partial<D3Config> | undefined
}
const defaultOptions: GraphOptions = {
globalOnly: false,
localGraph: {
drag: true,
zoom: true,
@ -37,6 +40,7 @@ const defaultOptions: GraphOptions = {
linkDistance: 30,
fontSize: 0.6,
opacityScale: 1,
removeSlugs: [],
showTags: true,
removeTags: [],
focusOnHover: false,
@ -52,6 +56,7 @@ const defaultOptions: GraphOptions = {
linkDistance: 30,
fontSize: 0.6,
opacityScale: 1,
removeSlugs: [],
showTags: true,
removeTags: [],
focusOnHover: true,
@ -59,45 +64,79 @@ 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>) => {
const Graph: QuartzComponent = ({ displayClass, cfg }: QuartzComponentProps) => {
const localGraph = { ...defaultOptions.localGraph, ...opts?.localGraph }
const globalGraph = { ...defaultOptions.globalGraph, ...opts?.globalGraph }
const graphConfig = opts?.globalOnly ? globalGraph : localGraph
const title = opts?.globalOnly ? "Graph" : i18n(cfg.locale).components.graph.title
return (
<div class={classNames(displayClass, "graph")}>
<h3>{i18n(cfg.locale).components.graph.title}</h3>
<div class={classNames(displayClass, "graph", ...(opts?.globalOnly ? ["global-only"] : []))}>
{opts?.globalOnly ? (
<h2 class="graph-section-heading">
<GraphIcon />
{title}
</h2>
) : (
<h3>{title}</h3>
)}
<div class="graph-outer">
<div class="graph-container" data-cfg={JSON.stringify(localGraph)}></div>
<button class="global-graph-icon" aria-label="Global Graph">
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 55 55"
fill="currentColor"
xmlSpace="preserve"
>
<path
d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17
s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4
c0.74,0,1.424-0.215,2.019-0.567l7.669,7.669C21.634,21.458,21,23.154,21,25s0.634,3.542,1.688,4.897L10.024,42.562
C8.958,41.595,7.549,41,6,41c-3.309,0-6,2.691-6,6s2.691,6,6,6s6-2.691,6-6c0-1.035-0.263-2.009-0.726-2.86l12.829-12.829
c1.106,0.86,2.44,1.436,3.898,1.619v10.16c-2.833,0.478-5,2.942-5,5.91c0,3.309,2.691,6,6,6s6-2.691,6-6c0-2.967-2.167-5.431-5-5.91
v-10.16c1.458-0.183,2.792-0.759,3.898-1.619l7.669,7.669C41.215,39.576,41,40.26,41,41c0,2.206,1.794,4,4,4s4-1.794,4-4
s-1.794-4-4-4c-0.74,0-1.424,0.215-2.019,0.567l-7.669-7.669C36.366,28.542,37,26.846,37,25s-0.634-3.542-1.688-4.897l9.665-9.665
C46.042,11.405,47.451,12,49,12c3.309,0,6-2.691,6-6S52.309,0,49,0z M11,9c0-1.103,0.897-2,2-2s2,0.897,2,2s-0.897,2-2,2
S11,10.103,11,9z M6,51c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S8.206,51,6,51z M33,49c0,2.206-1.794,4-4,4s-4-1.794-4-4
s1.794-4,4-4S33,46.794,33,49z M29,31c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S32.309,31,29,31z M47,41c0,1.103-0.897,2-2,2
s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z"
/>
</svg>
</button>
</div>
<div class="global-graph-outer">
<div class="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
<div class="graph-container" data-cfg={JSON.stringify(graphConfig)}></div>
{!opts?.globalOnly && (
<button class="global-graph-icon" aria-label="Global Graph">
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 55 55"
fill="currentColor"
xmlSpace="preserve"
>
<path
d="M49,0c-3.309,0-6,2.691-6,6c0,1.035,0.263,2.009,0.726,2.86l-9.829,9.829C32.542,17.634,30.846,17,29,17
s-3.542,0.634-4.898,1.688l-7.669-7.669C16.785,10.424,17,9.74,17,9c0-2.206-1.794-4-4-4S9,6.794,9,9s1.794,4,4,4
c0.74,0,1.424-0.215,2.019-0.567l7.669,7.669C21.634,21.458,21,23.154,21,25s0.634,3.542,1.688,4.897L10.024,42.562
C8.958,41.595,7.549,41,6,41c-3.309,0-6,2.691-6,6s2.691,6,6,6s6-2.691,6-6c0-1.035-0.263-2.009-0.726-2.86l12.829-12.829
c1.106,0.86,2.44,1.436,3.898,1.619v10.16c-2.833,0.478-5,2.942-5,5.91c0,3.309,2.691,6,6,6s6-2.691,6-6c0-2.967-2.167-5.431-5-5.91
v-10.16c1.458-0.183,2.792-0.759,3.898-1.619l7.669,7.669C41.215,39.576,41,40.26,41,41c0,2.206,1.794,4,4,4s4-1.794,4-4
s-1.794-4-4-4c-0.74,0-1.424,0.215-2.019,0.567l-7.669-7.669C36.366,28.542,37,26.846,37,25s-0.634-3.542-1.688-4.897l9.665-9.665
C46.042,11.405,47.451,12,49,12c3.309,0,6-2.691,6-6S52.309,0,49,0z M11,9c0-1.103,0.897-2,2-2s2,0.897,2,2s-0.897,2-2,2
S11,10.103,11,9z M6,51c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S8.206,51,6,51z M33,49c0,2.206-1.794,4-4,4s-4-1.794-4-4
s1.794-4,4-4S33,46.794,33,49z M29,31c-3.309,0-6-2.691-6-6s2.691-6,6-6s6,2.691,6,6S32.309,31,29,31z M47,41c0,1.103-0.897,2-2,2
s-2-0.897-2-2s0.897-2,2-2S47,39.897,47,41z M49,10c-2.206,0-4-1.794-4-4s1.794-4,4-4s4,1.794,4,4S51.206,10,49,10z"
/>
</svg>
</button>
)}
</div>
{!opts?.globalOnly && (
<div class="global-graph-outer">
<div class="global-graph-container" data-cfg={JSON.stringify(globalGraph)}></div>
</div>
)}
</div>
)
}

View File

@ -31,7 +31,7 @@ const HomeHero: QuartzComponent = () => {
<p class="home-bio">
Software engineer and linguist. Interning at MIXI, Inc building iOS features for
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>
<div class="home-links">
<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",
institution: "Tokyo University of Foreign Studies",
institutionUrl: "https://www.tufs.ac.jp/english/",
logo: "/static/logos/tufs.svg",
logo: "/static/logos/Logo_tufs-cropped.svg",
period: "2024 2028",
},
{
@ -98,10 +98,10 @@ const education: EducationItem[] = [
const languages: Language[] = [
{ 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: "Spanish", level: "Professional · TOEIC 945" },
{ flag: "🇨🇳", name: "Mandarin", level: "Working · HSK 3 · TOCFL 4" },
{ flag: "🇪🇸", name: "Spanish", level: "Professional" },
{ flag: "🇨🇳", name: "Mandarin", level: "Working · HSK 3 · TBCL 4" },
]
// ── Component ──────────────────────────────────────────────────────────────

View File

@ -68,6 +68,17 @@ type TweenNode = {
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) {
const slug = simplifySlug(fullSlug)
const visited = getVisited()
@ -83,17 +94,18 @@ async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
linkDistance,
fontSize,
opacityScale,
removeSlugs,
removeTags,
showTags,
focusOnHover,
enableRadial,
} = 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(
Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
simplifySlug(k as FullSlug),
v,
]),
Object.entries<ContentDetails>(await fetchData)
.map(([k, v]) => [simplifySlug(k as FullSlug), v] as const)
.filter(([slug]) => !hiddenSlugs.has(slug)),
)
const links: SimpleLinkData[] = []
const tags: SimpleSlug[] = []
@ -123,7 +135,7 @@ async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
}
const neighbourhood = new Set<SimpleSlug>()
const wl: (SimpleSlug | "__SENTINEL")[] = [slug, "__SENTINEL"]
const wl: (SimpleSlug | "__SENTINEL")[] = validLinks.has(slug) ? [slug, "__SENTINEL"] : []
if (depth >= 0) {
while (depth >= 0 && wl.length > 0) {
// compute neighbours
@ -161,8 +173,7 @@ async function renderGraph(graph: HTMLElement, fullSlug: FullSlug) {
})),
}
const width = graph.offsetWidth
const height = Math.max(graph.offsetHeight, 250)
const { width, height } = getGraphDimensions(graph)
// we virtualize the simulation and use pixi to actually render it
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 globalGraphCleanups: (() => void)[] = []
@ -581,7 +643,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
cleanupLocalGraphs()
const localGraphContainers = document.getElementsByClassName("graph-container")
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
registerEscapeHandler(container, hideGlobalGraph)
if (graphContainer) {
globalGraphCleanups.push(await renderGraph(graphContainer, slug))
globalGraphCleanups.push(await mountGraph(graphContainer, slug))
}
}
}

View File

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

View File

@ -6,6 +6,41 @@
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 {
border-radius: 5px;
border: 1px solid var(--lightgray);
@ -15,6 +50,17 @@
position: relative;
overflow: hidden;
& > .graph-container {
width: 100%;
height: 100%;
}
& canvas {
display: block;
width: 100%;
height: 100%;
}
& > .global-graph-icon {
cursor: pointer;
background: none;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB