mirror of
https://github.com/litruv/Docs-Viewer.git
synced 2026-09-10 10:09:46 +10:00
Replace the classic DOM shell with slate widgets, markdown→slate rendering, and a CI index build that uses build-docs.cjs.
310 lines
7.9 KiB
Plaintext
310 lines
7.9 KiB
Plaintext
--- html ---
|
|
<verticalbox class="docs-nav-shell" data-content gap="2" width="100%"></verticalbox>
|
|
|
|
--- 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 ---
|
|
<slate-docs-nav selected="welcome"></slate-docs-nav>
|