--- html ---
--- style ---
self {
display: block; /* umc-layout-ok */
box-sizing: border-box;
width: 100%; /* umc-layout-ok */
color: inherit;
font: inherit;
min-width: 0;
}
.docs-nav-folder {
--widget-padding: 0;
--widget-border: none;
--widget-background: transparent;
}
.docs-nav-folder-face {
--widget-padding: 0.4rem 0.8rem;
--widget-radius: 4px;
--widget-border: none;
--widget-background: transparent;
cursor: pointer;
}
.docs-nav-folder-face:hover {
--widget-background: rgba(255, 255, 255, 0.04);
background: var(--widget-background);
}
.docs-nav-folder-label {
font-size: 0.875rem;
font-weight: 600;
}
.docs-nav-folder-body {
padding-left: 0.5rem; /* umc-layout-ok */
}
.docs-nav-link {
--widget-padding: 0.4rem 0.8rem;
--widget-radius: 4px;
--widget-border: none;
--widget-background: transparent;
cursor: pointer;
}
.docs-nav-link:hover {
--widget-background: rgba(255, 255, 255, 0.04);
background: var(--widget-background);
}
.docs-nav-link[active] {
--widget-background: color-mix(in srgb, var(--accent, #2196f3) 28%, transparent);
background: var(--widget-background);
color: #fff;
}
.docs-nav-link-label {
font-size: 0.875rem;
}
.docs-nav-link-icon,
.docs-nav-folder-icon {
color: var(--muted, #a0a0a0);
flex: 0 0 auto; /* umc-layout-ok */
}
.docs-nav-link[active] .docs-nav-link-icon {
color: inherit;
}
--- script ---
/**
* Document tree for the left rail.
* Uses index.json icons (FA class strings) when present — same as classic Docs-Viewer.
* Attrs: selected (slug)
* Events: navigate { slug }
*/
function folderOpen(doc, selected, parents) {
if (doc.defaultOpen) return true;
if (selected && parents.has(doc.slug)) return true;
return false;
}
function parentSlugSet(documents, selectedSlug) {
const parents = new Set();
if (!selectedSlug) return parents;
walk(documents, []);
function walk(docs, trail) {
for (const doc of docs || []) {
if (doc.slug === selectedSlug) {
for (const s of trail) parents.add(s);
return true;
}
if (doc.type === "folder" && doc.items) {
if (walk(doc.items, [...trail, doc.slug])) return true;
}
}
return false;
}
return parents;
}
/** FA class list from index.json → slate-icon name (fas:plug, fab:github, …). */
function faToSlateIcon(raw, fallback = "") {
const parts = String(raw || "")
.split(/\s+/)
.filter(Boolean);
if (!parts.length) return fallback;
const namePart = parts.find(
(p) =>
p.startsWith("fa-") &&
!["fa-solid", "fa-brands", "fa-regular", "fa-sharp", "fas", "fab", "far", "fa"].includes(p)
);
if (!namePart) return fallback;
let name = namePart.replace(/^fa-/, "");
// Classic Docs-Viewer used file-alt; FA6 free solid aliases file-lines.
if (name === "file-alt") name = "file-lines";
if (parts.some((p) => p.includes("brands") || p === "fab")) return `fab:${name}`;
if (parts.some((p) => p.includes("regular") || p === "far")) return `far:${name}`;
return `fas:${name}`;
}
function folderIcon(doc, open) {
if (doc.icon) return faToSlateIcon(doc.icon, "fas:folder");
return open ? "fas:folder-open" : "fas:folder";
}
function fileIcon(doc) {
if (!doc.icon) return "";
return faToSlateIcon(doc.icon, "");
}
function linkSpec(doc, selected) {
const icon = fileIcon(doc);
const row = [];
if (icon) {
row.push({
tag: "slate-icon",
class: "docs-nav-link-icon",
name: icon,
size: "13",
});
}
row.push({
tag: "textblock",
class: "docs-nav-link-label",
text: doc.title || doc.slug,
});
return {
tag: "border",
class: "docs-nav-link",
active: doc.slug === selected ? "" : undefined,
role: "link",
tabindex: "0",
"data-slug": doc.slug,
children: [
{
tag: "horizontalbox",
gap: icon ? "8" : "0",
valign: "center",
children: row,
},
],
};
}
function folderSpec(doc, selected, parents) {
const open = folderOpen(doc, selected, parents);
const kids = [];
if (doc.path && doc.showfolderpage !== "false") kids.push(linkSpec(doc, selected));
for (const item of doc.items || []) {
kids.push(item.type === "folder" ? folderSpec(item, selected, parents) : linkSpec(item, selected));
}
const custom = !!doc.icon;
return {
tag: "verticalbox",
class: "docs-nav-folder",
gap: "2",
"data-folder": doc.slug,
"data-folder-custom-icon": custom ? "" : undefined,
children: [
{
tag: "border",
class: "docs-nav-folder-face",
role: "button",
tabindex: "0",
"data-toggle-folder": doc.slug,
children: [
{
tag: "horizontalbox",
gap: "8",
valign: "center",
children: [
{
tag: "slate-icon",
class: "docs-nav-folder-icon",
name: folderIcon(doc, open),
size: "13",
"data-folder-icon": "",
},
{
tag: "textblock",
class: "docs-nav-folder-label",
text: doc.title || doc.slug,
},
],
},
],
},
{
tag: "verticalbox",
class: "docs-nav-folder-body",
gap: "2",
hidden: open ? undefined : "",
"data-folder-body": doc.slug,
children: kids,
},
],
};
}
function buildTree(documents, selected) {
const parents = parentSlugSet(documents, selected);
return (documents || []).map((doc) =>
doc.type === "folder" ? folderSpec(doc, selected, parents) : linkSpec(doc, selected)
);
}
export default defineUmc({
tag: "slate-docs-nav",
attrs: {
selected: "",
documents: "",
},
Construct(el, api) {
el.setDocuments = (docs) => {
el.__documents = Array.isArray(docs) ? docs : [];
el.__navDirty = true;
sync(el, api);
};
el.addEventListener("click", (e) => {
const toggle = e.target.closest?.("[data-toggle-folder]");
if (toggle && el.contains(toggle)) {
const slug = toggle.getAttribute("data-toggle-folder");
const folder = el.querySelector(`[data-folder="${CSS.escape(slug)}"]`);
const body = el.querySelector(`[data-folder-body="${CSS.escape(slug)}"]`);
const icon = toggle.querySelector("[data-folder-icon]");
if (body) {
const open = body.hasAttribute("hidden");
if (open) body.removeAttribute("hidden");
else body.setAttribute("hidden", "");
// Classic Docs-Viewer: only default folder icons swap open/closed.
if (icon && folder && !folder.hasAttribute("data-folder-custom-icon")) {
icon.setAttribute("name", open ? "fas:folder-open" : "fas:folder");
}
}
return;
}
const link = e.target.closest?.("[data-slug]");
if (link && el.contains(link)) {
api.emit("navigate", { slug: link.getAttribute("data-slug") });
}
});
el.addEventListener("keydown", (e) => {
if (e.key !== "Enter" && e.key !== " ") return;
const link = e.target.closest?.("[data-slug], [data-toggle-folder]");
if (!link || !el.contains(link)) return;
e.preventDefault();
link.click();
});
},
SynchronizeProperties(el, api) {
sync(el, api);
},
});
function sync(el, api) {
let docs = el.__documents;
if (!docs) {
const raw = el.getAttribute("documents") || "";
if (raw) {
try {
docs = JSON.parse(raw);
} catch {
docs = [];
}
} else {
docs = [];
}
}
const selected = el.getAttribute("selected") || "";
const key = `${selected}::${docs.length}::${el.__navDirty || false}`;
if (el.__navKey === key && !el.__navDirty) return;
el.__navKey = key;
el.__navDirty = false;
el.set(...buildTree(docs, selected));
}
--- preview ---