living-ai-documentation 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.fr.md +344 -0
- package/README.md +344 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +262 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/frontend/accuracy-gauge.js +70 -0
- package/dist/src/frontend/admin.html +1532 -0
- package/dist/src/frontend/annotations.js +585 -0
- package/dist/src/frontend/boot.js +101 -0
- package/dist/src/frontend/config.js +29 -0
- package/dist/src/frontend/confirm-modal.js +82 -0
- package/dist/src/frontend/context.html +1252 -0
- package/dist/src/frontend/dark-mode.js +20 -0
- package/dist/src/frontend/diagram/alignment.js +161 -0
- package/dist/src/frontend/diagram/clipboard.js +187 -0
- package/dist/src/frontend/diagram/constants.js +109 -0
- package/dist/src/frontend/diagram/custom-shapes.js +104 -0
- package/dist/src/frontend/diagram/debug.js +43 -0
- package/dist/src/frontend/diagram/drawio-export.js +649 -0
- package/dist/src/frontend/diagram/edge-panel.js +293 -0
- package/dist/src/frontend/diagram/edge-rendering.js +12 -0
- package/dist/src/frontend/diagram/evidence.js +146 -0
- package/dist/src/frontend/diagram/grid.js +78 -0
- package/dist/src/frontend/diagram/groups.js +102 -0
- package/dist/src/frontend/diagram/history.js +157 -0
- package/dist/src/frontend/diagram/image-name-modal.js +48 -0
- package/dist/src/frontend/diagram/image-upload.js +36 -0
- package/dist/src/frontend/diagram/label-editor.js +115 -0
- package/dist/src/frontend/diagram/link-panel.js +144 -0
- package/dist/src/frontend/diagram/main.js +364 -0
- package/dist/src/frontend/diagram/network.js +2214 -0
- package/dist/src/frontend/diagram/node-panel.js +389 -0
- package/dist/src/frontend/diagram/node-rendering.js +964 -0
- package/dist/src/frontend/diagram/persistence.js +168 -0
- package/dist/src/frontend/diagram/ports.js +421 -0
- package/dist/src/frontend/diagram/selection-overlay.js +387 -0
- package/dist/src/frontend/diagram/state.js +43 -0
- package/dist/src/frontend/diagram/t.js +3 -0
- package/dist/src/frontend/diagram/toast.js +21 -0
- package/dist/src/frontend/diagram/unlock-hold.js +206 -0
- package/dist/src/frontend/diagram/zoom.js +20 -0
- package/dist/src/frontend/diagram-link-modal.js +137 -0
- package/dist/src/frontend/diagram.html +1494 -0
- package/dist/src/frontend/documents.js +479 -0
- package/dist/src/frontend/export.js +338 -0
- package/dist/src/frontend/file-attach.js +178 -0
- package/dist/src/frontend/files-modal.js +243 -0
- package/dist/src/frontend/i18n/en.json +624 -0
- package/dist/src/frontend/i18n/fr.json +624 -0
- package/dist/src/frontend/i18n.js +32 -0
- package/dist/src/frontend/image-paste.js +126 -0
- package/dist/src/frontend/index.html +2806 -0
- package/dist/src/frontend/local-search.js +476 -0
- package/dist/src/frontend/metadata.js +318 -0
- package/dist/src/frontend/misc.js +92 -0
- package/dist/src/frontend/new-doc-modal.js +285 -0
- package/dist/src/frontend/new-folder-modal.js +169 -0
- package/dist/src/frontend/search.js +194 -0
- package/dist/src/frontend/shape-editor.html +685 -0
- package/dist/src/frontend/sidebar-helpers.js +96 -0
- package/dist/src/frontend/sidebar-resize.js +98 -0
- package/dist/src/frontend/sidebar.js +351 -0
- package/dist/src/frontend/snippet-detect.js +25 -0
- package/dist/src/frontend/snippet-table.js +85 -0
- package/dist/src/frontend/snippet-tree.js +94 -0
- package/dist/src/frontend/snippets.js +1146 -0
- package/dist/src/frontend/state.js +46 -0
- package/dist/src/frontend/utils.js +21 -0
- package/dist/src/frontend/validate.js +107 -0
- package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
- package/dist/src/frontend/wordcloud.js +693 -0
- package/dist/src/lib/config.d.ts +26 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +195 -0
- package/dist/src/lib/config.js.map +1 -0
- package/dist/src/lib/hash.d.ts +2 -0
- package/dist/src/lib/hash.d.ts.map +1 -0
- package/dist/src/lib/hash.js +18 -0
- package/dist/src/lib/hash.js.map +1 -0
- package/dist/src/lib/metadata.d.ts +31 -0
- package/dist/src/lib/metadata.d.ts.map +1 -0
- package/dist/src/lib/metadata.js +128 -0
- package/dist/src/lib/metadata.js.map +1 -0
- package/dist/src/lib/parser.d.ts +11 -0
- package/dist/src/lib/parser.d.ts.map +1 -0
- package/dist/src/lib/parser.js +111 -0
- package/dist/src/lib/parser.js.map +1 -0
- package/dist/src/lib/status.d.ts +9 -0
- package/dist/src/lib/status.d.ts.map +1 -0
- package/dist/src/lib/status.js +72 -0
- package/dist/src/lib/status.js.map +1 -0
- package/dist/src/mcp/server.d.ts +3 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +2046 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/diagrams.d.ts +82 -0
- package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
- package/dist/src/mcp/tools/diagrams.js +594 -0
- package/dist/src/mcp/tools/diagrams.js.map +1 -0
- package/dist/src/mcp/tools/documents.d.ts +44 -0
- package/dist/src/mcp/tools/documents.d.ts.map +1 -0
- package/dist/src/mcp/tools/documents.js +186 -0
- package/dist/src/mcp/tools/documents.js.map +1 -0
- package/dist/src/mcp/tools/git.d.ts +10 -0
- package/dist/src/mcp/tools/git.d.ts.map +1 -0
- package/dist/src/mcp/tools/git.js +217 -0
- package/dist/src/mcp/tools/git.js.map +1 -0
- package/dist/src/mcp/tools/metadata.d.ts +57 -0
- package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
- package/dist/src/mcp/tools/metadata.js +222 -0
- package/dist/src/mcp/tools/metadata.js.map +1 -0
- package/dist/src/mcp/tools/source.d.ts +29 -0
- package/dist/src/mcp/tools/source.d.ts.map +1 -0
- package/dist/src/mcp/tools/source.js +196 -0
- package/dist/src/mcp/tools/source.js.map +1 -0
- package/dist/src/routes/annotations.d.ts +3 -0
- package/dist/src/routes/annotations.d.ts.map +1 -0
- package/dist/src/routes/annotations.js +83 -0
- package/dist/src/routes/annotations.js.map +1 -0
- package/dist/src/routes/browse-source.d.ts +3 -0
- package/dist/src/routes/browse-source.d.ts.map +1 -0
- package/dist/src/routes/browse-source.js +79 -0
- package/dist/src/routes/browse-source.js.map +1 -0
- package/dist/src/routes/browse.d.ts +3 -0
- package/dist/src/routes/browse.d.ts.map +1 -0
- package/dist/src/routes/browse.js +91 -0
- package/dist/src/routes/browse.js.map +1 -0
- package/dist/src/routes/config.d.ts +3 -0
- package/dist/src/routes/config.d.ts.map +1 -0
- package/dist/src/routes/config.js +145 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/context.d.ts +3 -0
- package/dist/src/routes/context.d.ts.map +1 -0
- package/dist/src/routes/context.js +287 -0
- package/dist/src/routes/context.js.map +1 -0
- package/dist/src/routes/diagrams.d.ts +3 -0
- package/dist/src/routes/diagrams.d.ts.map +1 -0
- package/dist/src/routes/diagrams.js +69 -0
- package/dist/src/routes/diagrams.js.map +1 -0
- package/dist/src/routes/documents.d.ts +11 -0
- package/dist/src/routes/documents.d.ts.map +1 -0
- package/dist/src/routes/documents.js +450 -0
- package/dist/src/routes/documents.js.map +1 -0
- package/dist/src/routes/export.d.ts +3 -0
- package/dist/src/routes/export.d.ts.map +1 -0
- package/dist/src/routes/export.js +280 -0
- package/dist/src/routes/export.js.map +1 -0
- package/dist/src/routes/files.d.ts +3 -0
- package/dist/src/routes/files.d.ts.map +1 -0
- package/dist/src/routes/files.js +180 -0
- package/dist/src/routes/files.js.map +1 -0
- package/dist/src/routes/images.d.ts +3 -0
- package/dist/src/routes/images.d.ts.map +1 -0
- package/dist/src/routes/images.js +49 -0
- package/dist/src/routes/images.js.map +1 -0
- package/dist/src/routes/metadata.d.ts +3 -0
- package/dist/src/routes/metadata.d.ts.map +1 -0
- package/dist/src/routes/metadata.js +131 -0
- package/dist/src/routes/metadata.js.map +1 -0
- package/dist/src/routes/shape-libraries.d.ts +3 -0
- package/dist/src/routes/shape-libraries.d.ts.map +1 -0
- package/dist/src/routes/shape-libraries.js +118 -0
- package/dist/src/routes/shape-libraries.js.map +1 -0
- package/dist/src/routes/wordcloud.d.ts +3 -0
- package/dist/src/routes/wordcloud.d.ts.map +1 -0
- package/dist/src/routes/wordcloud.js +95 -0
- package/dist/src/routes/wordcloud.js.map +1 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +93 -0
- package/dist/src/server.js.map +1 -0
- package/dist/starter-doc/.living-doc.json +52 -0
- package/dist/starter-doc/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
- package/dist/starter-doc/AI/2026_01_01_how_to.md +112 -0
- package/dist/starter-doc/AI/PROJECT-INSTRUCTIONS.md +172 -0
- package/dist/starter-doc/AI/PROJECT-STACK.md +77 -0
- package/dist/starter-doc/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
- package/dist/starter-doc/AI/default/AGENTS.md +31 -0
- package/dist/starter-doc/AI/default/CLAUDE.md +31 -0
- package/dist/starter-doc/AI/default/MEMORY.md +24 -0
- package/dist/starter-doc/AI/rules/no-magic-numbers.md +18 -0
- package/dist/starter-doc/AI/rules/track-current-work.md +23 -0
- package/dist/starter-doc/WORKLOG/current-task.md +57 -0
- package/dist/starter-doc-fr/.living-doc.json +52 -0
- package/dist/starter-doc-fr/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
- package/dist/starter-doc-fr/AI/2026_01_01_how_to.md +100 -0
- package/dist/starter-doc-fr/AI/PROJECT-INSTRUCTIONS.md +172 -0
- package/dist/starter-doc-fr/AI/PROJECT-STACK.md +77 -0
- package/dist/starter-doc-fr/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
- package/dist/starter-doc-fr/AI/default/AGENTS.md +31 -0
- package/dist/starter-doc-fr/AI/default/CLAUDE.md +31 -0
- package/dist/starter-doc-fr/AI/default/MEMORY.md +24 -0
- package/dist/starter-doc-fr/AI/rules/no-magic-numbers.md +18 -0
- package/dist/starter-doc-fr/AI/rules/track-current-work.md +23 -0
- package/dist/starter-doc-fr/WORKLOG/current-task.md +57 -0
- package/images/living_documentation.jpg +0 -0
- package/images/readme-extra-files.png +0 -0
- package/images/readme-filename-pattern.png +0 -0
- package/images/readme-intelligent-search-demo.jpg +0 -0
- package/images/readme-sidebar.png +0 -0
- package/package.json +72 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// ── Sidebar helpers — tree traversal + annotation badges ─────────────────────
|
|
2
|
+
// Loaded as a classic script; all symbols are global.
|
|
3
|
+
// Depends on globals defined elsewhere: `annotationCounts` (index.html),
|
|
4
|
+
// `esc` (utils.js), `window.t` (i18n.js).
|
|
5
|
+
|
|
6
|
+
function countTreeDocs(node) {
|
|
7
|
+
let n = Object.values(node.categories).reduce(
|
|
8
|
+
(s, arr) => s + arr.length,
|
|
9
|
+
0,
|
|
10
|
+
);
|
|
11
|
+
for (const child of Object.values(node.children))
|
|
12
|
+
n += countTreeDocs(child);
|
|
13
|
+
return n;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function countTreeAnnotations(node) {
|
|
17
|
+
let n = 0;
|
|
18
|
+
for (const arr of Object.values(node.categories)) {
|
|
19
|
+
for (const doc of arr) n += annotationCounts[doc.id] || 0;
|
|
20
|
+
}
|
|
21
|
+
for (const child of Object.values(node.children))
|
|
22
|
+
n += countTreeAnnotations(child);
|
|
23
|
+
return n;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function countTreeAnnotatedDocs(node) {
|
|
27
|
+
let n = 0;
|
|
28
|
+
for (const arr of Object.values(node.categories)) {
|
|
29
|
+
for (const doc of arr) if (annotationCounts[doc.id] > 0) n += 1;
|
|
30
|
+
}
|
|
31
|
+
for (const child of Object.values(node.children))
|
|
32
|
+
n += countTreeAnnotatedDocs(child);
|
|
33
|
+
return n;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function annotationBadge(count) {
|
|
37
|
+
if (!count) return "";
|
|
38
|
+
if (typeof stabiloHidden !== "undefined" && stabiloHidden) return "";
|
|
39
|
+
const label = window.t
|
|
40
|
+
? window.t("sidebar.annotation_badge")
|
|
41
|
+
: "annotation";
|
|
42
|
+
return `<span title="${count} ${esc(label)}${count > 1 ? "s" : ""}"
|
|
43
|
+
class="inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5
|
|
44
|
+
rounded-full bg-yellow-300 dark:bg-yellow-400
|
|
45
|
+
text-[10px] font-bold text-yellow-900
|
|
46
|
+
border border-yellow-500 shadow-sm">${count}</span>`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function annotatedDocsBadge(count) {
|
|
50
|
+
if (!count) return "";
|
|
51
|
+
if (typeof stabiloHidden !== "undefined" && stabiloHidden) return "";
|
|
52
|
+
const label = window.t
|
|
53
|
+
? window.t("sidebar.annotated_docs_badge")
|
|
54
|
+
: "document with annotations";
|
|
55
|
+
return `<span title="${count} ${esc(label)}${count > 1 ? "s" : ""}"
|
|
56
|
+
class="inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5
|
|
57
|
+
rounded-full bg-orange-400 dark:bg-orange-500
|
|
58
|
+
text-[10px] font-bold text-white
|
|
59
|
+
border border-orange-600 shadow-sm">${count}</span>`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function countTreeFileAttachedDocs(node) {
|
|
63
|
+
let n = 0;
|
|
64
|
+
for (const arr of Object.values(node.categories)) {
|
|
65
|
+
for (const doc of arr) if (fileAttachmentCounts[doc.id] > 0) n += 1;
|
|
66
|
+
}
|
|
67
|
+
for (const child of Object.values(node.children))
|
|
68
|
+
n += countTreeFileAttachedDocs(child);
|
|
69
|
+
return n;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function fileAttachmentBadge(count) {
|
|
73
|
+
if (!count) return "";
|
|
74
|
+
if (typeof hideAttachments !== "undefined" && hideAttachments) return "";
|
|
75
|
+
const label = window.t
|
|
76
|
+
? window.t("sidebar.file_attachment_badge")
|
|
77
|
+
: "attachment";
|
|
78
|
+
return `<span title="${count} ${esc(label)}${count > 1 ? "s" : ""}"
|
|
79
|
+
class="inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5
|
|
80
|
+
rounded-full bg-sky-200 dark:bg-sky-300
|
|
81
|
+
text-[10px] font-bold text-sky-900
|
|
82
|
+
border border-sky-500 shadow-sm">${count}</span>`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function fileAttachedDocsBadge(count) {
|
|
86
|
+
if (!count) return "";
|
|
87
|
+
if (typeof hideAttachments !== "undefined" && hideAttachments) return "";
|
|
88
|
+
const label = window.t
|
|
89
|
+
? window.t("sidebar.file_attached_docs_badge")
|
|
90
|
+
: "document with attachments";
|
|
91
|
+
return `<span title="${count} ${esc(label)}${count > 1 ? "s" : ""}"
|
|
92
|
+
class="inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1.5
|
|
93
|
+
rounded-full bg-slate-400 dark:bg-slate-500
|
|
94
|
+
text-[10px] font-bold text-white
|
|
95
|
+
border border-slate-600 shadow-sm">${count}</span>`;
|
|
96
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// ── Sidebar horizontal resize ───────────────────────────────────────────────
|
|
2
|
+
// Drag a handle on the sidebar's right edge to resize it.
|
|
3
|
+
// Width is persisted in localStorage (`ld-sidebar-w`) across reloads.
|
|
4
|
+
// The toggle (toggleSidebar) keeps working because it only flips the `hidden`
|
|
5
|
+
// class — width stays intact on the element.
|
|
6
|
+
|
|
7
|
+
(function () {
|
|
8
|
+
const STORAGE_KEY = "ld-sidebar-w";
|
|
9
|
+
const MIN_W = 200;
|
|
10
|
+
const MAX_W = 600;
|
|
11
|
+
|
|
12
|
+
function applyStoredWidth() {
|
|
13
|
+
const sidebar = document.getElementById("sidebar");
|
|
14
|
+
if (!sidebar) return;
|
|
15
|
+
let w = parseInt(localStorage.getItem(STORAGE_KEY) || "", 10);
|
|
16
|
+
if (!Number.isFinite(w)) return;
|
|
17
|
+
w = Math.max(MIN_W, Math.min(MAX_W, w));
|
|
18
|
+
sidebar.classList.remove("w-72");
|
|
19
|
+
sidebar.style.width = w + "px";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function initSidebarResize() {
|
|
23
|
+
const sidebar = document.getElementById("sidebar");
|
|
24
|
+
if (!sidebar) return;
|
|
25
|
+
|
|
26
|
+
applyStoredWidth();
|
|
27
|
+
|
|
28
|
+
const handle = document.createElement("div");
|
|
29
|
+
handle.id = "sidebar-resize-handle";
|
|
30
|
+
handle.setAttribute("aria-hidden", "true");
|
|
31
|
+
handle.style.cssText = [
|
|
32
|
+
"position:absolute",
|
|
33
|
+
"top:0",
|
|
34
|
+
"right:-2px",
|
|
35
|
+
"width:6px",
|
|
36
|
+
"height:100%",
|
|
37
|
+
"cursor:col-resize",
|
|
38
|
+
"z-index:20",
|
|
39
|
+
"user-select:none",
|
|
40
|
+
"background:transparent",
|
|
41
|
+
"transition:background 0.15s ease",
|
|
42
|
+
].join(";");
|
|
43
|
+
handle.addEventListener("mouseenter", () => {
|
|
44
|
+
handle.style.background = "rgba(59,130,246,0.35)";
|
|
45
|
+
});
|
|
46
|
+
handle.addEventListener("mouseleave", () => {
|
|
47
|
+
if (!handle.dataset.dragging) handle.style.background = "transparent";
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Sidebar needs `relative` so the absolutely-positioned handle anchors to it.
|
|
51
|
+
sidebar.style.position = "relative";
|
|
52
|
+
sidebar.appendChild(handle);
|
|
53
|
+
|
|
54
|
+
let startX = 0;
|
|
55
|
+
let startW = 0;
|
|
56
|
+
|
|
57
|
+
function onMove(e) {
|
|
58
|
+
const dx = e.clientX - startX;
|
|
59
|
+
let w = startW + dx;
|
|
60
|
+
w = Math.max(MIN_W, Math.min(MAX_W, w));
|
|
61
|
+
sidebar.style.width = w + "px";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function onUp() {
|
|
65
|
+
document.removeEventListener("mousemove", onMove);
|
|
66
|
+
document.removeEventListener("mouseup", onUp);
|
|
67
|
+
document.body.style.cursor = "";
|
|
68
|
+
document.body.style.userSelect = "";
|
|
69
|
+
delete handle.dataset.dragging;
|
|
70
|
+
handle.style.background = "transparent";
|
|
71
|
+
const finalW = parseInt(sidebar.style.width, 10);
|
|
72
|
+
if (Number.isFinite(finalW)) {
|
|
73
|
+
try {
|
|
74
|
+
localStorage.setItem(STORAGE_KEY, String(finalW));
|
|
75
|
+
} catch {
|
|
76
|
+
/* ignore */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
handle.addEventListener("mousedown", (e) => {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
startX = e.clientX;
|
|
84
|
+
startW = sidebar.getBoundingClientRect().width;
|
|
85
|
+
// Drop the Tailwind width class so inline width takes over for good.
|
|
86
|
+
sidebar.classList.remove("w-72");
|
|
87
|
+
sidebar.style.width = startW + "px";
|
|
88
|
+
handle.dataset.dragging = "1";
|
|
89
|
+
handle.style.background = "rgba(59,130,246,0.55)";
|
|
90
|
+
document.body.style.cursor = "col-resize";
|
|
91
|
+
document.body.style.userSelect = "none";
|
|
92
|
+
document.addEventListener("mousemove", onMove);
|
|
93
|
+
document.addEventListener("mouseup", onUp);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
window.initSidebarResize = initSidebarResize;
|
|
98
|
+
})();
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// ── Sidebar rendering ────────────────────────────────────────────────────────
|
|
2
|
+
// Depends on globals from state.js, utils.js, sidebar-helpers.js, and
|
|
3
|
+
// `currentDocId` (updated by documents.js when a doc opens).
|
|
4
|
+
|
|
5
|
+
function renderSidebar(docs) {
|
|
6
|
+
const tree = document.getElementById("category-tree");
|
|
7
|
+
const countEl = document.getElementById("doc-count");
|
|
8
|
+
|
|
9
|
+
if (!docs.length && !allFolderPaths.length) {
|
|
10
|
+
tree.innerHTML =
|
|
11
|
+
`<p class="px-4 py-8 text-sm text-gray-400 text-center">${window.t('sidebar.no_docs')}</p>`;
|
|
12
|
+
countEl.textContent = "";
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
countEl.textContent = `${docs.length} document${docs.length !== 1 ? "s" : ""}`;
|
|
17
|
+
|
|
18
|
+
// Auto-expand only root General initially
|
|
19
|
+
if (expandedCategories.size === 0) expandedCategories.add("General");
|
|
20
|
+
|
|
21
|
+
tree.innerHTML = renderTreeNode(buildFolderTree(docs), []);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Build a recursive tree: each node holds { categories: {cat: docs[]}, children: {label: node} }
|
|
25
|
+
function buildFolderTree(docs) {
|
|
26
|
+
const root = { categories: {}, children: {} };
|
|
27
|
+
for (const doc of docs) {
|
|
28
|
+
let node = root;
|
|
29
|
+
for (const seg of doc.folder || []) {
|
|
30
|
+
if (!node.children[seg])
|
|
31
|
+
node.children[seg] = { categories: {}, children: {} };
|
|
32
|
+
node = node.children[seg];
|
|
33
|
+
}
|
|
34
|
+
if (!node.categories[doc.category])
|
|
35
|
+
node.categories[doc.category] = [];
|
|
36
|
+
node.categories[doc.category].push(doc);
|
|
37
|
+
}
|
|
38
|
+
// Ensure empty folders appear in the tree (only when no active filter)
|
|
39
|
+
if (searchQuery) return root;
|
|
40
|
+
for (const folderPath of allFolderPaths) {
|
|
41
|
+
const segments = folderPath.split("/").filter(Boolean);
|
|
42
|
+
let node = root;
|
|
43
|
+
for (const seg of segments) {
|
|
44
|
+
if (!node.children[seg])
|
|
45
|
+
node.children[seg] = { categories: {}, children: {} };
|
|
46
|
+
node = node.children[seg];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return root;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Render a tree node at a given folder path (array of segment strings)
|
|
53
|
+
function renderTreeNode(node, folderPath) {
|
|
54
|
+
let html = "";
|
|
55
|
+
|
|
56
|
+
// Flat mode: categories are hidden — merge all docs in this node and sort by filename
|
|
57
|
+
if (hideCategories) {
|
|
58
|
+
const flatDocs = Object.values(node.categories)
|
|
59
|
+
.flat()
|
|
60
|
+
.sort((a, b) => (a.filename || "").localeCompare(b.filename || ""));
|
|
61
|
+
html += flatDocs.map((doc) => renderDocItem(doc)).join("");
|
|
62
|
+
|
|
63
|
+
// Subfolders (recursive) — folders are still shown
|
|
64
|
+
const childKeys = Object.keys(node.children).sort((a, b) =>
|
|
65
|
+
a.localeCompare(b),
|
|
66
|
+
);
|
|
67
|
+
for (const key of childKeys) {
|
|
68
|
+
const childPath = [...folderPath, key];
|
|
69
|
+
const pathKey = childPath.join("|");
|
|
70
|
+
const nodeId =
|
|
71
|
+
"folder-" + childPath.map((s) => s.replace(/\W/g, "-")).join("-");
|
|
72
|
+
const isExpanded = expandedFolders.has(pathKey);
|
|
73
|
+
const docCount = countTreeDocs(node.children[key]);
|
|
74
|
+
const folderAnnotatedDocs = countTreeAnnotatedDocs(node.children[key]);
|
|
75
|
+
const folderFileDocs = countTreeFileAttachedDocs(node.children[key]);
|
|
76
|
+
html += `
|
|
77
|
+
<div class="mb-1">
|
|
78
|
+
<button onclick="toggleFolder('${esc(pathKey)}')"
|
|
79
|
+
class="w-full flex items-center justify-between px-3 py-1.5 text-xs font-semibold
|
|
80
|
+
text-violet-600 dark:text-violet-400 uppercase tracking-wider
|
|
81
|
+
hover:bg-gray-50 dark:hover:bg-gray-800/60 rounded-md transition-colors">
|
|
82
|
+
<span class="flex items-center gap-2 min-w-0">
|
|
83
|
+
<span title="${esc(key)}" class="truncate">📁 ${esc(folderLabel(key))}</span>
|
|
84
|
+
${annotatedDocsBadge(folderAnnotatedDocs)}
|
|
85
|
+
${fileAttachedDocsBadge(folderFileDocs)}
|
|
86
|
+
</span>
|
|
87
|
+
<span class="flex items-center gap-1.5">
|
|
88
|
+
<span class="font-normal normal-case text-gray-400">${docCount}</span>
|
|
89
|
+
<span class="transition-transform duration-200 ${isExpanded ? "rotate-90" : ""}" id="arrow-${nodeId}">▸</span>
|
|
90
|
+
</span>
|
|
91
|
+
</button>
|
|
92
|
+
<div id="${nodeId}" class="category-docs pl-3 ${isExpanded ? "expanded" : "collapsed"}">
|
|
93
|
+
${renderTreeNode(node.children[key], childPath)}
|
|
94
|
+
</div>
|
|
95
|
+
</div>`;
|
|
96
|
+
}
|
|
97
|
+
return html;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Helper to render one category group
|
|
101
|
+
const renderCat = (cat) => {
|
|
102
|
+
const catPathKey = [...folderPath, cat].join("|");
|
|
103
|
+
const catNodeId =
|
|
104
|
+
"cat-" +
|
|
105
|
+
[...folderPath, cat].map((s) => s.replace(/\W/g, "-")).join("-");
|
|
106
|
+
const isExpanded = expandedCategories.has(catPathKey);
|
|
107
|
+
const catAnnotatedDocs = node.categories[cat].reduce(
|
|
108
|
+
(s, d) => s + (annotationCounts[d.id] > 0 ? 1 : 0),
|
|
109
|
+
0,
|
|
110
|
+
);
|
|
111
|
+
const catFileDocs = node.categories[cat].reduce(
|
|
112
|
+
(s, d) => s + (fileAttachmentCounts[d.id] > 0 ? 1 : 0),
|
|
113
|
+
0,
|
|
114
|
+
);
|
|
115
|
+
return `
|
|
116
|
+
<div class="mb-0.5">
|
|
117
|
+
<button onclick="toggleCategory('${esc(catPathKey)}')"
|
|
118
|
+
class="w-full flex items-center justify-between px-3 py-1.5 text-xs font-semibold
|
|
119
|
+
text-gray-500 dark:text-gray-400 uppercase tracking-wider
|
|
120
|
+
hover:bg-gray-50 dark:hover:bg-gray-800/60 rounded-md transition-colors">
|
|
121
|
+
<span class="flex items-center gap-2">
|
|
122
|
+
<span>${esc(cat)}</span>
|
|
123
|
+
${annotatedDocsBadge(catAnnotatedDocs)}
|
|
124
|
+
${fileAttachedDocsBadge(catFileDocs)}
|
|
125
|
+
</span>
|
|
126
|
+
<span class="flex items-center gap-1.5">
|
|
127
|
+
<span class="font-normal normal-case text-gray-400">${node.categories[cat].length}</span>
|
|
128
|
+
<span class="transition-transform duration-200 ${isExpanded ? "rotate-90" : ""}" id="arrow-${catNodeId}">▸</span>
|
|
129
|
+
</span>
|
|
130
|
+
</button>
|
|
131
|
+
<div id="${catNodeId}" class="category-docs pl-2 ${isExpanded ? "expanded" : "collapsed"}">
|
|
132
|
+
${node.categories[cat].map((doc) => renderDocItem(doc)).join("")}
|
|
133
|
+
</div>
|
|
134
|
+
</div>`;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// General always first
|
|
138
|
+
if (node.categories["General"]) html += renderCat("General");
|
|
139
|
+
|
|
140
|
+
// Subfolders (sorted alphabetically — numeric prefix like "1_" sorts naturally)
|
|
141
|
+
const childKeys = Object.keys(node.children).sort((a, b) =>
|
|
142
|
+
a.localeCompare(b),
|
|
143
|
+
);
|
|
144
|
+
for (const key of childKeys) {
|
|
145
|
+
const childPath = [...folderPath, key];
|
|
146
|
+
const pathKey = childPath.join("|");
|
|
147
|
+
const nodeId =
|
|
148
|
+
"folder-" + childPath.map((s) => s.replace(/\W/g, "-")).join("-");
|
|
149
|
+
const isExpanded = expandedFolders.has(pathKey);
|
|
150
|
+
const docCount = countTreeDocs(node.children[key]);
|
|
151
|
+
const folderAnnotatedDocs = countTreeAnnotatedDocs(node.children[key]);
|
|
152
|
+
const folderFileDocs = countTreeFileAttachedDocs(node.children[key]);
|
|
153
|
+
html += `
|
|
154
|
+
<div class="mb-1">
|
|
155
|
+
<button onclick="toggleFolder('${esc(pathKey)}')"
|
|
156
|
+
class="w-full flex items-center justify-between px-3 py-1.5 text-xs font-semibold
|
|
157
|
+
text-violet-600 dark:text-violet-400 uppercase tracking-wider
|
|
158
|
+
hover:bg-gray-50 dark:hover:bg-gray-800/60 rounded-md transition-colors">
|
|
159
|
+
<span class="flex items-center gap-2 min-w-0">
|
|
160
|
+
<span title="${esc(key)}" class="truncate">📁 ${esc(folderLabel(key))}</span>
|
|
161
|
+
${annotatedDocsBadge(folderAnnotatedDocs)}
|
|
162
|
+
${fileAttachedDocsBadge(folderFileDocs)}
|
|
163
|
+
</span>
|
|
164
|
+
<span class="flex items-center gap-1.5">
|
|
165
|
+
<span class="font-normal normal-case text-gray-400">${docCount}</span>
|
|
166
|
+
<span class="transition-transform duration-200 ${isExpanded ? "rotate-90" : ""}" id="arrow-${nodeId}">▸</span>
|
|
167
|
+
</span>
|
|
168
|
+
</button>
|
|
169
|
+
<div id="${nodeId}" class="category-docs pl-3 ${isExpanded ? "expanded" : "collapsed"}">
|
|
170
|
+
${renderTreeNode(node.children[key], childPath)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Non-General category groups (sorted alphabetically)
|
|
176
|
+
const otherCats = Object.keys(node.categories)
|
|
177
|
+
.filter((c) => c !== "General")
|
|
178
|
+
.sort((a, b) => a.localeCompare(b));
|
|
179
|
+
for (const cat of otherCats) html += renderCat(cat);
|
|
180
|
+
|
|
181
|
+
return html;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function renderDocItem(doc) {
|
|
185
|
+
const isActive = doc.id === currentDocId;
|
|
186
|
+
const annCount = annotationCounts[doc.id] || 0;
|
|
187
|
+
const fileCount = fileAttachmentCounts[doc.id] || 0;
|
|
188
|
+
return `
|
|
189
|
+
<button onclick="openDocument('${esc(doc.id)}')"
|
|
190
|
+
id="item-${esc(doc.id)}"
|
|
191
|
+
class="doc-item w-full text-left px-3 py-1.5 rounded-md text-sm transition-colors
|
|
192
|
+
text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800/60
|
|
193
|
+
${isActive ? "active" : ""}">
|
|
194
|
+
<div class="leading-snug flex items-center justify-between gap-2">
|
|
195
|
+
<span class="truncate">${esc(doc.title)}</span>
|
|
196
|
+
<span class="flex items-center gap-1 shrink-0">
|
|
197
|
+
${annotationBadge(annCount)}
|
|
198
|
+
${fileAttachmentBadge(fileCount)}
|
|
199
|
+
</span>
|
|
200
|
+
</div>
|
|
201
|
+
${doc.formattedDate ? `<div class="text-xs text-gray-400 dark:text-gray-500 mt-0.5">${esc(doc.formattedDate)}</div>` : ""}
|
|
202
|
+
</button>`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function setInstantCollapse(el) {
|
|
206
|
+
el.classList.add("no-transition");
|
|
207
|
+
el.classList.add("collapsed");
|
|
208
|
+
el.classList.remove("expanded");
|
|
209
|
+
// Force reflow so the collapsed state is committed while transitions are off
|
|
210
|
+
void el.offsetHeight;
|
|
211
|
+
requestAnimationFrame(() => el.classList.remove("no-transition"));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function collapseCategoryByKey(key, instant = false) {
|
|
215
|
+
const parts = key.split("|");
|
|
216
|
+
const catNodeId =
|
|
217
|
+
"cat-" + parts.map((p) => p.replace(/\W/g, "-")).join("-");
|
|
218
|
+
const el = document.getElementById(catNodeId);
|
|
219
|
+
const arrow = document.getElementById("arrow-" + catNodeId);
|
|
220
|
+
if (el) {
|
|
221
|
+
if (instant) setInstantCollapse(el);
|
|
222
|
+
else {
|
|
223
|
+
el.classList.add("collapsed");
|
|
224
|
+
el.classList.remove("expanded");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (arrow) arrow.style.transform = "";
|
|
228
|
+
expandedCategories.delete(key);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function collapseFolderAndDescendants(pathKey, instant = false) {
|
|
232
|
+
const parts = pathKey.split("|");
|
|
233
|
+
const nodeId =
|
|
234
|
+
"folder-" + parts.map((p) => p.replace(/\W/g, "-")).join("-");
|
|
235
|
+
const el = document.getElementById(nodeId);
|
|
236
|
+
const arrow = document.getElementById("arrow-" + nodeId);
|
|
237
|
+
if (el) {
|
|
238
|
+
if (instant) setInstantCollapse(el);
|
|
239
|
+
else {
|
|
240
|
+
el.classList.add("collapsed");
|
|
241
|
+
el.classList.remove("expanded");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (arrow) arrow.style.transform = "";
|
|
245
|
+
expandedFolders.delete(pathKey);
|
|
246
|
+
|
|
247
|
+
const prefix = pathKey + "|";
|
|
248
|
+
for (const key of [...expandedFolders]) {
|
|
249
|
+
if (key.startsWith(prefix)) collapseFolderAndDescendants(key, instant);
|
|
250
|
+
}
|
|
251
|
+
for (const key of [...expandedCategories]) {
|
|
252
|
+
if (key.startsWith(prefix)) collapseCategoryByKey(key, instant);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function toggleCategory(key) {
|
|
257
|
+
const parts = key.split("|");
|
|
258
|
+
const catNodeId =
|
|
259
|
+
"cat-" + parts.map((p) => p.replace(/\W/g, "-")).join("-");
|
|
260
|
+
const el = document.getElementById(catNodeId);
|
|
261
|
+
const arrow = document.getElementById("arrow-" + catNodeId);
|
|
262
|
+
if (!el) return;
|
|
263
|
+
const expanding = el.classList.contains("collapsed");
|
|
264
|
+
|
|
265
|
+
if (expanding && exclusiveCategoryExpansion) {
|
|
266
|
+
const parentPath = parts.slice(0, -1).join("|");
|
|
267
|
+
for (const otherKey of [...expandedCategories]) {
|
|
268
|
+
if (otherKey === key) continue;
|
|
269
|
+
const otherParts = otherKey.split("|");
|
|
270
|
+
const otherParent = otherParts.slice(0, -1).join("|");
|
|
271
|
+
if (otherParent === parentPath) collapseCategoryByKey(otherKey, true);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
el.classList.toggle("collapsed", !expanding);
|
|
276
|
+
el.classList.toggle("expanded", expanding);
|
|
277
|
+
if (arrow) arrow.style.transform = expanding ? "rotate(90deg)" : "";
|
|
278
|
+
if (expanding) expandedCategories.add(key);
|
|
279
|
+
else expandedCategories.delete(key);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function toggleFolder(pathKey) {
|
|
283
|
+
const parts = pathKey.split("|");
|
|
284
|
+
const nodeId =
|
|
285
|
+
"folder-" + parts.map((p) => p.replace(/\W/g, "-")).join("-");
|
|
286
|
+
const el = document.getElementById(nodeId);
|
|
287
|
+
const arrow = document.getElementById("arrow-" + nodeId);
|
|
288
|
+
if (!el) return;
|
|
289
|
+
const expanding = el.classList.contains("collapsed");
|
|
290
|
+
|
|
291
|
+
if (expanding && exclusiveFolderExpansion) {
|
|
292
|
+
const parentPath = parts.slice(0, -1).join("|");
|
|
293
|
+
for (const otherKey of [...expandedFolders]) {
|
|
294
|
+
if (otherKey === pathKey) continue;
|
|
295
|
+
const otherParts = otherKey.split("|");
|
|
296
|
+
const otherParent = otherParts.slice(0, -1).join("|");
|
|
297
|
+
if (otherParent === parentPath)
|
|
298
|
+
collapseFolderAndDescendants(otherKey, true);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
el.classList.toggle("collapsed", !expanding);
|
|
303
|
+
el.classList.toggle("expanded", expanding);
|
|
304
|
+
if (arrow) arrow.style.transform = expanding ? "rotate(90deg)" : "";
|
|
305
|
+
if (expanding) expandedFolders.add(pathKey);
|
|
306
|
+
else expandedFolders.delete(pathKey);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function toggleSidebar() {
|
|
310
|
+
document.getElementById("sidebar").classList.toggle("hidden");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function toggleHideCategories() {
|
|
314
|
+
hideCategories = !hideCategories;
|
|
315
|
+
try {
|
|
316
|
+
localStorage.setItem("ld-hide-categories", hideCategories ? "1" : "0");
|
|
317
|
+
} catch {
|
|
318
|
+
/* ignore */
|
|
319
|
+
}
|
|
320
|
+
applyHideCategoriesButtonState();
|
|
321
|
+
refreshSidebar();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function applyHideCategoriesButtonState() {
|
|
325
|
+
const btn = document.getElementById("toggle-categories-btn");
|
|
326
|
+
if (!btn) return;
|
|
327
|
+
btn.classList.toggle("text-blue-500", !hideCategories);
|
|
328
|
+
btn.classList.toggle("dark:text-blue-400", !hideCategories);
|
|
329
|
+
btn.classList.toggle("text-gray-400", hideCategories);
|
|
330
|
+
btn.classList.toggle("dark:text-gray-500", hideCategories);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function toggleHideAttachments() {
|
|
334
|
+
hideAttachments = !hideAttachments;
|
|
335
|
+
try {
|
|
336
|
+
localStorage.setItem("ld-hide-attachments", hideAttachments ? "1" : "0");
|
|
337
|
+
} catch {
|
|
338
|
+
/* ignore */
|
|
339
|
+
}
|
|
340
|
+
applyHideAttachmentsButtonState();
|
|
341
|
+
refreshSidebar();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function applyHideAttachmentsButtonState() {
|
|
345
|
+
const btn = document.getElementById("toggle-attachments-btn");
|
|
346
|
+
if (!btn) return;
|
|
347
|
+
btn.classList.toggle("text-blue-500", !hideAttachments);
|
|
348
|
+
btn.classList.toggle("dark:text-blue-400", !hideAttachments);
|
|
349
|
+
btn.classList.toggle("text-gray-400", hideAttachments);
|
|
350
|
+
btn.classList.toggle("dark:text-gray-500", hideAttachments);
|
|
351
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// ── Pure snippet format detection ────────────────────────────────────────────
|
|
2
|
+
// Given a raw piece of Markdown/HTML text, returns the canonical snippet type
|
|
3
|
+
// used by the snippets modal, or null if no format matches.
|
|
4
|
+
// Loaded as a classic script; exposes `detectSnippetType` as a global.
|
|
5
|
+
|
|
6
|
+
function detectSnippetType(text) {
|
|
7
|
+
const t = text.trim();
|
|
8
|
+
// anchor-doc-link before doc-link and anchor-link
|
|
9
|
+
if (/^\[.*?\]\(\?doc=.+#.+\)$/.test(t)) return "anchor-doc-link";
|
|
10
|
+
if (/^\[.*?\]\(\?doc=.+\)$/.test(t)) return "doc-link";
|
|
11
|
+
if (/^\[.*?\]\(#.+\)$/.test(t)) return "anchor-link";
|
|
12
|
+
if (/^!\[.*?\]\(.*?\)$/.test(t)) return "image";
|
|
13
|
+
if (/^\[.*?\]\(.*?\)$/.test(t)) return "link";
|
|
14
|
+
if (/^<details[\s>]/i.test(t)) return "collapsible";
|
|
15
|
+
if (/^<span\s[^>]*color:[^>]*>[\s\S]*<\/span>$/.test(t)) return "colored-text";
|
|
16
|
+
if (/^<div\s[^>]*border-left[^>]*>/.test(t)) return "colored-section";
|
|
17
|
+
if (/^```text\n/.test(t) && /[├└│]/.test(t)) return "tree";
|
|
18
|
+
if (/^```/.test(t)) return "code-block";
|
|
19
|
+
if (/^(\| *.*? *\|)+\n(\| *-+.*\|)+/.test(t)) return "table";
|
|
20
|
+
if (/^> /.test(t)) return "blockquote";
|
|
21
|
+
if (/^(---|\n---\n)$/.test(t)) return "separator";
|
|
22
|
+
if (/^1\. /.test(t)) return "ordered-list";
|
|
23
|
+
if (/^- /.test(t)) return "unordered-list";
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// ── Snippets — Table builder ─────────────────────────────────────────────────
|
|
2
|
+
// Loaded as a classic script; exposes the table-builder helpers and `_tableData`
|
|
3
|
+
// state as globals. Depends on `esc` (utils.js) and `snippetUpdatePreview`
|
|
4
|
+
// (defined inline in index.html).
|
|
5
|
+
|
|
6
|
+
let _tableData = [];
|
|
7
|
+
|
|
8
|
+
function tableInit() {
|
|
9
|
+
_tableData = [
|
|
10
|
+
["En-tête 1", "En-tête 2", "En-tête 3"],
|
|
11
|
+
["", "", ""],
|
|
12
|
+
["", "", ""],
|
|
13
|
+
];
|
|
14
|
+
tableRenderGrid();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function tableRenderGrid() {
|
|
18
|
+
const rows = _tableData.length;
|
|
19
|
+
const cols = _tableData[0]?.length ?? 0;
|
|
20
|
+
document.getElementById("snip-table-rows").textContent = rows;
|
|
21
|
+
document.getElementById("snip-table-cols").textContent = cols;
|
|
22
|
+
|
|
23
|
+
const inputBase =
|
|
24
|
+
"px-2 py-1 text-xs rounded border focus:outline-none focus:ring-1 focus:ring-blue-500 w-full min-w-[80px]";
|
|
25
|
+
const hdrCls = `${inputBase} font-semibold border-blue-300 dark:border-blue-600 bg-blue-50 dark:bg-blue-900/20 text-gray-900 dark:text-gray-100`;
|
|
26
|
+
const cellCls = `${inputBase} border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100`;
|
|
27
|
+
|
|
28
|
+
let html = '<table class="border-collapse w-full"><tbody>';
|
|
29
|
+
for (let r = 0; r < rows; r++) {
|
|
30
|
+
html += "<tr>";
|
|
31
|
+
for (let c = 0; c < cols; c++) {
|
|
32
|
+
const cls = r === 0 ? hdrCls : cellCls;
|
|
33
|
+
html += `<td class="p-0.5"><input type="text" value="${esc(_tableData[r][c])}" oninput="tableUpdate(${r},${c},this.value)" class="${cls}" /></td>`;
|
|
34
|
+
}
|
|
35
|
+
html += "</tr>";
|
|
36
|
+
}
|
|
37
|
+
html += "</tbody></table>";
|
|
38
|
+
document.getElementById("snip-table-grid").innerHTML = html;
|
|
39
|
+
snippetUpdatePreview();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function tableUpdate(r, c, val) {
|
|
43
|
+
_tableData[r][c] = val;
|
|
44
|
+
snippetUpdatePreview();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function tableChangeRows(delta) {
|
|
48
|
+
const cols = _tableData[0]?.length ?? 3;
|
|
49
|
+
if (delta > 0) {
|
|
50
|
+
_tableData.push(Array(cols).fill(""));
|
|
51
|
+
} else if (_tableData.length > 2) {
|
|
52
|
+
_tableData.pop();
|
|
53
|
+
}
|
|
54
|
+
tableRenderGrid();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function tableChangeCols(delta) {
|
|
58
|
+
if (delta > 0) {
|
|
59
|
+
_tableData.forEach((row) => row.push(""));
|
|
60
|
+
} else if ((_tableData[0]?.length ?? 0) > 1) {
|
|
61
|
+
_tableData.forEach((row) => row.pop());
|
|
62
|
+
}
|
|
63
|
+
tableRenderGrid();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildTableMarkdown() {
|
|
67
|
+
if (!_tableData.length || !_tableData[0]?.length) return "";
|
|
68
|
+
const cols = _tableData[0].length;
|
|
69
|
+
const widths = Array(cols).fill(3);
|
|
70
|
+
_tableData.forEach((row) => {
|
|
71
|
+
row.forEach((cell, c) => {
|
|
72
|
+
widths[c] = Math.max(widths[c], cell.length);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
const fmtRow = (row) =>
|
|
76
|
+
"| " +
|
|
77
|
+
row.map((cell, c) => cell.padEnd(widths[c])).join(" | ") +
|
|
78
|
+
" |";
|
|
79
|
+
const sep = "| " + widths.map((w) => "-".repeat(w)).join(" | ") + " |";
|
|
80
|
+
return [
|
|
81
|
+
fmtRow(_tableData[0]),
|
|
82
|
+
sep,
|
|
83
|
+
..._tableData.slice(1).map(fmtRow),
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|