gh-Dimillian-Skills/docs/app.js
Thomas Ricouard 2b99f8520f Add light/dark theme toggle to docs UI
Introduced a theme toggle button in the docs header, allowing users to switch between light and dark modes. Theme preference is saved in localStorage and applied on load. Updated CSS to support theme variables and improved code block styling for both themes.
2025-12-30 20:39:11 +01:00

343 lines
9.7 KiB
JavaScript

const skills = [
{
name: "App Store Changelog",
folder: "app-store-changelog",
description:
"Generate App Store release notes from git history with user-focused summaries.",
references: [
{
title: "Release notes guidelines",
file: "references/release-notes-guidelines.md",
},
],
},
{
name: "iOS Debugger Agent",
folder: "ios-debugger-agent",
description:
"Build, run, and debug iOS apps on simulators with UI interaction and log capture.",
references: [],
},
{
name: "Swift Concurrency Expert",
folder: "swift-concurrency-expert",
description:
"Review and remediate Swift 6.2+ concurrency issues with actor isolation and Sendable safety.",
references: [
{
title: "Swift 6.2 concurrency",
file: "references/swift-6-2-concurrency.md",
},
{
title: "SwiftUI concurrency tour",
file: "references/swiftui-concurrency-tour-wwdc.md",
},
],
},
{
name: "SwiftUI Liquid Glass",
folder: "swiftui-liquid-glass",
description:
"Adopt and review Liquid Glass APIs in SwiftUI with correct usage patterns and fallbacks.",
references: [
{
title: "Liquid Glass reference",
file: "references/liquid-glass.md",
},
],
},
{
name: "SwiftUI View Refactor",
folder: "swiftui-view-refactor",
description:
"Refactor SwiftUI views for consistent structure, dependency injection, and Observation usage.",
references: [
{
title: "MV patterns",
file: "references/mv-patterns.md",
},
],
},
{
name: "SwiftUI Performance Audit",
folder: "swiftui-performance-audit",
description:
"Code-first review for SwiftUI performance pitfalls with targeted fixes and profiling guidance.",
references: [
{
title: "Optimizing with Instruments",
file: "references/optimizing-swiftui-performance-instruments.md",
},
{
title: "Understanding SwiftUI performance",
file: "references/understanding-improving-swiftui-performance.md",
},
{
title: "Understanding hangs",
file: "references/understanding-hangs-in-your-app.md",
},
{
title: "Demystify SwiftUI performance",
file: "references/demystify-swiftui-performance-wwdc23.md",
},
],
},
];
const repoInfo = getRepoInfo();
const githubLink = document.getElementById("githubLink");
const themeToggle = document.getElementById("themeToggle");
const skillsList = document.getElementById("skillsList");
const skillTitle = document.getElementById("skillTitle");
const skillDescription = document.getElementById("skillDescription");
const skillUsage = document.getElementById("skillUsage");
const referenceBar = document.getElementById("referenceBar");
const markdownContent = document.getElementById("markdownContent");
githubLink.href = repoInfo
? `https://github.com/${repoInfo.owner}/${repoInfo.repo}`
: "#";
renderSkillList();
selectSkill(skills[0]);
initTheme();
function renderSkillList() {
skillsList.innerHTML = "";
skills.forEach((skill) => {
const item = document.createElement("button");
item.className = "skill-item";
item.type = "button";
item.dataset.folder = skill.folder;
const title = document.createElement("div");
title.className = "skill-item__title";
title.textContent = skill.name;
const meta = document.createElement("div");
meta.className = "skill-item__meta";
const badge = document.createElement("span");
badge.className = "skill-item__badge";
badge.textContent = `${skill.references.length} ref${skill.references.length === 1 ? "" : "s"}`;
const folder = document.createElement("span");
folder.textContent = skill.folder;
meta.append(badge, folder);
const preview = document.createElement("div");
preview.className = "skill-item__preview";
preview.textContent = truncateText(skill.description, 110);
item.append(title, meta, preview);
item.addEventListener("click", () => selectSkill(skill));
skillsList.append(item);
});
}
function selectSkill(skill) {
setActiveSkill(skill);
skillTitle.textContent = skill.name;
skillDescription.textContent = skill.description;
skillUsage.textContent = "";
renderReferenceBar(skill);
loadMarkdown(skill, "SKILL.md");
}
function setActiveSkill(skill) {
Array.from(skillsList.children).forEach((node) => {
node.classList.toggle("active", node.dataset.folder === skill.folder);
});
}
function renderReferenceBar(skill) {
referenceBar.innerHTML = "";
const mainButton = document.createElement("button");
mainButton.className = "reference-pill active";
mainButton.type = "button";
mainButton.textContent = "SKILL.md";
mainButton.addEventListener("click", () => {
setActiveReference(mainButton);
loadMarkdown(skill, "SKILL.md");
});
referenceBar.append(mainButton);
if (!skill.references.length) {
const empty = document.createElement("span");
empty.className = "muted";
empty.textContent = "No references";
referenceBar.append(empty);
return;
}
skill.references.forEach((ref) => {
const refButton = document.createElement("button");
refButton.className = "reference-pill";
refButton.type = "button";
refButton.textContent = ref.title;
refButton.addEventListener("click", () => {
setActiveReference(refButton);
loadMarkdown(skill, ref.file);
});
referenceBar.append(refButton);
});
}
function setActiveReference(activeButton) {
Array.from(referenceBar.querySelectorAll(".reference-pill")).forEach((node) => {
node.classList.toggle("active", node === activeButton);
});
}
function loadMarkdown(skill, filePath) {
const contentPath = buildContentPath(`${skill.folder}/${filePath}`);
fetch(contentPath)
.then((response) => {
if (!response.ok) {
throw new Error("Failed to load content");
}
return response.text();
})
.then((text) => {
const parsed = parseFrontmatter(text);
const content = stripH1(parsed.content, parsed.frontmatter.name || skill.name);
const overview = extractOverview(content);
updateHeader(parsed.frontmatter, overview, skill);
if (window.marked) {
markdownContent.innerHTML = window.marked.parse(content);
} else {
markdownContent.textContent = content;
}
})
.catch(() => {
markdownContent.textContent =
"Unable to load this document. Make sure the file exists and the server can reach it.";
});
}
function buildContentPath(path) {
if (!repoInfo) {
return `../${path}`;
}
return `https://raw.githubusercontent.com/${repoInfo.owner}/${repoInfo.repo}/main/${path}`;
}
function updateHeader(frontmatter, overview, skill) {
if (frontmatter.name) {
skillTitle.textContent = frontmatter.name;
}
if (frontmatter.description) {
skillDescription.textContent = frontmatter.description;
}
if (overview) {
skillUsage.textContent = `Usage: ${overview}`;
} else if (skill.description) {
skillUsage.textContent = `Usage: ${skill.description}`;
}
}
function parseFrontmatter(text) {
if (!text.startsWith("---")) {
return { frontmatter: {}, content: text };
}
const parts = text.split("\n");
let endIndex = -1;
for (let i = 1; i < parts.length; i += 1) {
if (parts[i].trim() === "---") {
endIndex = i;
break;
}
}
if (endIndex === -1) {
return { frontmatter: {}, content: text };
}
const yamlLines = parts.slice(1, endIndex);
const content = parts.slice(endIndex + 1).join("\n");
const frontmatter = {};
yamlLines.forEach((line) => {
const match = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
if (!match) return;
const key = match[1];
let value = match[2].trim();
if (value.startsWith("\"") && value.endsWith("\"")) {
value = value.slice(1, -1);
}
frontmatter[key] = value;
});
return { frontmatter, content };
}
function stripH1(content, title) {
const lines = content.split("\n");
if (!lines.length) return content;
const firstLine = lines[0].trim();
const normalizedTitle = (title || "").trim().toLowerCase();
if (firstLine.startsWith("#")) {
const heading = firstLine.replace(/^#+\s*/, "").trim().toLowerCase();
if (!normalizedTitle || heading === normalizedTitle) {
return lines.slice(1).join("\n").trimStart();
}
}
return content;
}
function extractOverview(content) {
const match = content.match(/##\s+Overview\s+([\s\S]*?)(\n##\s|$)/i);
if (!match) return "";
const body = match[1].trim();
const paragraph = body.split("\n\n")[0];
return paragraph.replace(/\s+/g, " ").trim();
}
function truncateText(text, maxLength) {
if (!text) return "";
if (text.length <= maxLength) return text;
return `${text.slice(0, maxLength - 1).trim()}`;
}
function getRepoInfo() {
const host = window.location.hostname;
const pathParts = window.location.pathname.split("/").filter(Boolean);
if (!host || host.indexOf(".github.io") === -1) {
return null;
}
const owner = host.split(".")[0];
const repo = pathParts[0];
if (!owner || !repo) {
return null;
}
return { owner, repo };
}
function initTheme() {
const saved = localStorage.getItem("codex-theme");
if (saved) {
setTheme(saved);
} else {
const prefersLight = window.matchMedia("(prefers-color-scheme: light)").matches;
setTheme(prefersLight ? "light" : "dark");
}
themeToggle.addEventListener("click", () => {
const next = document.documentElement.getAttribute("data-theme") === "dark" ? "light" : "dark";
setTheme(next);
localStorage.setItem("codex-theme", next);
});
}
function setTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
}