/* dlstack — distilled
* Loads data/links.json and renders sections, cards, YouTube facades.
* No search, no filter, no clock, no theme toggle. The page is the index.
*/
(() => {
"use strict";
const $ = (s, el = document) => el.querySelector(s);
const $$ = (s, el = document) => Array.from(el.querySelectorAll(s));
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
}[c]));
const frag = (html) => document.createRange().createContextualFragment(html);
const hostOf = (url) => {
try { return new URL(url, location.href).hostname.replace(/^www\./, ""); }
catch { return ""; }
};
const faviconFor = (url) => {
const h = hostOf(url);
return h ? `https://www.google.com/s2/favicons?domain=${encodeURIComponent(h)}&sz=128` : "";
};
const ytThumb = (id) => `https://i.ytimg.com/vi/${encodeURIComponent(id)}/hqdefault.jpg`;
function renderLink(it) {
const host = hostOf(it.url);
const custom = it.image || it.icon;
const src = custom || faviconFor(it.url);
const isFavicon = !custom;
const initial = (it.title || host || "·").trim().charAt(0).toUpperCase();
return `
${src
? `
`
: esc(initial)}
${esc(it.title)}
${it.description ? `${esc(it.description)}` : ""}
${host ? `${esc(host)}` : ""}
`;
}
function renderProject(it) {
const featured = it.featured ? " card--featured" : "";
const tag = it.url ? "a" : "div";
const attrs = it.url ? `href="${esc(it.url)}" target="_blank" rel="noopener noreferrer"` : "";
const tags = (it.tags || []).map(t => `${esc(t)}`).join("");
return `
<${tag} class="card card--project${featured} reveal" ${attrs}>
${esc(it.title)}
${it.description ? `
${esc(it.description)}
` : ""}
${tags ? `${tags}
` : ""}
${tag}>`;
}
function renderYouTube(it) {
return `
`;
}
function renderPortfolio(it) {
const src = it.image || it.url;
const tag = it.url ? "a" : "div";
const attrs = it.url ? `href="${esc(it.url)}" target="_blank" rel="noopener noreferrer"` : "";
const ratio = it.ratio ? ` style="--portfolio-ratio:${esc(String(it.ratio).replace(":", " / "))}"` : "";
return `
<${tag} class="card card--portfolio reveal" ${attrs}>
${(it.title || it.description) ? `
${it.title ? `${esc(it.title)}` : ""}
${it.description ? `${esc(it.description)}` : ""}
` : ""}
${tag}>`;
}
function renderClient(it) {
const host = hostOf(it.url);
const custom = it.image || it.icon;
const src = custom || faviconFor(it.url);
const isFavicon = !custom;
const initial = (it.title || host || "·").trim().charAt(0).toUpperCase();
const tag = it.url ? "a" : "div";
const attrs = it.url ? `href="${esc(it.url)}" target="_blank" rel="noopener noreferrer"` : "";
return `
<${tag} class="card card--client reveal" ${attrs} title="${esc(it.title || host || "")}">
${src
? `
`
: esc(initial)}
${it.title ? `${esc(it.title)}` : ""}
${tag}>`;
}
const renderItem = (it) =>
it.type === "youtube" ? renderYouTube(it) :
it.type === "card" ? renderProject(it) :
it.type === "client" ? renderClient(it) :
it.type === "portfolio" ? renderPortfolio(it) :
renderLink(it);
function renderSection(sec, n) {
const items = (sec.items || []).map(renderItem).join("");
const num = String(n).padStart(2, "0");
const isClients = sec.layout === "clients" || sec.items?.[0]?.type === "client";
const gridClass = isClients ? "grid--clients" : "grid";
return `
`;
}
const SOCIAL_ICONS = {
github: '',
linkedin: '',
mail: '',
rss: '',
link: '',
calendar: '',
bluesky: ''
};
function renderSocial(items) {
return items.map(s =>
`${SOCIAL_ICONS[s.icon] || SOCIAL_ICONS.link}${esc(s.label)}`
).join("");
}
function nameMarkup(name) {
if (!name) return "";
const parts = String(name).trim().split(/\s+/);
if (parts.length < 2) return esc(name);
const last = parts.pop();
return `${esc(parts.join(" "))} ${esc(last)}`;
}
function attachFaviconFallback(root) {
$$("img[data-fallback-initial]", root).forEach(img => {
img.addEventListener("error", () => {
const initial = img.dataset.fallbackInitial || "·";
const parent = img.parentElement;
if (parent) { parent.setAttribute("data-fallback", ""); parent.textContent = initial; }
}, { once: true });
});
}
function attachYouTube(root) {
const open = (fac) => {
const iframe = document.createElement("iframe");
iframe.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(fac.dataset.yt)}?autoplay=1&rel=0`;
iframe.allow = "accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture";
iframe.allowFullscreen = true;
iframe.title = fac.getAttribute("aria-label") || "YouTube video";
fac.replaceChildren(iframe);
fac.style.cursor = "default";
};
root.addEventListener("click", (e) => {
const fac = e.target.closest("[data-yt]");
if (fac) { e.preventDefault(); open(fac); }
});
root.addEventListener("keydown", (e) => {
if (e.key !== "Enter" && e.key !== " ") return;
const fac = e.target.closest("[data-yt]");
if (fac) { e.preventDefault(); open(fac); }
});
}
function attachTheme() {
const root = document.documentElement;
const btn = $("#theme");
if (!btn) return;
const order = ["auto", "light", "dark"];
const label = { auto: "Auto", light: "Light", dark: "Dark" };
const stored = localStorage.getItem("dlstack-theme");
if (!order.includes(root.dataset.theme)) root.dataset.theme = "auto";
if (order.includes(stored)) root.dataset.theme = stored;
const sync = () => {
btn.textContent = label[root.dataset.theme];
btn.setAttribute("aria-label", `Theme: ${label[root.dataset.theme]}. Click to change.`);
};
sync();
btn.addEventListener("click", () => {
const i = order.indexOf(root.dataset.theme);
root.dataset.theme = order[(i + 1) % order.length];
localStorage.setItem("dlstack-theme", root.dataset.theme);
sync();
});
}
function attachReveal(root) {
if (!("IntersectionObserver" in window)) {
$$(".reveal", root).forEach(el => el.classList.add("in"));
return;
}
const io = new IntersectionObserver((entries) => {
entries.forEach(en => {
if (en.isIntersecting) { en.target.classList.add("in"); io.unobserve(en.target); }
});
}, { threshold: 0.08, rootMargin: "0px 0px -40px 0px" });
$$(".reveal", root).forEach(el => io.observe(el));
}
async function loadData() {
const sources = ["data/links.json", "data/links.example.json"];
let lastErr;
for (const src of sources) {
try {
const res = await fetch(src, { cache: "no-cache" });
if (!res.ok) { lastErr = new Error(`${src}: HTTP ${res.status}`); continue; }
return await res.json();
} catch (err) { lastErr = err; }
}
throw lastErr || new Error("No data file found.");
}
async function main() {
const app = $("#app");
let data;
try {
data = await loadData();
} catch (err) {
const p = document.createElement("p");
p.style.cssText = "color:var(--accent);font-family:var(--mono);padding:2rem;max-width:48ch";
p.textContent = `Couldn't load data/links.json or data/links.example.json. Check that one exists and is valid JSON. Details: ${err.message}`;
app.replaceChildren(p);
return;
}
if (data.theme?.accent) {
document.documentElement.style.setProperty("--accent", data.theme.accent);
}
const tpl = data.theme?.template === "swiss" ? "swiss" : "editorial";
document.documentElement.dataset.template = tpl;
try { localStorage.setItem("dlstack-template", tpl); } catch (e) {}
const p = data.profile || {};
const sections = data.sections || [];
const social = data.social || [];
document.title = `${p.name || "Links"}`;
const taglineMarkup = (t) =>
esc(t).replace(/\s*[·•|]\s*/g, '·');
const html = `
✱ Index №01
MMXXVI
${sections.map((s, i) => renderSection(s, i + 1)).join("")}
`;
app.replaceChildren(frag(html));
attachFaviconFallback(app);
attachYouTube(app);
attachReveal(app);
attachTheme();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", main);
} else {
main();
}
})();