commit 47ba205af44ff590f48935478dea74c436e1f040
parent 19b356d368a3e14b10d5c2d4f821137b175c9498
Author: breadcat <breadcat@users.noreply.github.com>
Date: Thu, 19 Mar 2026 16:39:36 +0000
Just embed the whole damn template in the source
Diffstat:
| M | main.go | | | 264 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- |
| M | readme.md | | | 2 | -- |
| D | templates/listing.html | | | 259 | ------------------------------------------------------------------------------- |
3 files changed, 262 insertions(+), 263 deletions(-)
diff --git a/main.go b/main.go
@@ -49,6 +49,266 @@ type PageData struct {
TreeJSON template.JS
}
+const tmpl = `<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>{{ .CurrentPath }}</title>
+<style>
+*,::after,::before{box-sizing:border-box;margin:0;padding:0}
+a:hover{text-decoration:underline}
+body,html{height:100%;overflow:hidden}
+body{background:#1a1a1a;color:#d4d4d4;font-family:Consolas,Menlo,'DejaVu Sans Mono',monospace;font-size:13px;display:flex;flex-direction:column}
+footer{padding:6px 16px;color:#777}
+header{border-bottom:1px solid #333;padding:10px 16px;display:flex;align-items:center;gap:12px}
+@media (max-width: 640px) { .sidebar { display: none; } .file-table td.date { display: none; } .file-table td.size { min-width: 50px; } .thumb-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; padding: 8px; } }
+.breadcrumb .current{color:#777}
+.breadcrumb .sep{color:#777;margin:0 2px}
+.breadcrumb a,.file-table thead th:hover,.thumb-label a{color:#d4d4d4}
+.breadcrumb a:hover,.thumb-label a:hover,a{color:#5b9bd5;text-decoration:none}
+.breadcrumb,.tree-item,.view-toggle{display:flex;gap:4px}
+.breadcrumb{align-items:center;flex-wrap:wrap;flex:1}
+.empty{padding:48px;text-align:center;color:#777}
+.file-icon.dir,.thumb-preview .dir-icon{color:#c8a94e}
+.file-icon.img{color:#7cbf7c}
+.file-icon{margin-right:6px;color:#777}
+.file-table tbody tr,.file-table thead th{border-bottom:1px solid #333}
+.file-table tbody tr:hover,.tree-item.active,.tree-item:hover{background:#2e2e2e}
+.file-table td.date{color:#777;min-width:130px}
+.file-table td.name{width:100%;white-space:normal;word-break:break-all}
+.file-table td.size{color:#777;text-align:right;min-width:70px}
+.file-table td{padding:7px 12px;white-space:nowrap}
+.file-table thead th,.tree-item{cursor:pointer;user-select:none;white-space:nowrap}
+.file-table thead th.sort-asc::after{content:' ↑';color:#5b9bd5}
+.file-table thead th.sort-desc::after{content:' ↓';color:#5b9bd5}
+.file-table thead th{background:#222;padding:8px 12px;text-align:left;font-weight:400;color:#777;font-size:11px;letter-spacing:.08em;text-transform:uppercase}
+.file-table thead{position:sticky;top:0}
+.file-table{width:100%;border-collapse:collapse}
+.layout{display:flex;flex:1;overflow:hidden}
+.main{flex:1;overflow-y:auto}
+.sidebar,.thumb-item,footer,header{background:#222}
+.sidebar{width:220px;min-width:220px;border-right:1px solid #333;overflow-y:auto;overflow-x:hidden;padding:8px 0;flex-shrink:0}
+.sidebar-title{font-size:10px;letter-spacing:.1em;color:#777;padding:4px 12px 8px;text-transform:uppercase}
+.thumb-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;padding:16px}
+.thumb-item:hover{border-color:#5b9bd5}
+.thumb-item{border:1px solid #333;overflow:hidden}
+.thumb-label,footer{border-top:1px solid #333;font-size:11px}
+.thumb-label{padding:6px 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.thumb-meta{font-size:10px;color:#777;padding:0 8px 6px}
+.thumb-preview .big-icon{font-size:36px;color:#777}
+.thumb-preview img{width:100%;height:100%;object-fit:cover}
+.thumb-preview{width:100%;height:110px;display:flex;align-items:center;justify-content:center;background:#2a2a2a;overflow:hidden}
+.tree-children.open{display:block}
+.tree-children{display:none;padding-left:12px}
+.tree-icon{color:#777;flex-shrink:0}
+.tree-item.active{color:#5b9bd5}
+.tree-item{align-items:center;padding:3px 8px;overflow:hidden;color:#d4d4d4;font-size:12px}
+.tree-label{overflow:hidden;text-overflow:ellipsis}
+.tree-root{list-style:none}
+.tree-toggle{width:12px;text-align:center;color:#777;flex-shrink:0;font-size:10px}
+.view-toggle button.active{background:#1a1a1a;color:#5b9bd5;border-color:#5b9bd5}
+.view-toggle button:hover{background:#2e2e2e;color:#d4d4d4}
+.view-toggle button{background:#2a2a2a;border:1px solid #333;color:#777;padding:4px 10px;cursor:pointer;font-size:12px;font-family:inherit}
+.view-toggle{margin-left:auto}
+</style>
+</head>
+<body>
+
+<header>
+ <span>📁</span>
+ <div class="breadcrumb">
+ {{ range $i, $crumb := .Breadcrumbs }}
+ {{ if gt $i 0 }}<span class="sep">/</span>{{ end }}
+ {{ if eq $i (sub1 (len $.Breadcrumbs)) }}
+ <span class="current">{{ $crumb.Name }}</span>
+ {{ else }}
+ <a href="{{ $crumb.Path }}">{{ $crumb.Name }}</a>
+ {{ end }}
+ {{ end }}
+ </div>
+ <div class="view-toggle">
+ <button id="btn-list" class="active" onclick="setView('list')">☰ List</button>
+ <button id="btn-thumb" onclick="setView('thumb')">▦ Thumbs</button>
+ </div>
+</header>
+
+<div class="layout">
+ <nav class="sidebar">
+ <div class="sidebar-title">Folders</div>
+ <ul class="tree-root" id="tree-root"></ul>
+ </nav>
+
+ <main class="main">
+ <div id="view-list">
+ {{ if .Files }}
+ <table class="file-table" id="file-table">
+ <thead>
+ <tr>
+ <th class="name" onclick="sortTable(0)">Name</th>
+ <th class="size" onclick="sortTable(1)">Size</th>
+ <th class="date" onclick="sortTable(2)">Modified</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{ if ne .CurrentPath "/" }}
+ <tr>
+ <td class="name"><span class="file-icon dir">📁</span><a href="../">../</a></td>
+ <td class="size">—</td>
+ <td class="date">—</td>
+ </tr>
+ {{ end }}
+ {{ range .Files }}
+ <tr data-name="{{ .Name }}" data-size="{{ .Size }}" data-date="{{ .ModTime.Unix }}">
+ <td class="name">
+ {{ if .IsDir }}
+ <span class="file-icon dir">📁</span><a href="{{ .Path }}">{{ .Name }}/</a>
+ {{ else if isImage .Ext }}
+ <span class="file-icon img">📷</span><a href="{{ .Path }}">{{ .Name }}</a>
+ {{ else }}
+ <span class="file-icon">📄</span><a href="{{ .Path }}">{{ .Name }}</a>
+ {{ end }}
+ </td>
+ <td class="size">{{ if .IsDir }}—{{ else }}{{ formatSize .Size }}{{ end }}</td>
+ <td class="date">{{ formatTime .ModTime }}</td>
+ </tr>
+ {{ end }}
+ </tbody>
+ </table>
+ {{ else }}
+ <div class="empty">Empty directory</div>
+ {{ end }}
+ </div>
+
+ <div id="view-thumb" style="display:none">
+ {{ if .Files }}
+ <div class="thumb-grid">
+ {{ if ne .CurrentPath "/" }}
+ <div class="thumb-item">
+ <a href="../" style="display:block">
+ <div class="thumb-preview"><span class="big-icon dir-icon">↑</span></div>
+ <div class="thumb-label">../</div>
+ </a>
+ </div>
+ {{ end }}
+ {{ range .Files }}
+ <div class="thumb-item">
+ <a href="{{ .Path }}" style="display:block">
+ <div class="thumb-preview">
+ {{ if .IsDir }}
+ <span class="big-icon dir-icon">📁</span>
+ {{ else if isImage .Ext }}
+ <img src="/__thumb{{ .Path }}" alt="{{ .Name }}" loading="lazy">
+ {{ else }}
+ <span class="big-icon">📄</span>
+ {{ end }}
+ </div>
+ <div class="thumb-label" title="{{ .Name }}">{{ .Name }}{{ if .IsDir }}/{{ end }}</div>
+ <div class="thumb-meta">{{ if .IsDir }}folder{{ else }}{{ formatSize .Size }}{{ end }}</div>
+ </a>
+ </div>
+ {{ end }}
+ </div>
+ {{ else }}
+ <div class="empty">Empty directory</div>
+ {{ end }}
+ </div>
+ </main>
+</div>
+
+<footer>{{ len .Files }} item(s) · {{ .CurrentPath }}</footer>
+
+<script>
+const TREE_DATA = {{ .TreeJSON }};
+const CURRENT_PATH = {{ printf "%q" .CurrentPath }};
+
+function setView(v) {
+ const isList = v === 'list';
+ document.getElementById('view-list').style.display = isList ? '' : 'none';
+ document.getElementById('view-thumb').style.display = isList ? 'none' : '';
+ document.getElementById('btn-list').className = isList ? 'active' : '';
+ document.getElementById('btn-thumb').className = isList ? '' : 'active';
+}
+
+let sortCol = -1, sortDir = 1;
+function sortTable(col) {
+ const tbody = document.querySelector('#file-table tbody');
+ const rows = Array.from(tbody.querySelectorAll('tr'));
+ const parentRow = rows.find(r => r.querySelector('a[href="../"]'));
+ const dataRows = rows.filter(r => r !== parentRow);
+
+ if (sortCol === col) sortDir *= -1;
+ else { sortCol = col; sortDir = 1; }
+
+ dataRows.sort((a, b) => {
+ let av, bv;
+ if (col === 0) { av = (a.dataset.name||'').toLowerCase(); bv = (b.dataset.name||'').toLowerCase(); }
+ else if (col === 1) { av = parseInt(a.dataset.size||0); bv = parseInt(b.dataset.size||0); }
+ else { av = parseInt(a.dataset.date||0); bv = parseInt(b.dataset.date||0); }
+ return av < bv ? -sortDir : av > bv ? sortDir : 0;
+ });
+
+ document.querySelectorAll('#file-table thead th').forEach((th, i) => {
+ th.className = th.className.replace(/ ?sort-(asc|desc)/g, '');
+ if (i === col) th.className += sortDir > 0 ? ' sort-asc' : ' sort-desc';
+ });
+
+ tbody.innerHTML = '';
+ if (parentRow) tbody.appendChild(parentRow);
+ dataRows.forEach(r => tbody.appendChild(r));
+}
+
+function buildTree(nodes, container, depth) {
+ if (!nodes || !nodes.length) return;
+ nodes.forEach(node => {
+ const li = document.createElement('li');
+ const item = document.createElement('div');
+ item.className = 'tree-item' + (node.path === CURRENT_PATH ? ' active' : '');
+ item.style.paddingLeft = (8 + depth * 12) + 'px';
+
+ const toggle = document.createElement('span');
+ toggle.className = 'tree-toggle';
+ toggle.textContent = (node.children && node.children.length) ? '▶' : ' ';
+
+ const icon = document.createElement('span');
+ icon.className = 'tree-icon';
+ icon.textContent = '📁';
+
+ const label = document.createElement('span');
+ label.className = 'tree-label';
+ label.textContent = node.name;
+
+ item.append(toggle, icon, label);
+ li.appendChild(item);
+
+ let childUl = null;
+ if (node.children && node.children.length) {
+ childUl = document.createElement('ul');
+ childUl.className = 'tree-children';
+ buildTree(node.children, childUl, depth + 1);
+ li.appendChild(childUl);
+ toggle.addEventListener('click', e => {
+ e.stopPropagation();
+ toggle.textContent = childUl.classList.toggle('open') ? '▼' : '▶';
+ });
+ }
+
+ item.addEventListener('click', () => { window.location.href = node.path; });
+
+ if (CURRENT_PATH.startsWith(node.path) && childUl) {
+ childUl.classList.add('open');
+ toggle.textContent = '▼';
+ }
+
+ container.appendChild(li);
+ });
+}
+
+buildTree(TREE_DATA, document.getElementById('tree-root'), 0);
+</script>
+</body>
+</html>`
+
func main() {
var dir string
var port int
@@ -166,14 +426,14 @@ func listingHandler(w http.ResponseWriter, r *http.Request) {
"sub1": func(n int) int { return n - 1 },
}
- tmpl, err := template.New("listing.html").Funcs(funcMap).ParseFiles("templates/listing.html")
+ t, err := template.New("page").Funcs(funcMap).Parse(tmpl)
if err != nil {
http.Error(w, "Template error: "+err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
- if err := tmpl.Execute(w, data); err != nil {
+ if err := t.Execute(w, data); err != nil {
log.Printf("Template error: %v", err)
}
}
diff --git a/readme.md b/readme.md
@@ -15,8 +15,6 @@ Then access the servers IP address via a web browser on port `8080`.
The application is also buildable via your usual `go build -o bruschetta .`.
-Note: the `templates/` directory must be alongside the binary at runtime.
-
## Features
* List and thumbnail view
* Thumbnails generated on the fly and cached
diff --git a/templates/listing.html b/templates/listing.html
@@ -1,259 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-<meta charset="UTF-8">
-<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<title>{{ .CurrentPath }}</title>
-<style>
-*,::after,::before{box-sizing:border-box;margin:0;padding:0}
-a:hover{text-decoration:underline}
-body,html{height:100%;overflow:hidden}
-body{background:#1a1a1a;color:#d4d4d4;font-family:Consolas,Menlo,'DejaVu Sans Mono',monospace;font-size:13px;display:flex;flex-direction:column}
-footer{padding:6px 16px;color:#777}
-header{border-bottom:1px solid #333;padding:10px 16px;display:flex;align-items:center;gap:12px}
-@media (max-width: 640px) { .sidebar { display: none; } .file-table td.date { display: none; } .file-table td.size { min-width: 50px; } .thumb-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; padding: 8px; } }
-.breadcrumb .current{color:#777}
-.breadcrumb .sep{color:#777;margin:0 2px}
-.breadcrumb a,.file-table thead th:hover,.thumb-label a{color:#d4d4d4}
-.breadcrumb a:hover,.thumb-label a:hover,a{color:#5b9bd5;text-decoration:none}
-.breadcrumb,.tree-item,.view-toggle{display:flex;gap:4px}
-.breadcrumb{align-items:center;flex-wrap:wrap;flex:1}
-.empty{padding:48px;text-align:center;color:#777}
-.file-icon.dir,.thumb-preview .dir-icon{color:#c8a94e}
-.file-icon.img{color:#7cbf7c}
-.file-icon{margin-right:6px;color:#777}
-.file-table tbody tr,.file-table thead th{border-bottom:1px solid #333}
-.file-table tbody tr:hover,.tree-item.active,.tree-item:hover{background:#2e2e2e}
-.file-table td.date{color:#777;min-width:130px}
-.file-table td.name{width:100%;white-space:normal;word-break:break-all}
-.file-table td.size{color:#777;text-align:right;min-width:70px}
-.file-table td{padding:7px 12px;white-space:nowrap}
-.file-table thead th,.tree-item{cursor:pointer;user-select:none;white-space:nowrap}
-.file-table thead th.sort-asc::after{content:' ↑';color:#5b9bd5}
-.file-table thead th.sort-desc::after{content:' ↓';color:#5b9bd5}
-.file-table thead th{background:#222;padding:8px 12px;text-align:left;font-weight:400;color:#777;font-size:11px;letter-spacing:.08em;text-transform:uppercase}
-.file-table thead{position:sticky;top:0}
-.file-table{width:100%;border-collapse:collapse}
-.layout{display:flex;flex:1;overflow:hidden}
-.main{flex:1;overflow-y:auto}
-.sidebar,.thumb-item,footer,header{background:#222}
-.sidebar{width:220px;min-width:220px;border-right:1px solid #333;overflow-y:auto;overflow-x:hidden;padding:8px 0;flex-shrink:0}
-.sidebar-title{font-size:10px;letter-spacing:.1em;color:#777;padding:4px 12px 8px;text-transform:uppercase}
-.thumb-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;padding:16px}
-.thumb-item:hover{border-color:#5b9bd5}
-.thumb-item{border:1px solid #333;overflow:hidden}
-.thumb-label,footer{border-top:1px solid #333;font-size:11px}
-.thumb-label{padding:6px 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-.thumb-meta{font-size:10px;color:#777;padding:0 8px 6px}
-.thumb-preview .big-icon{font-size:36px;color:#777}
-.thumb-preview img{width:100%;height:100%;object-fit:cover}
-.thumb-preview{width:100%;height:110px;display:flex;align-items:center;justify-content:center;background:#2a2a2a;overflow:hidden}
-.tree-children.open{display:block}
-.tree-children{display:none;padding-left:12px}
-.tree-icon{color:#777;flex-shrink:0}
-.tree-item.active{color:#5b9bd5}
-.tree-item{align-items:center;padding:3px 8px;overflow:hidden;color:#d4d4d4;font-size:12px}
-.tree-label{overflow:hidden;text-overflow:ellipsis}
-.tree-root{list-style:none}
-.tree-toggle{width:12px;text-align:center;color:#777;flex-shrink:0;font-size:10px}
-.view-toggle button.active{background:#1a1a1a;color:#5b9bd5;border-color:#5b9bd5}
-.view-toggle button:hover{background:#2e2e2e;color:#d4d4d4}
-.view-toggle button{background:#2a2a2a;border:1px solid #333;color:#777;padding:4px 10px;cursor:pointer;font-size:12px;font-family:inherit}
-.view-toggle{margin-left:auto}
-</style>
-</head>
-<body>
-
-<header>
- <span>📁</span>
- <div class="breadcrumb">
- {{ range $i, $crumb := .Breadcrumbs }}
- {{ if gt $i 0 }}<span class="sep">/</span>{{ end }}
- {{ if eq $i (sub1 (len $.Breadcrumbs)) }}
- <span class="current">{{ $crumb.Name }}</span>
- {{ else }}
- <a href="{{ $crumb.Path }}">{{ $crumb.Name }}</a>
- {{ end }}
- {{ end }}
- </div>
- <div class="view-toggle">
- <button id="btn-list" class="active" onclick="setView('list')">☰ List</button>
- <button id="btn-thumb" onclick="setView('thumb')">▦ Thumbs</button>
- </div>
-</header>
-
-<div class="layout">
- <nav class="sidebar">
- <div class="sidebar-title">Folders</div>
- <ul class="tree-root" id="tree-root"></ul>
- </nav>
-
- <main class="main">
- <div id="view-list">
- {{ if .Files }}
- <table class="file-table" id="file-table">
- <thead>
- <tr>
- <th class="name" onclick="sortTable(0)">Name</th>
- <th class="size" onclick="sortTable(1)">Size</th>
- <th class="date" onclick="sortTable(2)">Modified</th>
- </tr>
- </thead>
- <tbody>
- {{ if ne .CurrentPath "/" }}
- <tr>
- <td class="name"><span class="file-icon dir">📁</span><a href="../">../</a></td>
- <td class="size">—</td>
- <td class="date">—</td>
- </tr>
- {{ end }}
- {{ range .Files }}
- <tr data-name="{{ .Name }}" data-size="{{ .Size }}" data-date="{{ .ModTime.Unix }}">
- <td class="name">
- {{ if .IsDir }}
- <span class="file-icon dir">📁</span><a href="{{ .Path }}">{{ .Name }}/</a>
- {{ else if isImage .Ext }}
- <span class="file-icon img">📷</span><a href="{{ .Path }}">{{ .Name }}</a>
- {{ else }}
- <span class="file-icon">📄</span><a href="{{ .Path }}">{{ .Name }}</a>
- {{ end }}
- </td>
- <td class="size">{{ if .IsDir }}—{{ else }}{{ formatSize .Size }}{{ end }}</td>
- <td class="date">{{ formatTime .ModTime }}</td>
- </tr>
- {{ end }}
- </tbody>
- </table>
- {{ else }}
- <div class="empty">Empty directory</div>
- {{ end }}
- </div>
-
- <div id="view-thumb" style="display:none">
- {{ if .Files }}
- <div class="thumb-grid">
- {{ if ne .CurrentPath "/" }}
- <div class="thumb-item">
- <a href="../" style="display:block">
- <div class="thumb-preview"><span class="big-icon dir-icon">↑</span></div>
- <div class="thumb-label">../</div>
- </a>
- </div>
- {{ end }}
- {{ range .Files }}
- <div class="thumb-item">
- <a href="{{ .Path }}" style="display:block">
- <div class="thumb-preview">
- {{ if .IsDir }}
- <span class="big-icon dir-icon">📁</span>
- {{ else if isImage .Ext }}
- <img src="/__thumb{{ .Path }}" alt="{{ .Name }}" loading="lazy">
- {{ else }}
- <span class="big-icon">📄</span>
- {{ end }}
- </div>
- <div class="thumb-label" title="{{ .Name }}">{{ .Name }}{{ if .IsDir }}/{{ end }}</div>
- <div class="thumb-meta">{{ if .IsDir }}folder{{ else }}{{ formatSize .Size }}{{ end }}</div>
- </a>
- </div>
- {{ end }}
- </div>
- {{ else }}
- <div class="empty">Empty directory</div>
- {{ end }}
- </div>
- </main>
-</div>
-
-<footer>{{ len .Files }} item(s) · {{ .CurrentPath }}</footer>
-
-<script>
-const TREE_DATA = {{ .TreeJSON }};
-const CURRENT_PATH = {{ printf "%q" .CurrentPath }};
-
-function setView(v) {
- const isList = v === 'list';
- document.getElementById('view-list').style.display = isList ? '' : 'none';
- document.getElementById('view-thumb').style.display = isList ? 'none' : '';
- document.getElementById('btn-list').className = isList ? 'active' : '';
- document.getElementById('btn-thumb').className = isList ? '' : 'active';
-}
-
-let sortCol = -1, sortDir = 1;
-function sortTable(col) {
- const tbody = document.querySelector('#file-table tbody');
- const rows = Array.from(tbody.querySelectorAll('tr'));
- const parentRow = rows.find(r => r.querySelector('a[href="../"]'));
- const dataRows = rows.filter(r => r !== parentRow);
-
- if (sortCol === col) sortDir *= -1;
- else { sortCol = col; sortDir = 1; }
-
- dataRows.sort((a, b) => {
- let av, bv;
- if (col === 0) { av = (a.dataset.name||'').toLowerCase(); bv = (b.dataset.name||'').toLowerCase(); }
- else if (col === 1) { av = parseInt(a.dataset.size||0); bv = parseInt(b.dataset.size||0); }
- else { av = parseInt(a.dataset.date||0); bv = parseInt(b.dataset.date||0); }
- return av < bv ? -sortDir : av > bv ? sortDir : 0;
- });
-
- document.querySelectorAll('#file-table thead th').forEach((th, i) => {
- th.className = th.className.replace(/ ?sort-(asc|desc)/g, '');
- if (i === col) th.className += sortDir > 0 ? ' sort-asc' : ' sort-desc';
- });
-
- tbody.innerHTML = '';
- if (parentRow) tbody.appendChild(parentRow);
- dataRows.forEach(r => tbody.appendChild(r));
-}
-
-function buildTree(nodes, container, depth) {
- if (!nodes || !nodes.length) return;
- nodes.forEach(node => {
- const li = document.createElement('li');
- const item = document.createElement('div');
- item.className = 'tree-item' + (node.path === CURRENT_PATH ? ' active' : '');
- item.style.paddingLeft = (8 + depth * 12) + 'px';
-
- const toggle = document.createElement('span');
- toggle.className = 'tree-toggle';
- toggle.textContent = (node.children && node.children.length) ? '▶' : ' ';
-
- const icon = document.createElement('span');
- icon.className = 'tree-icon';
- icon.textContent = '📁';
-
- const label = document.createElement('span');
- label.className = 'tree-label';
- label.textContent = node.name;
-
- item.append(toggle, icon, label);
- li.appendChild(item);
-
- let childUl = null;
- if (node.children && node.children.length) {
- childUl = document.createElement('ul');
- childUl.className = 'tree-children';
- buildTree(node.children, childUl, depth + 1);
- li.appendChild(childUl);
- toggle.addEventListener('click', e => {
- e.stopPropagation();
- toggle.textContent = childUl.classList.toggle('open') ? '▼' : '▶';
- });
- }
-
- item.addEventListener('click', () => { window.location.href = node.path; });
-
- if (CURRENT_PATH.startsWith(node.path) && childUl) {
- childUl.classList.add('open');
- toggle.textContent = '▼';
- }
-
- container.appendChild(li);
- });
-}
-
-buildTree(TREE_DATA, document.getElementById('tree-root'), 0);
-</script>
-</body>
-</html>