Add AGENTS.md as the single source of truth (with CLAUDE.md and GEMINI.md as pointers), permissions for Claude Code, Codex and Gemini, the docs tree (architecture, decisions, TODO, permissions, specs, plans), PRODUCT.md and DESIGN.md, plus the design-phase record: the inspiration log, all mockup rounds with briefs and screenshots, and the owner's work history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
330 lines
23 KiB
HTML
330 lines
23 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Lucas Winther: what I know, and the proof</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:ital,wght@0,400;0,500;0,700;1,400&family=Martian+Mono:wght@300;400&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root{
|
||
--bg:#1C1A18; --bg-2:#25221F; --bg-3:#302C28; --bone:#E8E1D2; --muted:#8C847A; --faint:#4A4540;
|
||
--ochre:#D8A23F; --moss:#9BAA74;
|
||
--sans:"Schibsted Grotesk",system-ui,sans-serif; --mono:"Martian Mono",ui-monospace,monospace;
|
||
}
|
||
*{box-sizing:border-box}
|
||
html{background:var(--bg)}
|
||
body{margin:0;color:var(--bone);font-family:var(--sans);font-size:15px;line-height:1.5}
|
||
a{color:inherit;text-decoration-color:var(--faint);text-underline-offset:3px}
|
||
a:hover{color:var(--ochre);text-decoration-color:var(--ochre)}
|
||
.ph{text-decoration:underline dotted var(--muted);text-underline-offset:3px;cursor:help}
|
||
.mono{font-family:var(--mono);font-size:11px;letter-spacing:.02em}
|
||
.wrap{max-width:1240px;margin:0 auto;padding:0 32px}
|
||
.mock-note{position:fixed;top:10px;right:10px;z-index:9999;max-width:290px;background:#fff;color:#222;border:1px solid #aaa;padding:8px 30px 8px 10px;font:12px/1.4 system-ui,sans-serif}
|
||
.mock-note b{display:block}
|
||
.mock-note button{position:absolute;top:4px;right:6px;border:0;background:none;font-size:16px;cursor:pointer;color:#555}
|
||
|
||
header{display:grid;grid-template-columns:1fr;gap:20px 40px;padding:40px 0 22px;border-bottom:1px solid var(--faint);align-items:end}
|
||
h1{margin:0;font-weight:700;font-size:40px;letter-spacing:-.02em;line-height:1}
|
||
h1 span{font-weight:400;color:var(--muted)}
|
||
.sub{margin:10px 0 0;color:var(--muted);max-width:44em}
|
||
.card{display:grid;grid-template-columns:repeat(5,auto);justify-content:start;gap:4px 44px;font-size:14px}
|
||
.card .mono{color:var(--muted);display:block;text-transform:uppercase}
|
||
.card a{white-space:nowrap}
|
||
|
||
/* AI filter: proportional bar */
|
||
.filterbar{display:grid;grid-template-columns:auto 1fr auto;gap:18px;align-items:center;padding:18px 0}
|
||
.filterbar .mono{color:var(--muted);text-transform:uppercase}
|
||
.seg{display:flex;height:34px;gap:3px}
|
||
.seg button{font:inherit;font-size:13px;border:0;cursor:pointer;color:var(--bone);background:var(--bg-3);text-align:left;padding:0 10px;display:flex;align-items:center;gap:8px;min-width:0;white-space:nowrap;overflow:hidden;transition:background .15s,opacity .15s;position:relative}
|
||
.seg button .ln{flex:0 0 22px;height:0;border-top:2px solid var(--bone)}
|
||
.seg button[data-ai="some"] .ln{border-top-style:dashed;border-color:var(--ochre)}
|
||
.seg button[data-ai="full"] .ln{border-top-style:dotted;border-top-width:3px;border-color:var(--ochre)}
|
||
.seg button:hover{background:#3B3631}
|
||
.seg button[aria-pressed="true"]{background:var(--bone);color:var(--bg)}
|
||
.seg button[aria-pressed="true"] .ln{border-color:var(--bg)}
|
||
.seg.filtered button[aria-pressed="false"]{opacity:.45}
|
||
.seg b{font-weight:500;font-family:var(--mono);font-size:11px;opacity:.7}
|
||
.reset{font:inherit;font-size:13px;background:none;border:0;color:var(--muted);cursor:pointer;text-decoration:underline;text-underline-offset:3px;visibility:hidden}
|
||
.seg.filtered ~ .reset{visibility:visible}
|
||
|
||
/* map */
|
||
.map{position:relative;display:grid;grid-template-columns:1.15fr 1fr .95fr;gap:0 72px;padding:8px 0 30px}
|
||
.map svg{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}
|
||
.map svg path{fill:none;stroke:var(--bone);stroke-width:1.2;opacity:.65}
|
||
.map svg path.some{stroke:var(--ochre);stroke-dasharray:5 4}
|
||
.map svg path.full{stroke:var(--ochre);stroke-dasharray:1.5 3.5;stroke-width:2;stroke-linecap:round}
|
||
.col h2{font-size:12px;font-family:var(--mono);font-weight:400;color:var(--muted);text-transform:uppercase;margin:0 0 12px;display:flex;justify-content:space-between}
|
||
.grp{margin:0 0 14px}
|
||
.grp h3{font-family:var(--mono);font-weight:300;font-size:10.5px;color:var(--muted);margin:0 0 4px;text-transform:uppercase;letter-spacing:.05em}
|
||
.skills{display:flex;flex-wrap:wrap;gap:4px 5px}
|
||
.node{position:relative;z-index:1;font:inherit;color:var(--bone);background:var(--bg-2);border:1px solid var(--faint);cursor:pointer;transition:background .12s,border-color .12s,opacity .15s,color .12s;text-align:left}
|
||
.skill{padding:2px 8px;font-size:calc(12.5px + var(--w,0) * 1.1px)}
|
||
.skill sup{font-family:var(--mono);font-size:9px;color:var(--muted);margin-left:3px}
|
||
.proj,.post{display:flex;justify-content:space-between;gap:10px;width:100%;padding:6px 10px;margin:0 0 4px;align-items:baseline}
|
||
.proj .y,.post .y{font-family:var(--mono);font-size:10.5px;color:var(--muted);flex:none}
|
||
.proj .t{font-weight:500}
|
||
.proj[data-ai="some"]{border-left:3px dashed var(--ochre)}
|
||
.proj[data-ai="full"]{border-left:3px dotted var(--ochre)}
|
||
.proj[data-ai="none"]{border-left:3px solid var(--bone)}
|
||
.post{border-color:transparent;background:transparent;border-left:1px solid var(--faint)}
|
||
.post .t{font-style:italic}
|
||
.map.active .node{opacity:.28}
|
||
.map.active .node.lit{opacity:1;border-color:var(--ochre);background:var(--bg-3)}
|
||
.map.active .node.src{opacity:1;background:var(--bone);color:var(--bg);border-color:var(--bone)}
|
||
.map.active .node.src sup,.map.active .node.src .y{color:var(--bg)}
|
||
.node.off{display:none}
|
||
.node:focus-visible{outline:2px solid var(--ochre);outline-offset:1px}
|
||
|
||
.inspector{min-height:66px;border-top:1px solid var(--faint);border-bottom:1px solid var(--faint);padding:12px 0;display:grid;grid-template-columns:200px 1fr auto;gap:24px;align-items:start}
|
||
.inspector .mono{color:var(--ochre);text-transform:uppercase}
|
||
.inspector h4{margin:0;font-size:18px;font-weight:500}
|
||
.inspector p{margin:2px 0 0;color:var(--muted);max-width:60em}
|
||
.inspector .pin{color:var(--muted)}
|
||
|
||
/* matrix */
|
||
.lower{display:grid;grid-template-columns:1.4fr 1fr;gap:56px;padding:44px 0 20px}
|
||
.lower h2{font-size:22px;font-weight:500;margin:0 0 4px;letter-spacing:-.01em}
|
||
.lower .note{color:var(--muted);margin:0 0 14px;font-size:14px}
|
||
.matrix{border-collapse:collapse;width:100%}
|
||
.matrix th{font-family:var(--mono);font-weight:300;font-size:9.5px;color:var(--muted);padding:2px 0}
|
||
.matrix th.r{text-align:right;padding-right:10px;font-family:var(--sans);font-size:12.5px;font-weight:400;color:var(--bone);white-space:nowrap}
|
||
.matrix td{padding:1.5px}
|
||
.matrix td i{display:block;height:15px;background:var(--bg-2);transition:outline-color .1s}
|
||
.matrix td i.l1{background:#5B5348}.matrix td i.l2{background:#A08F72}.matrix td i.l3{background:var(--bone)}
|
||
.matrix td i.ai{box-shadow:inset 0 -4px 0 var(--ochre)}
|
||
.matrix td i:hover{outline:1px solid var(--ochre)}
|
||
.matrix tr.split th.r,.matrix tr.split td{border-top:1px solid var(--faint)}
|
||
.matrix .era th{font-size:9.5px;color:var(--muted);text-align:left;padding-top:6px}
|
||
.legend{display:flex;gap:16px;margin-top:10px;color:var(--muted);align-items:center;flex-wrap:wrap}
|
||
.legend i{display:inline-block;width:14px;height:10px;vertical-align:-1px;margin-right:4px}
|
||
|
||
.writing ul{list-style:none;margin:0;padding:0;border-top:1px solid var(--faint)}
|
||
.writing li a{display:grid;grid-template-columns:72px 1fr;gap:12px;padding:10px 0;border-bottom:1px solid var(--faint);text-decoration:none}
|
||
.writing li .k{display:block;font-family:var(--mono);font-size:10px;color:var(--muted);margin-top:2px}
|
||
.writing li a:hover .tt{color:var(--ochre)}
|
||
.old{display:block;margin-top:22px;padding:14px 16px;border:1px solid var(--faint);text-decoration:none}
|
||
.old b{font-weight:500}
|
||
.old span{display:block;color:var(--muted);font-size:14px}
|
||
.old:hover{border-color:var(--bone);color:var(--bone)}
|
||
|
||
footer{padding:24px 0 56px;border-top:1px solid var(--faint);margin-top:24px;color:var(--muted);font-size:13px;display:flex;justify-content:space-between;gap:24px;flex-wrap:wrap}
|
||
|
||
@media (max-width:900px){
|
||
header{grid-template-columns:1fr}
|
||
.card{grid-template-columns:1fr 1fr}
|
||
.map{grid-template-columns:1fr;gap:20px}
|
||
.map svg{display:none}
|
||
.lower{grid-template-columns:1fr}
|
||
.inspector{grid-template-columns:1fr;gap:4px}
|
||
.filterbar{grid-template-columns:1fr}
|
||
}
|
||
@media (max-width:480px){ .wrap{padding:0 16px} h1{font-size:32px} .seg button{font-size:11px;padding:0 6px} .seg button .ln{display:none} .matrix th.r{font-size:11px} }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="mock-note"><button aria-label="Dismiss note" onclick="this.parentNode.remove()">×</button><b>tech-4 · The skills map</b>Skills linked to the projects and posts that prove them: hover one side and the other lights up, with connecting lines whose dash shows AI involvement. The same data again as a skills × years matrix. Borrows sankar.work's "information in different ways".</div>
|
||
|
||
<div class="wrap">
|
||
<header>
|
||
<div>
|
||
<h1>Lucas Winther <span>/ what I know, and the proof</span></h1>
|
||
<p class="sub">Computer scientist. Every skill below links to something I built or wrote. Point at a skill to see where it was used, or at a project to see what it took.</p>
|
||
</div>
|
||
<div class="card">
|
||
<div><span class="mono">Now</span><span class="ph" title="placeholder">Software engineer, Nordlys Systems</span></div>
|
||
<div><span class="mono">Education</span>BSc CS, <span class="ph" title="placeholder">Aarhus Univ., 2022</span></div>
|
||
<div><span class="mono">Contact</span><a href="mailto:lucas@lucaswinther.info">lucas@lucaswinther.info</a></div>
|
||
<div><span class="mono">Elsewhere</span><a href="https://www.linkedin.com/in/lucas-winther">LinkedIn</a> · <a href="https://github.com/StereotypicalCat">GitHub</a> · <a href="#" title="placeholder: CV PDF">CV</a></div>
|
||
<div><span class="mono">By hand, before</span><a href="https://old.lucaswinther.info">old.lucaswinther.info</a></div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="filterbar">
|
||
<span class="mono">How it was made</span>
|
||
<div class="seg" id="seg" role="group" aria-label="Filter projects by AI involvement"></div>
|
||
<button class="reset" id="reset">Show all</button>
|
||
</div>
|
||
|
||
<div class="map" id="map">
|
||
<svg id="wires" aria-hidden="true"></svg>
|
||
<div class="col"><h2><span>Skills & tools</span><span>size = projects</span></h2><div id="skillCol"></div></div>
|
||
<div class="col"><h2><span>Projects</span><span id="pc"></span></h2><div id="projCol"></div></div>
|
||
<div class="col"><h2><span>Writing</span><a href="https://blog.lucaswinther.info">blog →</a></h2><div id="postCol"></div></div>
|
||
</div>
|
||
|
||
<div class="inspector" id="insp" aria-live="polite">
|
||
<span class="mono">Inspector</span>
|
||
<div><h4>Point at anything above</h4><p>Click to pin a selection, click again or press Esc to release it.</p></div>
|
||
<span class="pin mono"></span>
|
||
</div>
|
||
|
||
<div class="lower">
|
||
<section>
|
||
<h2>Skills over time</h2>
|
||
<p class="note">The same links, laid out by year. Brighter cells mean more projects that year; an ochre underline means AI was involved. The rule at 2023 is where the by-hand years end.</p>
|
||
<table class="matrix" id="matrix"></table>
|
||
<div class="legend mono"><span><i style="background:#5B5348"></i>1 project</span><span><i style="background:#A08F72"></i>2</span><span><i style="background:var(--bone)"></i>3+</span><span><i style="background:var(--bg-2);box-shadow:inset 0 -4px 0 var(--ochre)"></i>with AI</span></div>
|
||
</section>
|
||
<section class="writing">
|
||
<h2>Latest writing</h2>
|
||
<p class="note">AI tooling, devops and the homelab, at <a href="https://blog.lucaswinther.info">blog.lucaswinther.info</a>.</p>
|
||
<ul id="writeList"></ul>
|
||
<a class="old" href="https://old.lucaswinther.info"><b>By hand, before →</b><span>The 2021 portfolio at old.lucaswinther.info, frozen. Everything there predates AI tools.</span></a>
|
||
</section>
|
||
</div>
|
||
|
||
<footer>
|
||
<span>Set in Schibsted Grotesk and Martian Mono. <a href="https://old.lucaswinther.info">Old site</a> · <a href="https://blog.lucaswinther.info">Blog</a> · <a href="https://github.com/StereotypicalCat">GitHub</a></span>
|
||
<span>Placeholders (dotted underline): employer, university, all years, blog post titles, CV link, “Homelab metrics” and “this site” projects, skill groupings.</span>
|
||
</footer>
|
||
</div>
|
||
|
||
<script>
|
||
const GROUPS=[
|
||
["Languages",[["js","JavaScript / TS"],["python","Python"],["csharp","C#"],["htmlcss","HTML & CSS"]]],
|
||
["Web & apps",[["node","Node.js"],["react","React"],["scraping","Scraping"]]],
|
||
["Infrastructure",[["linux","Linux"],["docker","Docker / compose"],["ansible","Ansible"],["k8s","k3s"],["prom","Prometheus & Grafana"]]],
|
||
["Hardware & XR",[["unity","Unity"],["vr","VR / MR"],["arduino","Arduino"]]],
|
||
["Research",[["p2p","Distributed systems"],["hci","User studies"]]],
|
||
["Working with AI",[["agents","Agent workflows"],["spec","Spec writing & review"]]]
|
||
];
|
||
const SK={};GROUPS.forEach(g=>g[1].forEach(s=>SK[s[0]]=s[1]));
|
||
const PROJ=[
|
||
{id:"site",y:2026,t:"lucaswinther.info",ai:"some",ph:1,s:["htmlcss","js","agents"],d:"This site. Layout and copy by hand; the wiring of this map pair-written with AI."},
|
||
{id:"metrics",y:2026,t:"Homelab metrics",ai:"some",ph:1,s:["prom","docker","linux","agents"],d:"Alerting and dashboards for the homelab. Rules by hand, dashboards drafted by AI."},
|
||
{id:"gacha",y:2025,t:"Event Tracker for Gacha Games",ai:"full",s:["js","node","scraping","agents","spec"],d:"Scrapes gacha-game events into one calendar. Spec and tests mine; code by an agent. Live demo and GitHub."},
|
||
{id:"config",y:2025,t:"Config",ai:"full",s:["ansible","docker","linux","agents","spec"],d:"Ansible and docker-compose that rebuild the homelab from bare metal."},
|
||
{id:"hyper",y:2022,t:"HyperCache (thesis)",ai:"none",s:["node","p2p","js"],d:"Bachelor project: reputation systems in peer-to-peer archival. Thesis PDF."},
|
||
{id:"dash",y:2021,t:"Student Dashboard",ai:"none",s:["react","js","hci"],d:"React dashboard built on two rounds of user studies. Report and demo."},
|
||
{id:"exp",y:2021,t:"Monthly Expenses Tracker",ai:"none",s:["js","htmlcss"],d:"Spending in DKK against a monthly budget."},
|
||
{id:"port",y:2021,t:"Portfolio (2021)",ai:"none",s:["react","js","htmlcss"],d:"The old site, kept as the by-hand archive."},
|
||
{id:"rowing",y:2020,t:"Mixed-Reality Rowing Game",ai:"none",s:["unity","csharp","arduino","vr"],d:"Two real oars with sensors steer a boat in Unity."},
|
||
{id:"chess",y:2020,t:"Chessswap",ai:"none",s:["react","js"],d:"Chess where the board swaps every few turns."},
|
||
{id:"vrc",y:2019,t:"VR-Classroom",ai:"none",s:["node","vr","js","hci"],d:"Shared VR classroom prototype. Report and video."},
|
||
{id:"lol",y:2019,t:"LoL Winrate Discord Bot",ai:"none",s:["node","js","scraping"],d:"Champion win rates on request."},
|
||
{id:"censor",y:2018,t:"Censor Simulator",ai:"none",s:["unity","csharp","vr"],d:"VR high-school exam project."},
|
||
{id:"breakout",y:2017,t:"BreakoutPython",ai:"none",s:["python"],d:"High-school exam project."},
|
||
{id:"borlaug",y:2016,t:"Tribute to Norman Borlaug",ai:"none",s:["htmlcss"],d:"One of the first websites I made."}
|
||
];
|
||
const POSTS=[
|
||
{id:"p1",y:"2026",t:"What I let the agent write, and what I didn't",k:"AI",s:["agents","spec","js"],p:["gacha","site"]},
|
||
{id:"p2",y:"2026",t:"Rebuilding the homelab from one git repo",k:"Devops",s:["ansible","linux","docker"],p:["config"]},
|
||
{id:"p3",y:"2026",t:"Three mini PCs, one k3s cluster",k:"Homelab",s:["k8s","linux","docker"],p:["metrics"]},
|
||
{id:"p4",y:"2026",t:"Specs are prompts with manners",k:"AI",s:["spec","agents"],p:["gacha"]},
|
||
{id:"p5",y:"2025",t:"Backups that have actually been restored",k:"Homelab",s:["linux","docker"],p:["config"]},
|
||
{id:"p6",y:"2025",t:"Ansible idempotency, the hard way",k:"Devops",s:["ansible"],p:["config"]},
|
||
{id:"p7",y:"2022",t:"Bachelor report: trust in P2P archival",k:"Thesis",s:["p2p","node"],p:["hyper"],real:1}
|
||
];
|
||
const AIL={none:"No AI",some:"Some AI help",full:"Fully AI-made"};
|
||
const esc=s=>String(s).replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));
|
||
let filter=null, pinned=null;
|
||
const map=document.getElementById("map"), svg=document.getElementById("wires"), insp=document.getElementById("insp");
|
||
const visP=()=>PROJ.filter(p=>!filter||p.ai===filter);
|
||
|
||
function renderSeg(){
|
||
const c={none:0,some:0,full:0};PROJ.forEach(p=>c[p.ai]++);
|
||
const seg=document.getElementById("seg");
|
||
seg.classList.toggle("filtered",!!filter);
|
||
seg.innerHTML=["none","some","full"].map(a=>`<button data-ai="${a}" aria-pressed="${filter===a}" style="flex:${c[a]+2} 1 0"><span class="ln"></span>${AIL[a]} <b>${c[a]}</b></button>`).join("");
|
||
}
|
||
function render(){
|
||
const vp=visP();
|
||
document.getElementById("skillCol").innerHTML=GROUPS.map(g=>`<div class="grp"><h3>${g[0]}</h3><div class="skills">`+g[1].map(([id,n])=>{
|
||
const cnt=vp.filter(p=>p.s.includes(id)).length;
|
||
const pc=POSTS.filter(p=>p.s.includes(id)).length;
|
||
return `<button class="node skill${cnt+pc===0?" off":""}" data-kind="s" data-id="${id}" style="--w:${Math.min(cnt,6)}">${esc(n)}<sup>${cnt}</sup></button>`;
|
||
}).join("")+`</div></div>`).join("");
|
||
document.getElementById("projCol").innerHTML=PROJ.map(p=>`<button class="node proj${vp.includes(p)?"":" off"}" data-kind="p" data-id="${p.id}" data-ai="${p.ai}"><span class="t">${p.ph?`<span class="ph" title="placeholder">${esc(p.t)}</span>`:esc(p.t)}</span><span class="y">${p.y}</span></button>`).join("");
|
||
document.getElementById("postCol").innerHTML=POSTS.map(p=>`<button class="node post" data-kind="w" data-id="${p.id}"><span class="t">${p.real?esc(p.t):`<span class="ph" title="placeholder">${esc(p.t)}</span>`}</span><span class="y">${p.k}</span></button>`).join("");
|
||
document.getElementById("pc").textContent=vp.length+" / "+PROJ.length;
|
||
renderMatrix();
|
||
if(pinned)activate(pinned.kind,pinned.id);else clear();
|
||
}
|
||
function related(kind,id){
|
||
const out={s:new Set(),p:new Set(),w:new Set()};
|
||
const vp=visP();
|
||
if(kind==="s"){vp.forEach(p=>p.s.includes(id)&&out.p.add(p.id));POSTS.forEach(p=>p.s.includes(id)&&out.w.add(p.id));}
|
||
if(kind==="p"){const p=PROJ.find(x=>x.id===id);p.s.forEach(s=>out.s.add(s));POSTS.forEach(w=>w.p.includes(id)&&out.w.add(w.id));}
|
||
if(kind==="w"){const w=POSTS.find(x=>x.id===id);w.s.forEach(s=>out.s.add(s));w.p.forEach(p=>vp.some(v=>v.id===p)&&out.p.add(p));}
|
||
return out;
|
||
}
|
||
const el=(k,id)=>map.querySelector(`.node[data-kind="${k}"][data-id="${id}"]`);
|
||
function wire(a,b,cls){
|
||
const m=map.getBoundingClientRect(),ra=a.getBoundingClientRect(),rb=b.getBoundingClientRect();
|
||
let x1,x2;
|
||
if(ra.left<rb.left){x1=ra.right;x2=rb.left;}else{x1=ra.left;x2=rb.right;}
|
||
const y1=ra.top+ra.height/2-m.top,y2=rb.top+rb.height/2-m.top;x1-=m.left;x2-=m.left;
|
||
const dx=(x2-x1)*.5;
|
||
return `<path class="${cls||""}" d="M${x1},${y1} C${x1+dx},${y1} ${x2-dx},${y2} ${x2},${y2}"/>`;
|
||
}
|
||
function activate(kind,id){
|
||
const src=el(kind,id);if(!src){clear();return;}
|
||
const r=related(kind,id);
|
||
map.classList.add("active");
|
||
map.querySelectorAll(".node").forEach(n=>n.classList.remove("lit","src"));
|
||
src.classList.add("src");
|
||
let paths="";
|
||
["s","p","w"].forEach(k=>r[k].forEach(i=>{
|
||
const n=el(k,i);if(!n||n.classList.contains("off"))return;n.classList.add("lit");
|
||
let cls="";
|
||
if(kind==="p")cls=PROJ.find(x=>x.id===id).ai;
|
||
else if(k==="p")cls=PROJ.find(x=>x.id===i).ai;
|
||
if(cls==="none")cls="";
|
||
// wires: skill<->project, project<->post, skill<->post only if no project in between
|
||
if(kind==="s"&&k==="w")cls="post-wire";
|
||
paths+=wire(src,n,cls);
|
||
}));
|
||
svg.innerHTML=paths;
|
||
svg.querySelectorAll(".post-wire").forEach(p=>{p.style.opacity=".25";});
|
||
// inspector
|
||
let title,body,meta="";
|
||
if(kind==="s"){title=SK[id];body=`Used in ${r.p.size} project${r.p.size===1?"":"s"}${filter?` (${AIL[filter].toLowerCase()})`:""} and ${r.w.size} post${r.w.size===1?"":"s"}: `+[...r.p].map(i=>PROJ.find(x=>x.id===i).t).join(", ")+".";}
|
||
if(kind==="p"){const p=PROJ.find(x=>x.id===id);title=`${p.t} · ${p.y}`;body=p.d;meta=AIL[p.ai];}
|
||
if(kind==="w"){const w=POSTS.find(x=>x.id===id);title=w.t;body=`${w.k} · ${w.y}. Touches ${[...r.s].map(s=>SK[s]).join(", ")}. Read on blog.lucaswinther.info.`;}
|
||
insp.innerHTML=`<span class="mono">${{s:"Skill",p:"Project",w:"Post"}[kind]}${pinned?" · pinned":""}</span><div><h4>${esc(title)}</h4><p>${esc(body)}</p></div><span class="pin mono">${meta}</span>`;
|
||
}
|
||
function clear(){
|
||
map.classList.remove("active");svg.innerHTML="";
|
||
map.querySelectorAll(".node").forEach(n=>n.classList.remove("lit","src"));
|
||
insp.innerHTML=`<span class="mono">Inspector</span><div><h4>Point at anything above</h4><p>Click to pin a selection, click again or press Esc to release it.</p></div><span class="pin mono"></span>`;
|
||
}
|
||
map.addEventListener("mouseover",e=>{const n=e.target.closest(".node");if(n&&!pinned)activate(n.dataset.kind,n.dataset.id);});
|
||
map.addEventListener("mouseleave",()=>{if(!pinned)clear();});
|
||
map.addEventListener("focusin",e=>{const n=e.target.closest(".node");if(n&&!pinned)activate(n.dataset.kind,n.dataset.id);});
|
||
map.addEventListener("click",e=>{
|
||
const n=e.target.closest(".node");if(!n)return;
|
||
if(pinned&&pinned.id===n.dataset.id&&pinned.kind===n.dataset.kind){pinned=null;activate(n.dataset.kind,n.dataset.id);return;}
|
||
pinned={kind:n.dataset.kind,id:n.dataset.id};activate(pinned.kind,pinned.id);
|
||
});
|
||
document.addEventListener("keydown",e=>{if(e.key==="Escape"){pinned=null;clear();}});
|
||
window.addEventListener("resize",()=>{if(pinned)activate(pinned.kind,pinned.id);else clear();});
|
||
|
||
document.getElementById("seg").addEventListener("click",e=>{
|
||
const b=e.target.closest("button");if(!b)return;
|
||
filter=filter===b.dataset.ai?null:b.dataset.ai;pinned=null;renderSeg();render();
|
||
});
|
||
document.getElementById("reset").addEventListener("click",()=>{filter=null;renderSeg();render();});
|
||
|
||
function renderMatrix(){
|
||
const years=[];for(let y=2016;y<=2026;y++)years.push(y);
|
||
const vp=visP();
|
||
let h=`<tr><th></th>${years.map(y=>`<th>${String(y).slice(2)}</th>`).join("")}</tr>`;
|
||
GROUPS.forEach((g,gi)=>g[1].forEach(([id,n],si)=>{
|
||
h+=`<tr class="${si===0&&gi>0?"split":""}"><th class="r">${esc(n)}</th>`+years.map(y=>{
|
||
const ps=vp.filter(p=>p.y===y&&p.s.includes(id));
|
||
const lv=Math.min(ps.length,3);const ai=ps.some(p=>p.ai!=="none");
|
||
const tip=ps.length?`${n}, ${y}: ${ps.map(p=>p.t).join(", ")}`:`${n}, ${y}`;
|
||
return `<td${y===2023?' style="border-left:1px solid var(--muted)"':""}><i class="${lv?"l"+lv:""}${ai?" ai":""}" title="${esc(tip)}"></i></td>`;
|
||
}).join("")+`</tr>`;
|
||
}));
|
||
h+=`<tr class="era"><th></th><th colspan="7">by hand</th><th colspan="4" style="padding-left:4px">with AI</th></tr>`;
|
||
document.getElementById("matrix").innerHTML=h;
|
||
}
|
||
document.getElementById("writeList").innerHTML=POSTS.filter(p=>!p.real).slice(0,5).map(p=>`<li><a href="https://blog.lucaswinther.info"><span class="mono" style="color:var(--muted)"><span class="ph" title="placeholder">${p.y}</span></span><span><span class="tt ph" title="placeholder">${esc(p.t)}</span><span class="k">${p.k} · ${p.s.map(s=>SK[s]).join(", ")}</span></span></a></li>`).join("");
|
||
renderSeg();render();
|
||
</script>
|
||
</body>
|
||
</html>
|