// Builds the page from window.FRY (content.js). Nothing here needs changing for a content update. (function () { const F = window.FRY; const $ = (id) => document.getElementById(id); const el = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }; // Feature icons: 24px line drawings in the text's colour. const ICONS = { cube: '', pen: '', combine: '', points: '', hole: '', copies: '', brush: '', layout: '', print: '', sun: '', folder: '', phone: '', save: '', code: '', sculpt: '', sphere: '', }; const icon = (name) => { const s = el("span", "feature__icon"); s.innerHTML = ``; return s; }; // ── Hero ── $("latest").textContent = `Version ${F.versions[0].v}: ${F.versions[0].items[0]}`; $("long").textContent = F.long; $("tagline").textContent = F.tagline; $("intro").textContent = F.intro; for (const id of ["gh-top", "gh-hero"]) $(id).href = F.repo; for (const d of F.downloads) $("platforms").append(el("span", "nv-badge", d.os)); $("author").textContent = F.author; $("author").href = F.authorUrl; $("why-title").textContent = F.why.title; for (const p of F.why.text) $("why-text").append(el("p", "nv-body", p)); // ── Features ── for (const f of F.features) { const card = el("article", "nv-card feature"); const body = el("div", "nv-card__body"); body.append(icon(f.icon), el("h3", "nv-card__title", f.title), el("p", "nv-body nv-text-secondary", f.text)); card.append(body); $("feature-grid").append(card); } // ── Screenshots: a tab each, the picture in a window ── F.shots.forEach((s, i) => { const tab = el("button", "nv-nav__item", s.title); Object.assign(tab, { id: `shot-tab-${i}`, role: "tab", tabIndex: i ? -1 : 0 }); tab.setAttribute("role", "tab"); tab.setAttribute("aria-controls", `shot-${i}`); tab.setAttribute("aria-selected", String(i === 0)); $("shot-tabs").append(tab); const panel = el("div", "nv-tab-panel shot"); Object.assign(panel, { id: `shot-${i}`, hidden: i !== 0 }); panel.setAttribute("role", "tabpanel"); panel.setAttribute("aria-labelledby", tab.id); const win = el("div", "nv-window"); const bar = el("div", "nv-window__titlebar"); const lights = el("div", "nv-window__controls"); for (const k of ["close", "minimize", "zoom"]) { const l = el("span", `nv-window__control nv-window__control--${k}`); l.setAttribute("aria-hidden", "true"); lights.append(l); } bar.append(lights, el("span", "nv-window__title", `Fry3DX — ${s.title}`)); const img = el("img", "shot__img"); Object.assign(img, { src: s.img, alt: s.title, loading: i ? "lazy" : "eager" }); win.append(bar, img); panel.append(win, el("p", "nv-body nv-text-secondary", s.text)); $("shot-panels").append(panel); }); // ── Scripting ── if (F.scripting) { const S = F.scripting; $("scripting").hidden = false; $("script-title").textContent = S.title; $("script-text").textContent = S.text; for (const p of S.points) $("script-points").append(el("li", "nv-body", p)); $("script-file").textContent = S.file; $("script-code").innerHTML = highlightLua(S.code); } // ── Files and keys ── const row = (label, items) => { const d = el("div", "fmt"); d.append(el("span", "nv-overline", label)); const c = el("div", "nv-cluster nv-cluster--tight"); for (const x of items) c.append(el("span", "nv-badge", x)); d.append(c); $("formats").append(d); }; row("Save", F.formats.save); row("Export", F.formats.export); $("formats").append(el("p", "nv-caption nv-text-tertiary", F.formats.note)); for (const [k, what] of F.keys) { const tr = el("tr"); const td = el("td"); td.append(el("kbd", null, k)); tr.append(td, el("td", "nv-body", what)); $("keys").append(tr); } // ── Downloads and building ── for (const d of F.downloads) { const a = el("a", "nv-card nv-card--interactive download"); a.href = F.releases; const b = el("div", "nv-card__body"); b.append(el("h3", "nv-card__title", d.os), el("p", "nv-caption nv-text-tertiary", d.what), el("code", "nv-code download__file", "Fry3DX-…-" + d.file), el("p", "nv-body nv-text-secondary", d.how)); a.append(b); $("downloads").append(a); } $("build-text").textContent = F.build.text; $("build-steps").textContent = F.build.steps.join("\n"); $("build-needs").textContent = F.build.needs; for (const btn of document.querySelectorAll("[data-copy]")) { btn.onclick = async () => { try { await navigator.clipboard.writeText($(btn.dataset.copy).textContent); btn.textContent = "Copied"; } catch { btn.textContent = "Select and copy"; } setTimeout(() => (btn.textContent = "Copy"), 1800); }; } // ── Versions ── F.versions.forEach((v, i) => { const d = el("details", "nv-disclosure"); d.open = i === 0; const s = el("summary", "nv-disclosure__summary"); s.append(el("span", "nv-title", "v" + v.v), el("span", "nv-caption nv-text-tertiary", " " + v.date)); const ch = el("span", "nv-disclosure__chevron"); ch.setAttribute("aria-hidden", "true"); s.append(ch); const body = el("div", "nv-disclosure__body"); const ul = el("ul", "ticks"); for (const it of v.items) ul.append(el("li", "nv-body", it)); body.append(ul); d.append(s, body); $("version-list").append(d); }); // ── The appearance, remembered; the bar's shadow once scrolled ── document.documentElement.addEventListener("nv:theme", (e) => { try { localStorage.setItem("fry-theme", e.detail?.theme ?? "system"); } catch (_) {} }); const saved = (() => { try { return localStorage.getItem("fry-theme"); } catch (_) { return null; } })(); if (saved) addEventListener("DOMContentLoaded", () => window.nv?.syncThemeControls(saved)); const nav = $("top-nav"); const onScroll = () => nav.toggleAttribute("data-scrolled", scrollY > 4); addEventListener("scroll", onScroll, { passive: true }); onScroll(); // A little Lua colouring for the scripting sample. function highlightLua(src) { const esc = (t) => t.replace(/&/g, "&").replace(/ { const at = line.indexOf("--"); const code = at >= 0 ? line.slice(0, at) : line, comment = at >= 0 ? line.slice(at) : ""; const parts = code.split(/("[^"]*")/); const out = parts.map((p) => (p.startsWith('"') ? `${esc(p)}` : esc(p).replace(words, '$1'))).join(""); return out + (comment ? `${esc(comment)}` : ""); }).join("\n"); } })();