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,1532 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" class="h-full">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Admin — Living Documentation</title>
|
|
7
|
+
|
|
8
|
+
<script src="/i18n.js"></script>
|
|
9
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
10
|
+
|
|
11
|
+
<script>
|
|
12
|
+
tailwind.config = { darkMode: "class", theme: { extend: {} } };
|
|
13
|
+
</script>
|
|
14
|
+
</head>
|
|
15
|
+
|
|
16
|
+
<body
|
|
17
|
+
class="h-full bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100"
|
|
18
|
+
>
|
|
19
|
+
<!-- ── Header ── -->
|
|
20
|
+
<header
|
|
21
|
+
class="sticky top-0 z-40 flex items-center justify-between px-4 h-14 border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm"
|
|
22
|
+
>
|
|
23
|
+
<div class="flex items-center gap-3">
|
|
24
|
+
<span
|
|
25
|
+
class="text-blue-600 dark:text-blue-400 text-xl select-none"
|
|
26
|
+
aria-hidden="true"
|
|
27
|
+
>📚</span
|
|
28
|
+
>
|
|
29
|
+
<h1
|
|
30
|
+
class="text-base font-semibold tracking-tight"
|
|
31
|
+
data-i18n="admin.title"
|
|
32
|
+
>
|
|
33
|
+
Admin Panel
|
|
34
|
+
</h1>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="flex items-center gap-3">
|
|
38
|
+
<button
|
|
39
|
+
id="dark-toggle"
|
|
40
|
+
data-i18n-title="admin.toggle_dark"
|
|
41
|
+
class="p-2 rounded-lg text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
42
|
+
>
|
|
43
|
+
<span id="dark-icon" class="text-lg leading-none">☾</span>
|
|
44
|
+
</button>
|
|
45
|
+
<span class="text-gray-300 dark:text-gray-700">|</span>
|
|
46
|
+
<a
|
|
47
|
+
href="/"
|
|
48
|
+
class="text-blue-600 dark:text-blue-400 hover:underline text-sm"
|
|
49
|
+
>
|
|
50
|
+
<span data-i18n="admin.back_to_docs">Back to docs →</span>
|
|
51
|
+
</a>
|
|
52
|
+
</div>
|
|
53
|
+
</header>
|
|
54
|
+
|
|
55
|
+
<!-- ── Page ── -->
|
|
56
|
+
<main class="pb-10">
|
|
57
|
+
<form id="config-form" novalidate>
|
|
58
|
+
<!-- Sticky sub-header: Configuration title + Save button -->
|
|
59
|
+
<div
|
|
60
|
+
class="sticky top-14 z-30 border-b border-gray-200 dark:border-gray-800 bg-gray-50/90 dark:bg-gray-950/90 backdrop-blur"
|
|
61
|
+
>
|
|
62
|
+
<div
|
|
63
|
+
class="max-w-3xl mx-auto px-6 py-4 flex items-center justify-between gap-4"
|
|
64
|
+
>
|
|
65
|
+
<div class="min-w-0">
|
|
66
|
+
<h2
|
|
67
|
+
class="text-lg font-bold text-gray-900 dark:text-gray-50 truncate"
|
|
68
|
+
data-i18n="admin.config.title"
|
|
69
|
+
>
|
|
70
|
+
Configuration
|
|
71
|
+
</h2>
|
|
72
|
+
<p
|
|
73
|
+
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400 truncate"
|
|
74
|
+
data-i18n="admin.config.description"
|
|
75
|
+
>
|
|
76
|
+
Settings are saved to .living-doc.json in your docs folder.
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="flex items-center gap-4 shrink-0">
|
|
80
|
+
<div id="save-msg" class="text-sm"></div>
|
|
81
|
+
<button
|
|
82
|
+
type="submit"
|
|
83
|
+
class="px-5 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-950"
|
|
84
|
+
>
|
|
85
|
+
<span data-i18n="admin.config.save_btn">Save changes</span>
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div class="max-w-3xl mx-auto px-6 pt-8 space-y-8">
|
|
92
|
+
<!-- ── 🌐 General ── -->
|
|
93
|
+
<section
|
|
94
|
+
class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm overflow-hidden"
|
|
95
|
+
>
|
|
96
|
+
<header
|
|
97
|
+
class="px-6 py-4 border-b border-gray-100 dark:border-gray-800"
|
|
98
|
+
>
|
|
99
|
+
<h3
|
|
100
|
+
class="text-base font-semibold text-gray-800 dark:text-gray-100 flex items-center gap-2"
|
|
101
|
+
>
|
|
102
|
+
<span class="text-xl" aria-hidden="true">🌐</span>
|
|
103
|
+
<span data-i18n="admin.section.general.title">General</span>
|
|
104
|
+
</h3>
|
|
105
|
+
<p
|
|
106
|
+
class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-8"
|
|
107
|
+
data-i18n="admin.section.general.desc"
|
|
108
|
+
>
|
|
109
|
+
Site identity, theme, and interface language.
|
|
110
|
+
</p>
|
|
111
|
+
</header>
|
|
112
|
+
<div class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
113
|
+
<div class="px-6 py-4 space-y-1.5">
|
|
114
|
+
<label
|
|
115
|
+
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
116
|
+
for="field-title"
|
|
117
|
+
data-i18n="admin.appearance.site_title_label"
|
|
118
|
+
>Site Title</label
|
|
119
|
+
>
|
|
120
|
+
<input
|
|
121
|
+
id="field-title"
|
|
122
|
+
name="title"
|
|
123
|
+
type="text"
|
|
124
|
+
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
125
|
+
placeholder="Living Documentation"
|
|
126
|
+
/>
|
|
127
|
+
<p
|
|
128
|
+
class="text-xs text-gray-400 dark:text-gray-500"
|
|
129
|
+
data-i18n="admin.appearance.site_title_hint"
|
|
130
|
+
>
|
|
131
|
+
Displayed in the browser tab and sidebar header.
|
|
132
|
+
</p>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="px-6 py-4 space-y-1.5">
|
|
135
|
+
<label
|
|
136
|
+
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
137
|
+
for="field-theme"
|
|
138
|
+
data-i18n="admin.appearance.theme_label"
|
|
139
|
+
>Default Theme</label
|
|
140
|
+
>
|
|
141
|
+
<select
|
|
142
|
+
id="field-theme"
|
|
143
|
+
name="theme"
|
|
144
|
+
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
145
|
+
>
|
|
146
|
+
<option
|
|
147
|
+
value="system"
|
|
148
|
+
data-i18n="admin.appearance.theme_system"
|
|
149
|
+
>
|
|
150
|
+
System (follow OS preference)
|
|
151
|
+
</option>
|
|
152
|
+
<option
|
|
153
|
+
value="light"
|
|
154
|
+
data-i18n="admin.appearance.theme_light"
|
|
155
|
+
>
|
|
156
|
+
Light
|
|
157
|
+
</option>
|
|
158
|
+
<option value="dark" data-i18n="admin.appearance.theme_dark">
|
|
159
|
+
Dark
|
|
160
|
+
</option>
|
|
161
|
+
</select>
|
|
162
|
+
<p
|
|
163
|
+
class="text-xs text-gray-400 dark:text-gray-500"
|
|
164
|
+
data-i18n="admin.appearance.theme_hint"
|
|
165
|
+
>
|
|
166
|
+
Users can always override with the toggle button.
|
|
167
|
+
</p>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="px-6 py-4 space-y-1.5">
|
|
170
|
+
<label
|
|
171
|
+
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
172
|
+
for="field-language"
|
|
173
|
+
data-i18n="admin.appearance.language_label"
|
|
174
|
+
>Language</label
|
|
175
|
+
>
|
|
176
|
+
<select
|
|
177
|
+
id="field-language"
|
|
178
|
+
name="language"
|
|
179
|
+
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
180
|
+
>
|
|
181
|
+
<option value="en" data-i18n="admin.appearance.language_en">
|
|
182
|
+
English
|
|
183
|
+
</option>
|
|
184
|
+
<option value="fr" data-i18n="admin.appearance.language_fr">
|
|
185
|
+
Français
|
|
186
|
+
</option>
|
|
187
|
+
</select>
|
|
188
|
+
<p
|
|
189
|
+
class="text-xs text-gray-400 dark:text-gray-500"
|
|
190
|
+
data-i18n="admin.appearance.language_hint"
|
|
191
|
+
>
|
|
192
|
+
Interface language for all pages.
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</section>
|
|
197
|
+
|
|
198
|
+
<!-- ── 📝 Filename convention ── -->
|
|
199
|
+
<section
|
|
200
|
+
class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm overflow-hidden"
|
|
201
|
+
>
|
|
202
|
+
<header
|
|
203
|
+
class="px-6 py-4 border-b border-gray-100 dark:border-gray-800"
|
|
204
|
+
>
|
|
205
|
+
<h3
|
|
206
|
+
class="text-base font-semibold text-gray-800 dark:text-gray-100 flex items-center gap-2"
|
|
207
|
+
>
|
|
208
|
+
<span class="text-xl" aria-hidden="true">📝</span>
|
|
209
|
+
<span data-i18n="admin.section.filename.title"
|
|
210
|
+
>Filename convention</span
|
|
211
|
+
>
|
|
212
|
+
</h3>
|
|
213
|
+
<p
|
|
214
|
+
class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-8"
|
|
215
|
+
data-i18n="admin.section.filename.desc"
|
|
216
|
+
>
|
|
217
|
+
How new documents are named, and how the viewer parses them.
|
|
218
|
+
</p>
|
|
219
|
+
</header>
|
|
220
|
+
<div class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
221
|
+
<div class="px-6 py-4 grid grid-cols-[1fr_auto] gap-x-6 gap-y-2">
|
|
222
|
+
<div>
|
|
223
|
+
<dt
|
|
224
|
+
class="text-xs text-gray-400 uppercase tracking-wide mb-0.5"
|
|
225
|
+
>
|
|
226
|
+
<span data-i18n="admin.server_info.docs_folder"
|
|
227
|
+
>Docs Folder</span
|
|
228
|
+
>
|
|
229
|
+
</dt>
|
|
230
|
+
<dd
|
|
231
|
+
id="info-folder"
|
|
232
|
+
class="font-mono text-xs text-gray-700 dark:text-gray-300 break-all"
|
|
233
|
+
>
|
|
234
|
+
—
|
|
235
|
+
</dd>
|
|
236
|
+
</div>
|
|
237
|
+
<div>
|
|
238
|
+
<dt
|
|
239
|
+
class="text-xs text-gray-400 uppercase tracking-wide mb-0.5"
|
|
240
|
+
>
|
|
241
|
+
<span data-i18n="admin.server_info.port">Port</span>
|
|
242
|
+
</dt>
|
|
243
|
+
<dd
|
|
244
|
+
id="info-port"
|
|
245
|
+
class="font-mono text-xs text-gray-700 dark:text-gray-300"
|
|
246
|
+
>
|
|
247
|
+
—
|
|
248
|
+
</dd>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="px-6 py-4 space-y-1.5">
|
|
252
|
+
<label
|
|
253
|
+
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
254
|
+
for="field-pattern"
|
|
255
|
+
data-i18n="admin.pattern.label"
|
|
256
|
+
>Filename Pattern</label
|
|
257
|
+
>
|
|
258
|
+
<input
|
|
259
|
+
id="field-pattern"
|
|
260
|
+
name="filenamePattern"
|
|
261
|
+
type="text"
|
|
262
|
+
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
263
|
+
placeholder="YYYY_MM_DD_HH_mm_[Category]_title"
|
|
264
|
+
data-i18n-placeholder="admin.pattern.placeholder"
|
|
265
|
+
/>
|
|
266
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">
|
|
267
|
+
<span data-i18n="admin.pattern.hint"
|
|
268
|
+
>Parsed for date, category, and title.</span
|
|
269
|
+
>
|
|
270
|
+
<span
|
|
271
|
+
id="pattern-desc-example"
|
|
272
|
+
class="font-mono bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 px-1 rounded"
|
|
273
|
+
>YYYY_MM_DD_HH_mm_[Category]_title.md</span
|
|
274
|
+
>
|
|
275
|
+
</p>
|
|
276
|
+
</div>
|
|
277
|
+
<div id="pattern-preview" class="px-6 py-4">
|
|
278
|
+
<h4
|
|
279
|
+
class="text-xs font-semibold text-blue-700 dark:text-blue-400 mb-2 uppercase tracking-wide"
|
|
280
|
+
>
|
|
281
|
+
<span data-i18n="admin.pattern.preview_title"
|
|
282
|
+
>Pattern Preview</span
|
|
283
|
+
>
|
|
284
|
+
</h4>
|
|
285
|
+
<p
|
|
286
|
+
class="text-xs text-gray-500 dark:text-gray-400 mb-3"
|
|
287
|
+
data-i18n="admin.pattern.preview_desc"
|
|
288
|
+
>
|
|
289
|
+
How your pattern parses example filenames:
|
|
290
|
+
</p>
|
|
291
|
+
<div id="preview-rows" class="space-y-2 text-sm"></div>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
</section>
|
|
295
|
+
|
|
296
|
+
<!-- ── 📂 Source & extra files ── -->
|
|
297
|
+
<section
|
|
298
|
+
class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm overflow-hidden"
|
|
299
|
+
>
|
|
300
|
+
<header
|
|
301
|
+
class="px-6 py-4 border-b border-gray-100 dark:border-gray-800"
|
|
302
|
+
>
|
|
303
|
+
<h3
|
|
304
|
+
class="text-base font-semibold text-gray-800 dark:text-gray-100 flex items-center gap-2"
|
|
305
|
+
>
|
|
306
|
+
<span class="text-xl" aria-hidden="true">📂</span>
|
|
307
|
+
<span data-i18n="admin.section.source.title"
|
|
308
|
+
>Source & extra files</span
|
|
309
|
+
>
|
|
310
|
+
</h3>
|
|
311
|
+
<p
|
|
312
|
+
class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-8"
|
|
313
|
+
data-i18n="admin.section.source.desc"
|
|
314
|
+
>
|
|
315
|
+
Code source root (for MCP tools) and Markdown files pulled from
|
|
316
|
+
outside the docs folder.
|
|
317
|
+
</p>
|
|
318
|
+
</header>
|
|
319
|
+
<div class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
320
|
+
<div class="px-6 py-4 space-y-1.5">
|
|
321
|
+
<label
|
|
322
|
+
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
323
|
+
for="field-source-root"
|
|
324
|
+
data-i18n="admin.server_info.source_root"
|
|
325
|
+
>Source Root</label
|
|
326
|
+
>
|
|
327
|
+
<input
|
|
328
|
+
id="field-source-root"
|
|
329
|
+
name="sourceRoot"
|
|
330
|
+
type="text"
|
|
331
|
+
class="w-full px-3 py-2 font-mono text-xs rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 break-all"
|
|
332
|
+
placeholder=".."
|
|
333
|
+
/>
|
|
334
|
+
<p
|
|
335
|
+
class="text-xs text-gray-400 dark:text-gray-500"
|
|
336
|
+
data-i18n="admin.server_info.source_root_hint"
|
|
337
|
+
>
|
|
338
|
+
Absolute path used by MCP source tools. Defaults to the parent
|
|
339
|
+
of Docs Folder when empty.
|
|
340
|
+
</p>
|
|
341
|
+
</div>
|
|
342
|
+
<div class="px-6 py-4 space-y-4">
|
|
343
|
+
<div>
|
|
344
|
+
<p
|
|
345
|
+
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
346
|
+
data-i18n="admin.extra_files.title"
|
|
347
|
+
>
|
|
348
|
+
General — Extra Files
|
|
349
|
+
</p>
|
|
350
|
+
<p
|
|
351
|
+
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400"
|
|
352
|
+
data-i18n="admin.extra_files.description"
|
|
353
|
+
>
|
|
354
|
+
Add Markdown files from outside the docs folder to the
|
|
355
|
+
General section.
|
|
356
|
+
</p>
|
|
357
|
+
</div>
|
|
358
|
+
<div
|
|
359
|
+
class="rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"
|
|
360
|
+
>
|
|
361
|
+
<div
|
|
362
|
+
class="flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"
|
|
363
|
+
>
|
|
364
|
+
<button
|
|
365
|
+
id="browse-up"
|
|
366
|
+
onclick="browseUp()"
|
|
367
|
+
class="text-xs text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-30 disabled:pointer-events-none shrink-0"
|
|
368
|
+
>
|
|
369
|
+
<span data-i18n="common.up">↑ Up</span>
|
|
370
|
+
</button>
|
|
371
|
+
<span
|
|
372
|
+
id="browse-path"
|
|
373
|
+
class="font-mono text-xs text-gray-400 dark:text-gray-500 truncate flex-1 text-right"
|
|
374
|
+
></span>
|
|
375
|
+
</div>
|
|
376
|
+
<div
|
|
377
|
+
id="browse-list"
|
|
378
|
+
class="divide-y divide-gray-100 dark:divide-gray-800 max-h-52 overflow-y-auto"
|
|
379
|
+
></div>
|
|
380
|
+
</div>
|
|
381
|
+
<div>
|
|
382
|
+
<p
|
|
383
|
+
class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2"
|
|
384
|
+
data-i18n="admin.section.added_files_label"
|
|
385
|
+
>
|
|
386
|
+
Added files
|
|
387
|
+
</p>
|
|
388
|
+
<div id="extra-files-list" class="space-y-1"></div>
|
|
389
|
+
<p
|
|
390
|
+
id="extra-files-empty"
|
|
391
|
+
class="text-xs text-gray-400 italic"
|
|
392
|
+
>
|
|
393
|
+
<span data-i18n="admin.extra_files.empty"
|
|
394
|
+
>No extra files added yet.</span
|
|
395
|
+
>
|
|
396
|
+
</p>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</section>
|
|
401
|
+
|
|
402
|
+
<!-- ── 🌳 Sidebar behaviour ── -->
|
|
403
|
+
<section
|
|
404
|
+
class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm overflow-hidden"
|
|
405
|
+
>
|
|
406
|
+
<header
|
|
407
|
+
class="px-6 py-4 border-b border-gray-100 dark:border-gray-800"
|
|
408
|
+
>
|
|
409
|
+
<h3
|
|
410
|
+
class="text-base font-semibold text-gray-800 dark:text-gray-100 flex items-center gap-2"
|
|
411
|
+
>
|
|
412
|
+
<span class="text-xl" aria-hidden="true">🗂️</span>
|
|
413
|
+
<span data-i18n="admin.section.sidebar.title"
|
|
414
|
+
>Sidebar behaviour</span
|
|
415
|
+
>
|
|
416
|
+
</h3>
|
|
417
|
+
<p
|
|
418
|
+
class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-8"
|
|
419
|
+
data-i18n="admin.section.sidebar.desc"
|
|
420
|
+
>
|
|
421
|
+
How folders and categories expand in the left drawer.
|
|
422
|
+
</p>
|
|
423
|
+
</header>
|
|
424
|
+
<div class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
425
|
+
<div class="px-6 py-4">
|
|
426
|
+
<label class="flex items-center gap-3 cursor-pointer">
|
|
427
|
+
<input
|
|
428
|
+
id="field-exclusive-folder"
|
|
429
|
+
name="exclusiveFolderExpansion"
|
|
430
|
+
type="checkbox"
|
|
431
|
+
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
|
432
|
+
/>
|
|
433
|
+
<span
|
|
434
|
+
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
435
|
+
data-i18n="admin.appearance.exclusive_folder_label"
|
|
436
|
+
>Exclusive folder expansion in sidebar</span
|
|
437
|
+
>
|
|
438
|
+
</label>
|
|
439
|
+
<p
|
|
440
|
+
class="mt-1 text-xs text-gray-400 dark:text-gray-500 ml-7"
|
|
441
|
+
data-i18n="admin.appearance.exclusive_folder_hint"
|
|
442
|
+
>
|
|
443
|
+
When enabled, opening a folder in the sidebar automatically
|
|
444
|
+
closes any other open folder at the same level (and its
|
|
445
|
+
children).
|
|
446
|
+
</p>
|
|
447
|
+
</div>
|
|
448
|
+
<div class="px-6 py-4">
|
|
449
|
+
<label class="flex items-center gap-3 cursor-pointer">
|
|
450
|
+
<input
|
|
451
|
+
id="field-exclusive-category"
|
|
452
|
+
name="exclusiveCategoryExpansion"
|
|
453
|
+
type="checkbox"
|
|
454
|
+
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
|
455
|
+
/>
|
|
456
|
+
<span
|
|
457
|
+
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
458
|
+
data-i18n="admin.appearance.exclusive_category_label"
|
|
459
|
+
>Exclusive category expansion in sidebar</span
|
|
460
|
+
>
|
|
461
|
+
</label>
|
|
462
|
+
<p
|
|
463
|
+
class="mt-1 text-xs text-gray-400 dark:text-gray-500 ml-7"
|
|
464
|
+
data-i18n="admin.appearance.exclusive_category_hint"
|
|
465
|
+
>
|
|
466
|
+
When enabled, opening a category automatically closes any
|
|
467
|
+
other open category under the same parent folder.
|
|
468
|
+
</p>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
</section>
|
|
472
|
+
|
|
473
|
+
<!-- ── ✍️ Markdown rendering ── -->
|
|
474
|
+
<section
|
|
475
|
+
class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm overflow-hidden"
|
|
476
|
+
>
|
|
477
|
+
<header
|
|
478
|
+
class="px-6 py-4 border-b border-gray-100 dark:border-gray-800"
|
|
479
|
+
>
|
|
480
|
+
<h3
|
|
481
|
+
class="text-base font-semibold text-gray-800 dark:text-gray-100 flex items-center gap-2"
|
|
482
|
+
>
|
|
483
|
+
<span class="text-xl" aria-hidden="true">✍️</span>
|
|
484
|
+
<span data-i18n="admin.section.markdown.title"
|
|
485
|
+
>Markdown rendering</span
|
|
486
|
+
>
|
|
487
|
+
</h3>
|
|
488
|
+
<p
|
|
489
|
+
class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-8"
|
|
490
|
+
data-i18n="admin.section.markdown.desc"
|
|
491
|
+
>
|
|
492
|
+
How the viewer renders newlines and long code blocks.
|
|
493
|
+
</p>
|
|
494
|
+
</header>
|
|
495
|
+
<div class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
496
|
+
<div class="px-6 py-4 space-y-1.5">
|
|
497
|
+
<label
|
|
498
|
+
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
499
|
+
for="field-code-max-height"
|
|
500
|
+
data-i18n="admin.appearance.code_max_height_label"
|
|
501
|
+
>Code block max height (px)</label
|
|
502
|
+
>
|
|
503
|
+
<input
|
|
504
|
+
id="field-code-max-height"
|
|
505
|
+
name="codeBlockMaxHeight"
|
|
506
|
+
type="number"
|
|
507
|
+
min="0"
|
|
508
|
+
max="5000"
|
|
509
|
+
step="10"
|
|
510
|
+
class="w-full px-3 py-2 text-sm rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
511
|
+
placeholder="400"
|
|
512
|
+
/>
|
|
513
|
+
<p
|
|
514
|
+
class="text-xs text-gray-400 dark:text-gray-500"
|
|
515
|
+
data-i18n="admin.appearance.code_max_height_hint"
|
|
516
|
+
>
|
|
517
|
+
Longer code blocks get a collapse/expand button. Set to 0 to
|
|
518
|
+
disable.
|
|
519
|
+
</p>
|
|
520
|
+
</div>
|
|
521
|
+
<div class="px-6 py-4">
|
|
522
|
+
<label class="flex items-center gap-3 cursor-pointer">
|
|
523
|
+
<input
|
|
524
|
+
id="field-soft-breaks"
|
|
525
|
+
name="markdownSoftBreaks"
|
|
526
|
+
type="checkbox"
|
|
527
|
+
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
|
528
|
+
/>
|
|
529
|
+
<span
|
|
530
|
+
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
531
|
+
data-i18n="admin.appearance.soft_breaks_label"
|
|
532
|
+
>Treat every newline as a line break</span
|
|
533
|
+
>
|
|
534
|
+
</label>
|
|
535
|
+
<p
|
|
536
|
+
class="mt-1 text-xs text-gray-400 dark:text-gray-500 ml-7"
|
|
537
|
+
data-i18n-html="admin.appearance.soft_breaks_hint"
|
|
538
|
+
>
|
|
539
|
+
<strong>Unchecked (CommonMark, default):</strong> a single
|
|
540
|
+
newline in the source is rendered as a space — two adjacent
|
|
541
|
+
lines merge into one paragraph. To force a line break without
|
|
542
|
+
leaving a blank line, end the previous line with
|
|
543
|
+
<code>two spaces</code> then newline, or use
|
|
544
|
+
<code><br/></code>.<br />
|
|
545
|
+
<strong>Checked (GitHub-flavoured):</strong> every single
|
|
546
|
+
newline becomes a real line break in the rendered output.
|
|
547
|
+
Convenient for chat-style writing, but you can no longer
|
|
548
|
+
hard-wrap long paragraphs in the source file without visible
|
|
549
|
+
breaks.
|
|
550
|
+
</p>
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
</section>
|
|
554
|
+
|
|
555
|
+
<!-- ── 📎 File attachments ── -->
|
|
556
|
+
<section
|
|
557
|
+
class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm overflow-hidden"
|
|
558
|
+
>
|
|
559
|
+
<header
|
|
560
|
+
class="px-6 py-4 border-b border-gray-100 dark:border-gray-800"
|
|
561
|
+
>
|
|
562
|
+
<h3
|
|
563
|
+
class="text-base font-semibold text-gray-800 dark:text-gray-100 flex items-center gap-2"
|
|
564
|
+
>
|
|
565
|
+
<span class="text-xl" aria-hidden="true">📎</span>
|
|
566
|
+
<span data-i18n="admin.section.files.title"
|
|
567
|
+
>File attachments</span
|
|
568
|
+
>
|
|
569
|
+
</h3>
|
|
570
|
+
<p
|
|
571
|
+
class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-8"
|
|
572
|
+
data-i18n-html="admin.files.description"
|
|
573
|
+
>
|
|
574
|
+
Extensions that are rejected by the
|
|
575
|
+
<code>/api/files/upload</code> endpoint. One extension per line
|
|
576
|
+
(or comma/space separated), without the leading dot.
|
|
577
|
+
</p>
|
|
578
|
+
</header>
|
|
579
|
+
<div class="px-6 py-4 space-y-1.5">
|
|
580
|
+
<label
|
|
581
|
+
class="block text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
582
|
+
for="field-blocked-extensions"
|
|
583
|
+
data-i18n="admin.files.blocked_label"
|
|
584
|
+
>Blocked extensions</label
|
|
585
|
+
>
|
|
586
|
+
<textarea
|
|
587
|
+
id="field-blocked-extensions"
|
|
588
|
+
name="blockedFileExtensions"
|
|
589
|
+
rows="3"
|
|
590
|
+
class="w-full px-3 py-2 font-mono text-xs rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
591
|
+
placeholder="exe sh bat cmd com scr ps1 msi"
|
|
592
|
+
></textarea>
|
|
593
|
+
<p
|
|
594
|
+
class="text-xs text-gray-400 dark:text-gray-500"
|
|
595
|
+
data-i18n="admin.files.hint"
|
|
596
|
+
>
|
|
597
|
+
Defaults to common executable formats. Max upload size is 19 MB
|
|
598
|
+
per file.
|
|
599
|
+
</p>
|
|
600
|
+
</div>
|
|
601
|
+
</section>
|
|
602
|
+
|
|
603
|
+
<!-- ── 🎨 Diagram palettes ── -->
|
|
604
|
+
<section
|
|
605
|
+
class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm overflow-hidden"
|
|
606
|
+
>
|
|
607
|
+
<header
|
|
608
|
+
class="px-6 py-4 border-b border-gray-100 dark:border-gray-800"
|
|
609
|
+
>
|
|
610
|
+
<h3
|
|
611
|
+
class="text-base font-semibold text-gray-800 dark:text-gray-100 flex items-center gap-2"
|
|
612
|
+
>
|
|
613
|
+
<span class="text-xl" aria-hidden="true">🎨</span>
|
|
614
|
+
<span data-i18n="admin.section.diagram.title"
|
|
615
|
+
>Diagram palettes</span
|
|
616
|
+
>
|
|
617
|
+
</h3>
|
|
618
|
+
<p
|
|
619
|
+
class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-8"
|
|
620
|
+
data-i18n="admin.section.diagram.desc"
|
|
621
|
+
>
|
|
622
|
+
Customize the colors available in the diagram editor.
|
|
623
|
+
</p>
|
|
624
|
+
</header>
|
|
625
|
+
<div class="divide-y divide-gray-100 dark:divide-gray-800">
|
|
626
|
+
<div class="px-6 py-4 space-y-3">
|
|
627
|
+
<div class="flex items-center justify-between">
|
|
628
|
+
<h4
|
|
629
|
+
class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide"
|
|
630
|
+
data-i18n="admin.palette.shapes_title"
|
|
631
|
+
>
|
|
632
|
+
Shape colors
|
|
633
|
+
</h4>
|
|
634
|
+
<button
|
|
635
|
+
type="button"
|
|
636
|
+
onclick="resetNodePalette()"
|
|
637
|
+
class="text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
|
638
|
+
data-i18n="common.reset"
|
|
639
|
+
>
|
|
640
|
+
Reset
|
|
641
|
+
</button>
|
|
642
|
+
</div>
|
|
643
|
+
<p
|
|
644
|
+
class="text-xs text-gray-400 dark:text-gray-500"
|
|
645
|
+
data-i18n="admin.palette.shapes_hint"
|
|
646
|
+
>
|
|
647
|
+
Click a color to show or hide it in the node panel. Dimmed =
|
|
648
|
+
hidden.
|
|
649
|
+
</p>
|
|
650
|
+
<div id="nodeSwatches" class="flex flex-wrap gap-2"></div>
|
|
651
|
+
</div>
|
|
652
|
+
<div class="px-6 py-4 space-y-3">
|
|
653
|
+
<div class="flex items-center justify-between">
|
|
654
|
+
<h4
|
|
655
|
+
class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide"
|
|
656
|
+
data-i18n="admin.palette.arrows_title"
|
|
657
|
+
>
|
|
658
|
+
Arrow colors
|
|
659
|
+
</h4>
|
|
660
|
+
<button
|
|
661
|
+
type="button"
|
|
662
|
+
onclick="resetEdgePalette()"
|
|
663
|
+
class="text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
|
664
|
+
data-i18n="common.reset"
|
|
665
|
+
>
|
|
666
|
+
Reset
|
|
667
|
+
</button>
|
|
668
|
+
</div>
|
|
669
|
+
<p
|
|
670
|
+
class="text-xs text-gray-400 dark:text-gray-500"
|
|
671
|
+
data-i18n="admin.palette.arrows_hint"
|
|
672
|
+
>
|
|
673
|
+
Add or remove colors available for arrows. Click ✕ to remove.
|
|
674
|
+
</p>
|
|
675
|
+
<div
|
|
676
|
+
id="edgeSwatches"
|
|
677
|
+
class="flex flex-wrap gap-3 items-end"
|
|
678
|
+
></div>
|
|
679
|
+
<div class="flex items-center gap-2 pt-1">
|
|
680
|
+
<input
|
|
681
|
+
type="color"
|
|
682
|
+
id="newEdgeColor"
|
|
683
|
+
value="#3b82f6"
|
|
684
|
+
class="w-8 h-8 rounded cursor-pointer border border-gray-300 dark:border-gray-600 bg-transparent"
|
|
685
|
+
/>
|
|
686
|
+
<button
|
|
687
|
+
type="button"
|
|
688
|
+
onclick="addEdgeColor()"
|
|
689
|
+
class="px-3 py-1.5 text-xs rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-medium transition-colors"
|
|
690
|
+
data-i18n="admin.palette.add_color_btn"
|
|
691
|
+
>
|
|
692
|
+
Add color
|
|
693
|
+
</button>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
</div>
|
|
697
|
+
</section>
|
|
698
|
+
|
|
699
|
+
<!-- ── 🛠️ Developer ── -->
|
|
700
|
+
<section
|
|
701
|
+
class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 shadow-sm overflow-hidden"
|
|
702
|
+
>
|
|
703
|
+
<header
|
|
704
|
+
class="px-6 py-4 border-b border-gray-100 dark:border-gray-800"
|
|
705
|
+
>
|
|
706
|
+
<h3
|
|
707
|
+
class="text-base font-semibold text-gray-800 dark:text-gray-100 flex items-center gap-2"
|
|
708
|
+
>
|
|
709
|
+
<span class="text-xl" aria-hidden="true">🛠️</span>
|
|
710
|
+
<span data-i18n="admin.section.developer.title">Developer</span>
|
|
711
|
+
</h3>
|
|
712
|
+
<p
|
|
713
|
+
class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-8"
|
|
714
|
+
data-i18n="admin.section.developer.desc"
|
|
715
|
+
>
|
|
716
|
+
Advanced options for power users.
|
|
717
|
+
</p>
|
|
718
|
+
</header>
|
|
719
|
+
<div class="px-6 py-4">
|
|
720
|
+
<label class="flex items-center gap-3 cursor-pointer">
|
|
721
|
+
<input
|
|
722
|
+
id="field-debug"
|
|
723
|
+
name="showDiagramDebug"
|
|
724
|
+
type="checkbox"
|
|
725
|
+
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
|
726
|
+
/>
|
|
727
|
+
<span
|
|
728
|
+
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
729
|
+
data-i18n="admin.appearance.debug_label"
|
|
730
|
+
>Show debug button in diagram editor</span
|
|
731
|
+
>
|
|
732
|
+
</label>
|
|
733
|
+
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500 ml-7">
|
|
734
|
+
Displays a Debug button that overlays raw position and size info
|
|
735
|
+
on each node — useful for diagnosing snap-to-grid issues.
|
|
736
|
+
</p>
|
|
737
|
+
</div>
|
|
738
|
+
</section>
|
|
739
|
+
</div>
|
|
740
|
+
</form>
|
|
741
|
+
|
|
742
|
+
<!-- ── AGPL-3.0 License ── -->
|
|
743
|
+
<div class="max-w-3xl mx-auto px-6">
|
|
744
|
+
<div
|
|
745
|
+
class="mt-12 rounded-2xl overflow-hidden border border-blue-100 dark:border-blue-900/40 shadow-sm"
|
|
746
|
+
>
|
|
747
|
+
<!-- Gradient banner -->
|
|
748
|
+
<div
|
|
749
|
+
class="bg-gradient-to-r from-blue-600 via-blue-500 to-indigo-500 px-6 py-5 flex items-center gap-4"
|
|
750
|
+
>
|
|
751
|
+
<div class="text-white text-3xl select-none" aria-hidden="true">
|
|
752
|
+
📖
|
|
753
|
+
</div>
|
|
754
|
+
<div>
|
|
755
|
+
<h3
|
|
756
|
+
class="text-white font-bold text-base tracking-tight"
|
|
757
|
+
data-i18n="admin.license.title"
|
|
758
|
+
>
|
|
759
|
+
AGPL-3.0 License — Open Source
|
|
760
|
+
</h3>
|
|
761
|
+
<p
|
|
762
|
+
class="text-blue-100 text-xs mt-0.5"
|
|
763
|
+
data-i18n="admin.license.subtitle"
|
|
764
|
+
>
|
|
765
|
+
Free to use, fork, and build upon — derivatives and
|
|
766
|
+
network-hosted services must remain under AGPL (copyleft + SaaS
|
|
767
|
+
clause).
|
|
768
|
+
</p>
|
|
769
|
+
</div>
|
|
770
|
+
</div>
|
|
771
|
+
|
|
772
|
+
<!-- Body -->
|
|
773
|
+
<div class="bg-white dark:bg-gray-900 px-6 py-5 space-y-4">
|
|
774
|
+
<p
|
|
775
|
+
class="text-sm text-gray-700 dark:text-gray-300 leading-relaxed"
|
|
776
|
+
data-i18n-html="admin.license.body"
|
|
777
|
+
>
|
|
778
|
+
This open source project is gracefully provided to you by its
|
|
779
|
+
conceptors
|
|
780
|
+
<strong>Youssef MEDAGHRI-ALAOUI</strong> &
|
|
781
|
+
<strong>Claude Code</strong>.
|
|
782
|
+
</p>
|
|
783
|
+
|
|
784
|
+
<div class="flex flex-wrap gap-3 pt-1">
|
|
785
|
+
<a
|
|
786
|
+
href="https://www.linkedin.com/in/youssef-medaghri-alaoui-93b2922/"
|
|
787
|
+
target="_blank"
|
|
788
|
+
rel="noopener noreferrer"
|
|
789
|
+
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-[#0A66C2] hover:bg-[#0958a8] text-white text-sm font-semibold transition-colors shadow-sm"
|
|
790
|
+
>
|
|
791
|
+
<svg
|
|
792
|
+
class="w-4 h-4 shrink-0"
|
|
793
|
+
viewBox="0 0 24 24"
|
|
794
|
+
fill="currentColor"
|
|
795
|
+
aria-hidden="true"
|
|
796
|
+
>
|
|
797
|
+
<path
|
|
798
|
+
d="M20.45 20.45h-3.55v-5.57c0-1.33-.03-3.04-1.85-3.04-1.85 0-2.13 1.44-2.13 2.93v5.68H9.37V9h3.41v1.56h.05c.47-.9 1.63-1.85 3.35-1.85 3.58 0 4.24 2.36 4.24 5.43v6.31ZM5.34 7.43a2.06 2.06 0 1 1 0-4.12 2.06 2.06 0 0 1 0 4.12ZM7.12 20.45H3.56V9h3.56v11.45ZM22.22 0H1.77C.79 0 0 .77 0 1.72v20.56C0 23.23.79 24 1.77 24h20.45C23.2 24 24 23.23 24 22.28V1.72C24 .77 23.2 0 22.22 0Z"
|
|
799
|
+
/>
|
|
800
|
+
</svg>
|
|
801
|
+
<span data-i18n="admin.license.linkedin_btn"
|
|
802
|
+
>Youssef on LinkedIn</span
|
|
803
|
+
>
|
|
804
|
+
</a>
|
|
805
|
+
|
|
806
|
+
<a
|
|
807
|
+
href="https://www.npmjs.com/package/living-ai-documentation"
|
|
808
|
+
target="_blank"
|
|
809
|
+
rel="noopener noreferrer"
|
|
810
|
+
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-[#CB3837] hover:bg-[#a82d2d] text-white text-sm font-semibold transition-colors shadow-sm"
|
|
811
|
+
>
|
|
812
|
+
<svg
|
|
813
|
+
class="w-4 h-4 shrink-0"
|
|
814
|
+
viewBox="0 0 24 24"
|
|
815
|
+
fill="currentColor"
|
|
816
|
+
aria-hidden="true"
|
|
817
|
+
>
|
|
818
|
+
<path
|
|
819
|
+
d="M0 0v24h24V0H0Zm19.2 19.2H13.6V8h-3.2v11.2H4.8V4.8h14.4v14.4Z"
|
|
820
|
+
/>
|
|
821
|
+
</svg>
|
|
822
|
+
<span data-i18n="admin.license.npm_btn"
|
|
823
|
+
>living-ai-documentation on npm</span
|
|
824
|
+
>
|
|
825
|
+
</a>
|
|
826
|
+
</div>
|
|
827
|
+
</div>
|
|
828
|
+
</div>
|
|
829
|
+
</div>
|
|
830
|
+
</main>
|
|
831
|
+
|
|
832
|
+
<script>
|
|
833
|
+
// ── Boot ───────────────────────────────────────────────────
|
|
834
|
+
document.addEventListener("DOMContentLoaded", async () => {
|
|
835
|
+
applyDarkMode(loadDarkPref());
|
|
836
|
+
setupDarkToggle();
|
|
837
|
+
await loadConfig();
|
|
838
|
+
setupPatternPreview();
|
|
839
|
+
document
|
|
840
|
+
.getElementById("config-form")
|
|
841
|
+
.addEventListener("submit", saveConfig);
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
// ── Dark mode ──────────────────────────────────────────────
|
|
845
|
+
function loadDarkPref() {
|
|
846
|
+
const saved = localStorage.getItem("ld-dark");
|
|
847
|
+
if (saved !== null) return saved === "true";
|
|
848
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
849
|
+
}
|
|
850
|
+
function applyDarkMode(dark) {
|
|
851
|
+
document.documentElement.classList.toggle("dark", dark);
|
|
852
|
+
document.getElementById("dark-icon").textContent = dark ? "☀" : "☾";
|
|
853
|
+
}
|
|
854
|
+
function setupDarkToggle() {
|
|
855
|
+
document.getElementById("dark-toggle").addEventListener("click", () => {
|
|
856
|
+
const isDark = document.documentElement.classList.toggle("dark");
|
|
857
|
+
localStorage.setItem("ld-dark", isDark);
|
|
858
|
+
document.getElementById("dark-icon").textContent = isDark ? "☀" : "☾";
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// ── Config ─────────────────────────────────────────────────
|
|
863
|
+
async function loadConfig() {
|
|
864
|
+
try {
|
|
865
|
+
const cfg = await fetch("/api/config").then((r) => r.json());
|
|
866
|
+
await window.initI18n(cfg.language || "en");
|
|
867
|
+
window.applyI18n();
|
|
868
|
+
window._ldCurrentLanguage = cfg.language || "en";
|
|
869
|
+
document.getElementById("info-folder").textContent =
|
|
870
|
+
cfg.docsFolder || "—";
|
|
871
|
+
document.getElementById("info-port").textContent = cfg.port || "—";
|
|
872
|
+
document.getElementById("field-source-root").value =
|
|
873
|
+
cfg.docsFolder && cfg.sourceRoot
|
|
874
|
+
? pathRelative(cfg.docsFolder, cfg.sourceRoot)
|
|
875
|
+
: "";
|
|
876
|
+
document.getElementById("field-title").value = cfg.title || "";
|
|
877
|
+
document.getElementById("field-theme").value = cfg.theme || "system";
|
|
878
|
+
document.getElementById("field-language").value =
|
|
879
|
+
cfg.language || "en";
|
|
880
|
+
document.getElementById("field-pattern").value =
|
|
881
|
+
cfg.filenamePattern || "";
|
|
882
|
+
document.getElementById("field-debug").checked =
|
|
883
|
+
!!cfg.showDiagramDebug;
|
|
884
|
+
document.getElementById("field-exclusive-folder").checked =
|
|
885
|
+
!!cfg.exclusiveFolderExpansion;
|
|
886
|
+
document.getElementById("field-exclusive-category").checked =
|
|
887
|
+
!!cfg.exclusiveCategoryExpansion;
|
|
888
|
+
document.getElementById("field-code-max-height").value =
|
|
889
|
+
typeof cfg.codeBlockMaxHeight === "number"
|
|
890
|
+
? cfg.codeBlockMaxHeight
|
|
891
|
+
: 400;
|
|
892
|
+
document.getElementById("field-soft-breaks").checked =
|
|
893
|
+
!!cfg.markdownSoftBreaks;
|
|
894
|
+
document.getElementById("field-blocked-extensions").value = (
|
|
895
|
+
cfg.blockedFileExtensions || []
|
|
896
|
+
).join(" ");
|
|
897
|
+
updatePreview(cfg.filenamePattern);
|
|
898
|
+
initExtraFiles(cfg);
|
|
899
|
+
initPalettes(cfg);
|
|
900
|
+
} catch {
|
|
901
|
+
showMsg(window.t("admin.msg.load_failed"), "error");
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
async function saveConfig(e) {
|
|
906
|
+
e.preventDefault();
|
|
907
|
+
const filenamePattern = document
|
|
908
|
+
.getElementById("field-pattern")
|
|
909
|
+
.value.trim();
|
|
910
|
+
if (filenamePattern) {
|
|
911
|
+
const catCount = (filenamePattern.match(/\[Category\]/gi) || [])
|
|
912
|
+
.length;
|
|
913
|
+
if (catCount === 0) {
|
|
914
|
+
showMsg(window.t("admin.msg.pattern_no_category"), "error");
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (catCount > 1) {
|
|
918
|
+
showMsg(window.t("admin.msg.pattern_one_category"), "error");
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const sourceRootRaw = document
|
|
923
|
+
.getElementById("field-source-root")
|
|
924
|
+
.value.trim();
|
|
925
|
+
if (sourceRootRaw !== "" && isAbsolutePath(sourceRootRaw)) {
|
|
926
|
+
showMsg(window.t("admin.msg.source_root_absolute"), "error");
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const blockedExtensions = document
|
|
930
|
+
.getElementById("field-blocked-extensions")
|
|
931
|
+
.value.split(/[\s,]+/)
|
|
932
|
+
.map((e) => e.trim().replace(/^\.+/, "").toLowerCase())
|
|
933
|
+
.filter((e) => /^[a-z0-9]+$/.test(e));
|
|
934
|
+
const payload = {
|
|
935
|
+
title: document.getElementById("field-title").value.trim(),
|
|
936
|
+
theme: document.getElementById("field-theme").value,
|
|
937
|
+
language: document.getElementById("field-language").value,
|
|
938
|
+
filenamePattern,
|
|
939
|
+
showDiagramDebug: document.getElementById("field-debug").checked,
|
|
940
|
+
exclusiveFolderExpansion: document.getElementById(
|
|
941
|
+
"field-exclusive-folder",
|
|
942
|
+
).checked,
|
|
943
|
+
exclusiveCategoryExpansion: document.getElementById(
|
|
944
|
+
"field-exclusive-category",
|
|
945
|
+
).checked,
|
|
946
|
+
codeBlockMaxHeight: Math.max(
|
|
947
|
+
0,
|
|
948
|
+
Math.min(
|
|
949
|
+
5000,
|
|
950
|
+
parseInt(
|
|
951
|
+
document.getElementById("field-code-max-height").value,
|
|
952
|
+
10,
|
|
953
|
+
) || 0,
|
|
954
|
+
),
|
|
955
|
+
),
|
|
956
|
+
markdownSoftBreaks:
|
|
957
|
+
document.getElementById("field-soft-breaks").checked,
|
|
958
|
+
diagramNodePalette: [...nodePalette],
|
|
959
|
+
diagramEdgePalette: [...edgePalette],
|
|
960
|
+
sourceRoot: sourceRootRaw === "" ? null : sourceRootRaw,
|
|
961
|
+
blockedFileExtensions: blockedExtensions,
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
try {
|
|
965
|
+
const res = await fetch("/api/config", {
|
|
966
|
+
method: "PUT",
|
|
967
|
+
headers: { "Content-Type": "application/json" },
|
|
968
|
+
body: JSON.stringify(payload),
|
|
969
|
+
});
|
|
970
|
+
if (!res.ok) throw new Error(await res.text());
|
|
971
|
+
if (
|
|
972
|
+
payload.language &&
|
|
973
|
+
payload.language !== window._ldCurrentLanguage
|
|
974
|
+
) {
|
|
975
|
+
await window.initI18n(payload.language);
|
|
976
|
+
window.applyI18n();
|
|
977
|
+
window._ldCurrentLanguage = payload.language;
|
|
978
|
+
updatePreview();
|
|
979
|
+
}
|
|
980
|
+
showMsg(window.t("admin.msg.saved"), "ok");
|
|
981
|
+
} catch (err) {
|
|
982
|
+
showMsg(window.t("admin.msg.save_failed") + err.message, "error");
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function showMsg(text, type) {
|
|
987
|
+
const el = document.getElementById("save-msg");
|
|
988
|
+
el.textContent = text;
|
|
989
|
+
el.className =
|
|
990
|
+
"text-sm " +
|
|
991
|
+
(type === "ok"
|
|
992
|
+
? "text-green-600 dark:text-green-400"
|
|
993
|
+
: "text-red-600 dark:text-red-400");
|
|
994
|
+
if (type === "ok")
|
|
995
|
+
setTimeout(() => {
|
|
996
|
+
el.textContent = "";
|
|
997
|
+
}, 3000);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ── Pattern preview ────────────────────────────────────────
|
|
1001
|
+
const EXAMPLES = [
|
|
1002
|
+
"2024_01_15_09_30_[DevOps]_deploy_pipeline.md",
|
|
1003
|
+
"2023_11_03_14_45_[Frontend]_react_hooks_guide.md",
|
|
1004
|
+
"2025_06_20_08_00_meeting_notes.md",
|
|
1005
|
+
"readme.md",
|
|
1006
|
+
];
|
|
1007
|
+
|
|
1008
|
+
function dateStrToISO(s) {
|
|
1009
|
+
const p = s.split("_");
|
|
1010
|
+
return p.length === 5
|
|
1011
|
+
? p[0] + "-" + p[1] + "-" + p[2] + "T" + p[3] + ":" + p[4]
|
|
1012
|
+
: p[0] + "-" + p[1] + "-" + p[2];
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function buildPatternsFromFormat(patternStr) {
|
|
1016
|
+
if (!patternStr) patternStr = "YYYY_MM_DD_HH_mm_[Category]_title";
|
|
1017
|
+
const hasDate = /YYYY.*MM.*DD/.test(patternStr);
|
|
1018
|
+
const hasTime = hasDate && /HH.*mm/.test(patternStr);
|
|
1019
|
+
const hasCategory = /\[Category\]/i.test(patternStr);
|
|
1020
|
+
const dateGroup = hasTime
|
|
1021
|
+
? "(\\d{4}_\\d{2}_\\d{2}(?:_\\d{2}_\\d{2})?)"
|
|
1022
|
+
: "(\\d{4}_\\d{2}_\\d{2})";
|
|
1023
|
+
const catGroup = "\\[([^\\]]+)\\]";
|
|
1024
|
+
const catBeforeDate =
|
|
1025
|
+
hasDate &&
|
|
1026
|
+
hasCategory &&
|
|
1027
|
+
patternStr.search(/\[Category\]/i) < patternStr.search(/YYYY/i);
|
|
1028
|
+
let full = null,
|
|
1029
|
+
dateOnly = null;
|
|
1030
|
+
if (hasDate && hasCategory) {
|
|
1031
|
+
const ordered = catBeforeDate
|
|
1032
|
+
? catGroup + "_" + dateGroup
|
|
1033
|
+
: dateGroup + "_" + catGroup;
|
|
1034
|
+
full = new RegExp("^" + ordered + "_(.+)\\.md$", "i");
|
|
1035
|
+
dateOnly = new RegExp("^" + dateGroup + "_(.+)\\.md$", "i");
|
|
1036
|
+
} else if (hasDate) {
|
|
1037
|
+
dateOnly = new RegExp("^" + dateGroup + "_(.+)\\.md$", "i");
|
|
1038
|
+
} else if (hasCategory) {
|
|
1039
|
+
full = new RegExp("^" + catGroup + "_(.+)\\.md$", "i");
|
|
1040
|
+
}
|
|
1041
|
+
return { full, dateOnly, hasDate, hasCategory, catBeforeDate };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function parsePreview(filename, patterns) {
|
|
1045
|
+
if (patterns.full) {
|
|
1046
|
+
const m = filename.match(patterns.full);
|
|
1047
|
+
if (m) {
|
|
1048
|
+
if (patterns.hasDate && patterns.hasCategory) {
|
|
1049
|
+
const dateStr = patterns.catBeforeDate ? m[2] : m[1];
|
|
1050
|
+
const category = patterns.catBeforeDate ? m[1] : m[2];
|
|
1051
|
+
return {
|
|
1052
|
+
date: dateStrToISO(dateStr),
|
|
1053
|
+
category,
|
|
1054
|
+
title: titleCase(m[3]),
|
|
1055
|
+
match: true,
|
|
1056
|
+
};
|
|
1057
|
+
} else if (patterns.hasCategory) {
|
|
1058
|
+
return {
|
|
1059
|
+
date: null,
|
|
1060
|
+
category: m[1],
|
|
1061
|
+
title: titleCase(m[2]),
|
|
1062
|
+
match: true,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (patterns.dateOnly) {
|
|
1068
|
+
const m = filename.match(patterns.dateOnly);
|
|
1069
|
+
if (m) {
|
|
1070
|
+
return {
|
|
1071
|
+
date: dateStrToISO(m[1]),
|
|
1072
|
+
category: "General",
|
|
1073
|
+
title: titleCase(m[2]),
|
|
1074
|
+
match: true,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return {
|
|
1079
|
+
date: null,
|
|
1080
|
+
category: "General",
|
|
1081
|
+
title: filename.replace(".md", ""),
|
|
1082
|
+
match: false,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function titleCase(s) {
|
|
1087
|
+
return s
|
|
1088
|
+
.split("_")
|
|
1089
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
1090
|
+
.join(" ");
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function updatePreview() {
|
|
1094
|
+
const patternVal = document
|
|
1095
|
+
.getElementById("field-pattern")
|
|
1096
|
+
.value.trim();
|
|
1097
|
+
const descSpan = document.getElementById("pattern-desc-example");
|
|
1098
|
+
descSpan.textContent =
|
|
1099
|
+
(patternVal || "YYYY_MM_DD_HH_mm_[Category]_title") + ".md";
|
|
1100
|
+
const patterns = buildPatternsFromFormat(patternVal);
|
|
1101
|
+
const rows = document.getElementById("preview-rows");
|
|
1102
|
+
const tr = (k, fb) =>
|
|
1103
|
+
(typeof window.t === "function" && window.t(k)) || fb;
|
|
1104
|
+
const labelDate = tr("admin.pattern.col_date", "Date");
|
|
1105
|
+
const labelCat = tr("admin.pattern.col_category", "Category");
|
|
1106
|
+
const labelTitle = tr("admin.pattern.col_title", "Title");
|
|
1107
|
+
const labelNone = tr("admin.pattern.col_none", "none");
|
|
1108
|
+
rows.innerHTML = EXAMPLES.map((f) => {
|
|
1109
|
+
const p = parsePreview(f, patterns);
|
|
1110
|
+
return `
|
|
1111
|
+
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-3">
|
|
1112
|
+
<p class="font-mono text-xs text-gray-500 dark:text-gray-400 mb-2 break-all">${esc(f)}</p>
|
|
1113
|
+
<div class="grid grid-cols-3 gap-2 text-xs">
|
|
1114
|
+
<div>
|
|
1115
|
+
<span class="text-gray-400 block mb-0.5">${labelDate}</span>
|
|
1116
|
+
<span class="${p.date ? "text-green-600 dark:text-green-400" : "text-gray-400"}">${p.date || labelNone}</span>
|
|
1117
|
+
</div>
|
|
1118
|
+
<div>
|
|
1119
|
+
<span class="text-gray-400 block mb-0.5">${labelCat}</span>
|
|
1120
|
+
<span class="text-blue-600 dark:text-blue-400">${esc(p.category)}</span>
|
|
1121
|
+
</div>
|
|
1122
|
+
<div>
|
|
1123
|
+
<span class="text-gray-400 block mb-0.5">${labelTitle}</span>
|
|
1124
|
+
<span class="text-gray-700 dark:text-gray-300 truncate block">${esc(p.title)}</span>
|
|
1125
|
+
</div>
|
|
1126
|
+
</div>
|
|
1127
|
+
</div>`;
|
|
1128
|
+
}).join("");
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function setupPatternPreview() {
|
|
1132
|
+
document
|
|
1133
|
+
.getElementById("field-pattern")
|
|
1134
|
+
.addEventListener("input", updatePreview);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function esc(s) {
|
|
1138
|
+
return String(s)
|
|
1139
|
+
.replace(/&/g, "&")
|
|
1140
|
+
.replace(/</g, "<")
|
|
1141
|
+
.replace(/>/g, ">")
|
|
1142
|
+
.replace(/"/g, """)
|
|
1143
|
+
.replace(/'/g, "'");
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// ── Extra Files / Browser ──────────────────────────────────
|
|
1147
|
+
// extraFiles is stored internally as absolute paths (matching what GET /api/config returns
|
|
1148
|
+
// and what /api/browse emits). On save, paths are converted to relative before PUT.
|
|
1149
|
+
let extraFiles = [];
|
|
1150
|
+
let browseParent = null;
|
|
1151
|
+
let browseCurrent = "";
|
|
1152
|
+
let docsFolder = "";
|
|
1153
|
+
|
|
1154
|
+
function initExtraFiles(cfg) {
|
|
1155
|
+
extraFiles = cfg.extraFiles || [];
|
|
1156
|
+
docsFolder = cfg.docsFolder || "";
|
|
1157
|
+
renderExtraFiles();
|
|
1158
|
+
loadBrowse(cfg.docsFolder || "/");
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// Returns true if the path looks absolute (unix, Windows, or tilde-home).
|
|
1162
|
+
function isAbsolutePath(p) {
|
|
1163
|
+
if (!p) return false;
|
|
1164
|
+
return /^([/\\]|[a-zA-Z]:[\\/]|~)/.test(p);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Posix-style path.relative(from, to). Returns "." when equal, "../x" etc.
|
|
1168
|
+
function pathRelative(from, to) {
|
|
1169
|
+
if (!from) return to;
|
|
1170
|
+
const norm = (s) => s.replace(/\\/g, "/");
|
|
1171
|
+
const fromParts = norm(from).split("/").filter(Boolean);
|
|
1172
|
+
const toParts = norm(to).split("/").filter(Boolean);
|
|
1173
|
+
let common = 0;
|
|
1174
|
+
while (
|
|
1175
|
+
common < fromParts.length &&
|
|
1176
|
+
common < toParts.length &&
|
|
1177
|
+
fromParts[common] === toParts[common]
|
|
1178
|
+
)
|
|
1179
|
+
common++;
|
|
1180
|
+
const ups = fromParts.length - common;
|
|
1181
|
+
const downs = toParts.slice(common).join("/");
|
|
1182
|
+
let rel = "../".repeat(ups) + downs;
|
|
1183
|
+
if (rel.endsWith("/")) rel = rel.slice(0, -1);
|
|
1184
|
+
return rel === "" ? "." : rel;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function relativeDisplay(filePath) {
|
|
1188
|
+
if (!docsFolder) return filePath;
|
|
1189
|
+
return "$DOCS_FOLDER/" + pathRelative(docsFolder, filePath);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
async function loadBrowse(dirPath) {
|
|
1193
|
+
const list = document.getElementById("browse-list");
|
|
1194
|
+
list.innerHTML =
|
|
1195
|
+
'<p class="px-3 py-4 text-xs text-gray-400 text-center">Loading…</p>';
|
|
1196
|
+
try {
|
|
1197
|
+
const data = await fetch(
|
|
1198
|
+
"/api/browse?path=" + encodeURIComponent(dirPath),
|
|
1199
|
+
).then((r) => r.json());
|
|
1200
|
+
browseCurrent = data.current;
|
|
1201
|
+
browseParent = data.parent;
|
|
1202
|
+
document.getElementById("browse-path").textContent = relativeDisplay(
|
|
1203
|
+
data.current,
|
|
1204
|
+
);
|
|
1205
|
+
document.getElementById("browse-up").disabled = !data.parent;
|
|
1206
|
+
|
|
1207
|
+
const rows = [];
|
|
1208
|
+
|
|
1209
|
+
for (const dir of data.dirs) {
|
|
1210
|
+
rows.push(`
|
|
1211
|
+
<button data-path="${esc(dir.path)}" onclick="loadBrowse(this.dataset.path)"
|
|
1212
|
+
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-left
|
|
1213
|
+
hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
1214
|
+
<span class="text-gray-400 shrink-0">📁</span>
|
|
1215
|
+
<span class="text-gray-700 dark:text-gray-300 truncate">${esc(dir.name)}</span>
|
|
1216
|
+
</button>`);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
for (const file of data.files) {
|
|
1220
|
+
const added = extraFiles.includes(file.path);
|
|
1221
|
+
rows.push(`
|
|
1222
|
+
<div class="flex items-center justify-between gap-2 px-3 py-2 text-sm
|
|
1223
|
+
hover:bg-gray-50 dark:hover:bg-gray-800">
|
|
1224
|
+
<span class="flex items-center gap-2 text-gray-700 dark:text-gray-300 min-w-0">
|
|
1225
|
+
<span class="text-gray-400 shrink-0">📄</span>
|
|
1226
|
+
<span class="truncate">${esc(file.name)}</span>
|
|
1227
|
+
</span>
|
|
1228
|
+
${
|
|
1229
|
+
added
|
|
1230
|
+
? '<span class="text-xs text-green-600 dark:text-green-400 shrink-0">Added</span>'
|
|
1231
|
+
: `<button data-path="${esc(file.path)}" onclick="addExtraFile(this.dataset.path)"
|
|
1232
|
+
class="text-xs text-blue-600 dark:text-blue-400 hover:underline shrink-0">+ Add</button>`
|
|
1233
|
+
}
|
|
1234
|
+
</div>`);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (rows.length === 0) {
|
|
1238
|
+
list.innerHTML =
|
|
1239
|
+
'<p class="px-3 py-4 text-xs text-gray-400 text-center">Empty directory</p>';
|
|
1240
|
+
} else {
|
|
1241
|
+
list.innerHTML = rows.join("");
|
|
1242
|
+
}
|
|
1243
|
+
} catch {
|
|
1244
|
+
list.innerHTML =
|
|
1245
|
+
'<p class="px-3 py-4 text-xs text-red-400 text-center">Cannot read directory</p>';
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function browseUp() {
|
|
1250
|
+
if (browseParent) loadBrowse(browseParent);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
async function addExtraFile(filePath) {
|
|
1254
|
+
if (extraFiles.includes(filePath)) return;
|
|
1255
|
+
extraFiles = [...extraFiles, filePath];
|
|
1256
|
+
await saveExtraFiles();
|
|
1257
|
+
renderExtraFiles();
|
|
1258
|
+
loadBrowse(browseCurrent);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
async function removeExtraFile(filePath) {
|
|
1262
|
+
extraFiles = extraFiles.filter((f) => f !== filePath);
|
|
1263
|
+
await saveExtraFiles();
|
|
1264
|
+
renderExtraFiles();
|
|
1265
|
+
loadBrowse(browseCurrent);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
async function saveExtraFiles() {
|
|
1269
|
+
try {
|
|
1270
|
+
const relativeEntries = extraFiles.map((abs) =>
|
|
1271
|
+
pathRelative(docsFolder, abs),
|
|
1272
|
+
);
|
|
1273
|
+
const res = await fetch("/api/config", {
|
|
1274
|
+
method: "PUT",
|
|
1275
|
+
headers: { "Content-Type": "application/json" },
|
|
1276
|
+
body: JSON.stringify({ extraFiles: relativeEntries }),
|
|
1277
|
+
});
|
|
1278
|
+
if (!res.ok) throw new Error(await res.text());
|
|
1279
|
+
} catch (err) {
|
|
1280
|
+
showMsg("Failed to save: " + err.message, "error");
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function moveExtraFile(index, direction) {
|
|
1285
|
+
const target = index + direction;
|
|
1286
|
+
if (target < 0 || target >= extraFiles.length) return;
|
|
1287
|
+
const next = [...extraFiles];
|
|
1288
|
+
[next[index], next[target]] = [next[target], next[index]];
|
|
1289
|
+
extraFiles = next;
|
|
1290
|
+
saveExtraFiles();
|
|
1291
|
+
renderExtraFiles();
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// ── Diagram Palettes ───────────────────────────────────────
|
|
1295
|
+
// NODE_COLORS_ADMIN mirrors constants.js NODE_COLORS (default bg values).
|
|
1296
|
+
// Mirrors NODE_COLORS from constants.js (bg + original border).
|
|
1297
|
+
const NODE_COLORS_ADMIN = {
|
|
1298
|
+
"c-white": { bg: "#ffffff", border: "#dbd5d1", label: "White" },
|
|
1299
|
+
"c-gray": { bg: "#f5f5f4", border: "#a8a29e", label: "Gris" },
|
|
1300
|
+
"c-slate": { bg: "#f1f5f9", border: "#64748b", label: "Ardoise" },
|
|
1301
|
+
"c-blue": { bg: "#dbeafe", border: "#3b82f6", label: "Bleu" },
|
|
1302
|
+
"c-sky": { bg: "#e0f2fe", border: "#0ea5e9", label: "Ciel" },
|
|
1303
|
+
"c-cyan": { bg: "#cffafe", border: "#06b6d4", label: "Cyan" },
|
|
1304
|
+
"c-teal": { bg: "#ccfbf1", border: "#14b8a6", label: "Teal" },
|
|
1305
|
+
"c-green": { bg: "#dcfce7", border: "#22c55e", label: "Vert" },
|
|
1306
|
+
"c-lime": { bg: "#ecfccb", border: "#84cc16", label: "Lime" },
|
|
1307
|
+
"c-amber": { bg: "#fef9c3", border: "#f59e0b", label: "Ambre" },
|
|
1308
|
+
"c-orange": { bg: "#ffedd5", border: "#f97316", label: "Orange" },
|
|
1309
|
+
"c-red": { bg: "#fee2e2", border: "#ef4444", label: "Rouge" },
|
|
1310
|
+
"c-rose": { bg: "#ffe4e6", border: "#f43f5e", label: "Rose" },
|
|
1311
|
+
"c-pink": { bg: "#fce7f3", border: "#ec4899", label: "Fuschia" },
|
|
1312
|
+
"c-purple": { bg: "#ede9fe", border: "#8b5cf6", label: "Violet" },
|
|
1313
|
+
};
|
|
1314
|
+
const NODE_PALETTE_KEYS = Object.keys(NODE_COLORS_ADMIN); // fixed order of 15 slots
|
|
1315
|
+
const DEFAULT_EDGE_PALETTE_ADMIN = [
|
|
1316
|
+
"#ffffff",
|
|
1317
|
+
"#a8a29e",
|
|
1318
|
+
"#374151",
|
|
1319
|
+
"#3b82f6",
|
|
1320
|
+
"#14b8a6",
|
|
1321
|
+
"#22c55e",
|
|
1322
|
+
"#f97316",
|
|
1323
|
+
"#ef4444",
|
|
1324
|
+
"#a855f7",
|
|
1325
|
+
];
|
|
1326
|
+
|
|
1327
|
+
// nodePalette: array of 15 bg hex strings, one per slot (positional).
|
|
1328
|
+
let nodePalette = NODE_PALETTE_KEYS.map((k) => NODE_COLORS_ADMIN[k].bg);
|
|
1329
|
+
let edgePalette = [...DEFAULT_EDGE_PALETTE_ADMIN];
|
|
1330
|
+
|
|
1331
|
+
// Per-slot lightness ratio (border_L / bg_L) — mirrors NODE_L_RATIOS from constants.js.
|
|
1332
|
+
const NODE_L_RATIOS_ADMIN = {
|
|
1333
|
+
"c-gray": 0.667,
|
|
1334
|
+
"c-slate": 0.488,
|
|
1335
|
+
"c-blue": 0.645,
|
|
1336
|
+
"c-sky": 0.518,
|
|
1337
|
+
"c-cyan": 0.473,
|
|
1338
|
+
"c-teal": 0.448,
|
|
1339
|
+
"c-green": 0.489,
|
|
1340
|
+
"c-lime": 0.496,
|
|
1341
|
+
"c-amber": 0.57,
|
|
1342
|
+
"c-orange": 0.578,
|
|
1343
|
+
"c-red": 0.64,
|
|
1344
|
+
"c-rose": 0.636,
|
|
1345
|
+
"c-pink": 0.638,
|
|
1346
|
+
"c-purple": 0.694,
|
|
1347
|
+
"c-indigo": 0.71,
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
// Mirrors deriveNodeColors from constants.js (no ES module in admin.html).
|
|
1351
|
+
function deriveNodeColors(bgHex, lRatio = 0.6) {
|
|
1352
|
+
const r = parseInt(bgHex.slice(1, 3), 16) / 255;
|
|
1353
|
+
const g = parseInt(bgHex.slice(3, 5), 16) / 255;
|
|
1354
|
+
const b = parseInt(bgHex.slice(5, 7), 16) / 255;
|
|
1355
|
+
const max = Math.max(r, g, b),
|
|
1356
|
+
min = Math.min(r, g, b);
|
|
1357
|
+
const l = (max + min) / 2;
|
|
1358
|
+
const d = max - min;
|
|
1359
|
+
let h = 0,
|
|
1360
|
+
s = 0;
|
|
1361
|
+
if (d > 0) {
|
|
1362
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
1363
|
+
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
1364
|
+
else if (max === g) h = ((b - r) / d + 2) / 6;
|
|
1365
|
+
else h = ((r - g) / d + 4) / 6;
|
|
1366
|
+
}
|
|
1367
|
+
function hslToHex(hh, ss, ll) {
|
|
1368
|
+
let rr, gg, bb;
|
|
1369
|
+
if (ss === 0) {
|
|
1370
|
+
rr = gg = bb = ll;
|
|
1371
|
+
} else {
|
|
1372
|
+
const hue2rgb = (p, q, t) => {
|
|
1373
|
+
if (t < 0) t += 1;
|
|
1374
|
+
if (t > 1) t -= 1;
|
|
1375
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
1376
|
+
if (t < 1 / 2) return q;
|
|
1377
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
1378
|
+
return p;
|
|
1379
|
+
};
|
|
1380
|
+
const q = ll < 0.5 ? ll * (1 + ss) : ll + ss - ll * ss;
|
|
1381
|
+
const p = 2 * ll - q;
|
|
1382
|
+
rr = hue2rgb(p, q, hh + 1 / 3);
|
|
1383
|
+
gg = hue2rgb(p, q, hh);
|
|
1384
|
+
bb = hue2rgb(p, q, hh - 1 / 3);
|
|
1385
|
+
}
|
|
1386
|
+
return (
|
|
1387
|
+
"#" +
|
|
1388
|
+
[rr, gg, bb]
|
|
1389
|
+
.map((v) =>
|
|
1390
|
+
Math.max(0, Math.min(255, Math.round(v * 255)))
|
|
1391
|
+
.toString(16)
|
|
1392
|
+
.padStart(2, "0"),
|
|
1393
|
+
)
|
|
1394
|
+
.join("")
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
const border = hslToHex(h, s, l * lRatio);
|
|
1398
|
+
return { bg: bgHex, border };
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function initPalettes(cfg) {
|
|
1402
|
+
// nodePalette: positional array of 15 bg hex strings
|
|
1403
|
+
if (
|
|
1404
|
+
Array.isArray(cfg.diagramNodePalette) &&
|
|
1405
|
+
cfg.diagramNodePalette.length
|
|
1406
|
+
) {
|
|
1407
|
+
nodePalette = NODE_PALETTE_KEYS.map(
|
|
1408
|
+
(k, i) => cfg.diagramNodePalette[i] || NODE_COLORS_ADMIN[k].bg,
|
|
1409
|
+
);
|
|
1410
|
+
} else {
|
|
1411
|
+
nodePalette = NODE_PALETTE_KEYS.map((k) => NODE_COLORS_ADMIN[k].bg);
|
|
1412
|
+
}
|
|
1413
|
+
edgePalette =
|
|
1414
|
+
Array.isArray(cfg.diagramEdgePalette) && cfg.diagramEdgePalette.length
|
|
1415
|
+
? [...cfg.diagramEdgePalette]
|
|
1416
|
+
: [...DEFAULT_EDGE_PALETTE_ADMIN];
|
|
1417
|
+
renderNodePalette();
|
|
1418
|
+
renderEdgePalette();
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function swatchBorder(key, bg) {
|
|
1422
|
+
// Use original hand-crafted border if bg is unchanged; derive for custom colours.
|
|
1423
|
+
const def = NODE_COLORS_ADMIN[key];
|
|
1424
|
+
return bg === def.bg
|
|
1425
|
+
? def.border
|
|
1426
|
+
: deriveNodeColors(bg, NODE_L_RATIOS_ADMIN[key] || 0.6).border;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function renderNodePalette() {
|
|
1430
|
+
const container = document.getElementById("nodeSwatches");
|
|
1431
|
+
container.innerHTML = NODE_PALETTE_KEYS.map((key, i) => {
|
|
1432
|
+
const bg = nodePalette[i] || NODE_COLORS_ADMIN[key].bg;
|
|
1433
|
+
const border = swatchBorder(key, bg);
|
|
1434
|
+
return `<div style="position:relative;width:32px;height:32px" title="${esc(NODE_COLORS_ADMIN[key].label)}">
|
|
1435
|
+
<div style="width:32px;height:32px;border-radius:50%;background:${bg};border:3px solid ${border};pointer-events:none"></div>
|
|
1436
|
+
<input type="color" value="${bg}" data-i="${i}"
|
|
1437
|
+
oninput="previewNodeColor(${i},this.value)"
|
|
1438
|
+
onchange="commitNodeColor(${i},this.value)"
|
|
1439
|
+
style="position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;cursor:pointer;border:none;padding:0" />
|
|
1440
|
+
</div>`;
|
|
1441
|
+
}).join("");
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function previewNodeColor(i, hex) {
|
|
1445
|
+
nodePalette[i] = hex.toLowerCase();
|
|
1446
|
+
const key = NODE_PALETTE_KEYS[i];
|
|
1447
|
+
const border = swatchBorder(key, hex.toLowerCase());
|
|
1448
|
+
const wrapper = document.getElementById("nodeSwatches").children[i];
|
|
1449
|
+
if (wrapper) {
|
|
1450
|
+
const swatch = wrapper.querySelector("div");
|
|
1451
|
+
if (swatch) {
|
|
1452
|
+
swatch.style.background = hex;
|
|
1453
|
+
swatch.style.borderColor = border;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function commitNodeColor(i, hex) {
|
|
1459
|
+
previewNodeColor(i, hex);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function resetNodePalette() {
|
|
1463
|
+
nodePalette = NODE_PALETTE_KEYS.map((k) => NODE_COLORS_ADMIN[k].bg);
|
|
1464
|
+
renderNodePalette();
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function renderEdgePalette() {
|
|
1468
|
+
const container = document.getElementById("edgeSwatches");
|
|
1469
|
+
container.innerHTML = edgePalette
|
|
1470
|
+
.map(
|
|
1471
|
+
(hex, i) => `
|
|
1472
|
+
<div style="display:flex;flex-direction:column;align-items:center;gap:3px">
|
|
1473
|
+
<div style="width:26px;height:26px;border-radius:50%;background:${hex};border:2px solid rgba(0,0,0,0.15)"></div>
|
|
1474
|
+
<button type="button" data-i="${i}" onclick="removeEdgeColor(+this.dataset.i)"
|
|
1475
|
+
style="font-size:10px;color:#ef4444;cursor:pointer;line-height:1;background:none;border:none"
|
|
1476
|
+
title="Supprimer">✕</button>
|
|
1477
|
+
</div>`,
|
|
1478
|
+
)
|
|
1479
|
+
.join("");
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function addEdgeColor() {
|
|
1483
|
+
const hex = document.getElementById("newEdgeColor").value.toLowerCase();
|
|
1484
|
+
if (edgePalette.includes(hex)) return;
|
|
1485
|
+
edgePalette = [...edgePalette, hex];
|
|
1486
|
+
renderEdgePalette();
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function removeEdgeColor(index) {
|
|
1490
|
+
if (edgePalette.length <= 1) return;
|
|
1491
|
+
edgePalette = edgePalette.filter((_, i) => i !== index);
|
|
1492
|
+
renderEdgePalette();
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function resetEdgePalette() {
|
|
1496
|
+
edgePalette = [...DEFAULT_EDGE_PALETTE_ADMIN];
|
|
1497
|
+
renderEdgePalette();
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function renderExtraFiles() {
|
|
1501
|
+
const list = document.getElementById("extra-files-list");
|
|
1502
|
+
const empty = document.getElementById("extra-files-empty");
|
|
1503
|
+
if (extraFiles.length === 0) {
|
|
1504
|
+
list.innerHTML = "";
|
|
1505
|
+
empty.style.display = "";
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
empty.style.display = "none";
|
|
1509
|
+
list.innerHTML = extraFiles
|
|
1510
|
+
.map(
|
|
1511
|
+
(f, i) => `
|
|
1512
|
+
<div class="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 dark:bg-gray-800">
|
|
1513
|
+
<div class="flex flex-col shrink-0">
|
|
1514
|
+
<button data-i="${i}" data-d="-1" onclick="moveExtraFile(+this.dataset.i, +this.dataset.d)"
|
|
1515
|
+
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 leading-none disabled:opacity-20"
|
|
1516
|
+
${i === 0 ? "disabled" : ""}>↑</button>
|
|
1517
|
+
<button data-i="${i}" data-d="1" onclick="moveExtraFile(+this.dataset.i, +this.dataset.d)"
|
|
1518
|
+
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 leading-none disabled:opacity-20"
|
|
1519
|
+
${i === extraFiles.length - 1 ? "disabled" : ""}>↓</button>
|
|
1520
|
+
</div>
|
|
1521
|
+
<div class="overflow-x-auto flex-1 min-w-0">
|
|
1522
|
+
<span class="font-mono text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap" title="${esc(f)}">${esc(relativeDisplay(f))}</span>
|
|
1523
|
+
</div>
|
|
1524
|
+
<button data-path="${esc(f)}" onclick="removeExtraFile(this.dataset.path)"
|
|
1525
|
+
class="text-xs text-red-500 hover:text-red-700 dark:hover:text-red-400 shrink-0">Remove</button>
|
|
1526
|
+
</div>`,
|
|
1527
|
+
)
|
|
1528
|
+
.join("");
|
|
1529
|
+
}
|
|
1530
|
+
</script>
|
|
1531
|
+
</body>
|
|
1532
|
+
</html>
|