mirror of
https://github.com/litruv/Docs-Viewer.git
synced 2026-09-10 18:19:45 +10:00
Port Docs-Viewer to slatehtml as the docs-viewer package.
Replace the classic DOM shell with slate widgets, markdown→slate rendering, and a CI index build that uses build-docs.cjs.
This commit is contained in:
309
src/widgets/slate-docs-nav.umc
Normal file
309
src/widgets/slate-docs-nav.umc
Normal file
@@ -0,0 +1,309 @@
|
||||
--- 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>
|
||||
Reference in New Issue
Block a user