living-documentation 7.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.md +329 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +62 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/frontend/admin.html +1073 -0
- package/dist/src/frontend/annotations.js +546 -0
- package/dist/src/frontend/boot.js +90 -0
- package/dist/src/frontend/config.js +19 -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 +172 -0
- package/dist/src/frontend/diagram/constants.js +109 -0
- package/dist/src/frontend/diagram/debug.js +43 -0
- package/dist/src/frontend/diagram/edge-panel.js +260 -0
- package/dist/src/frontend/diagram/edge-rendering.js +12 -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 +153 -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 +299 -0
- package/dist/src/frontend/diagram/network.js +1473 -0
- package/dist/src/frontend/diagram/node-panel.js +267 -0
- package/dist/src/frontend/diagram/node-rendering.js +773 -0
- package/dist/src/frontend/diagram/persistence.js +161 -0
- package/dist/src/frontend/diagram/ports.js +386 -0
- package/dist/src/frontend/diagram/selection-overlay.js +336 -0
- package/dist/src/frontend/diagram/state.js +39 -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 +182 -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 +1279 -0
- package/dist/src/frontend/documents.js +373 -0
- package/dist/src/frontend/export.js +338 -0
- package/dist/src/frontend/i18n/en.json +406 -0
- package/dist/src/frontend/i18n/fr.json +406 -0
- package/dist/src/frontend/i18n.js +32 -0
- package/dist/src/frontend/image-paste.js +101 -0
- package/dist/src/frontend/index.html +2314 -0
- package/dist/src/frontend/misc.js +25 -0
- package/dist/src/frontend/new-doc-modal.js +260 -0
- package/dist/src/frontend/new-folder-modal.js +174 -0
- package/dist/src/frontend/search.js +157 -0
- package/dist/src/frontend/sidebar-helpers.js +58 -0
- package/dist/src/frontend/sidebar.js +182 -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 +534 -0
- package/dist/src/frontend/state.js +28 -0
- package/dist/src/frontend/utils.js +21 -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 +17 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +79 -0
- package/dist/src/lib/config.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/mcp/server.d.ts +3 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +986 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/diagrams.d.ts +44 -0
- package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
- package/dist/src/mcp/tools/diagrams.js +245 -0
- package/dist/src/mcp/tools/diagrams.js.map +1 -0
- package/dist/src/mcp/tools/documents.d.ts +26 -0
- package/dist/src/mcp/tools/documents.d.ts.map +1 -0
- package/dist/src/mcp/tools/documents.js +127 -0
- package/dist/src/mcp/tools/documents.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 +200 -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.d.ts +3 -0
- package/dist/src/routes/browse.d.ts.map +1 -0
- package/dist/src/routes/browse.js +75 -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 +97 -0
- package/dist/src/routes/config.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 +8 -0
- package/dist/src/routes/documents.d.ts.map +1 -0
- package/dist/src/routes/documents.js +332 -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 +277 -0
- package/dist/src/routes/export.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/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 +76 -0
- package/dist/src/server.js.map +1 -0
- package/dist/starting-doc/.annotations.json +3 -0
- package/dist/starting-doc/.diagrams.json +1884 -0
- package/dist/starting-doc/.living-doc.json +39 -0
- package/dist/starting-doc/1_tutorial/2026_04_11_13_25_[General]_crer_vos_dossiers.md +16 -0
- package/dist/starting-doc/1_tutorial/2026_04_11_18_58_[General]_creer_un_document_dans_un_dossier.md +9 -0
- package/dist/starting-doc/1_tutorial/2026_04_12_09_00_[General]_editer_et_sauvegarder.md +39 -0
- package/dist/starting-doc/1_tutorial/2026_04_12_10_00_[General]_utiliser_les_snippets.md +71 -0
- package/dist/starting-doc/2026_04_08_20_52_[General]_welcome.md +17 -0
- package/dist/starting-doc/2026_04_11_12_55_[General]_premiers_pas.md +271 -0
- package/dist/starting-doc/2_guide/2026_04_08_00_04_[DOCUMENT]_utilisation_des_images_plein_ecran_lien_clickable.md +40 -0
- package/dist/starting-doc/2_guide/2026_04_08_23_38_[Configuration]_demarrage_de_living_documentation.md +32 -0
- package/dist/starting-doc/2_guide/2026_04_09_09_00_[NAVIGATION]_recherche_plein_texte.md +65 -0
- package/dist/starting-doc/2_guide/2026_04_09_10_00_[EXPORT]_exporter_en_pdf.md +43 -0
- package/dist/starting-doc/2_guide/2026_04_09_11_00_[Configuration]_configurer_le_panneau_admin.md +55 -0
- package/dist/starting-doc/2_guide/2026_04_09_12_00_[Configuration]_extra_files.md +68 -0
- package/dist/starting-doc/2_guide/2026_04_09_13_00_[WORDCLOUD]_word_cloud.md +54 -0
- package/dist/starting-doc/2_guide/2026_04_09_14_00_[DIAGRAM]_creer_et_lier_un_diagramme.md +77 -0
- package/dist/starting-doc/3_concept/2026_04_08_20_58_[DOCUMENTING]_ADRS.md +20 -0
- package/dist/starting-doc/3_concept/2026_04_08_22_15_[DOCUMENTING]_living_documentation.md +17 -0
- package/dist/starting-doc/3_concept/2026_04_08_22_46_[METHODOLOGY]_diataxis_architecture_du_contenu.md +16 -0
- package/dist/starting-doc/4_reference/2026_04_08_23_14_[FUNDAMENTALS]_the_living_documentation_tool.md +41 -0
- package/dist/starting-doc/4_reference/2026_04_09_01_00_[REFERENCE]_raccourcis_clavier.md +61 -0
- package/dist/starting-doc/4_reference/2026_04_09_02_00_[REFERENCE]_tokens_pattern_nommage.md +75 -0
- package/dist/starting-doc/4_reference/2026_04_09_03_00_[REFERENCE]_types_de_snippets.md +68 -0
- package/dist/starting-doc/4_reference/2026_04_11_17_31_[FUNDAMENTALS]_architecturer_une_documentation.md +12 -0
- package/dist/starting-doc/4_reference/2026_04_12_14_07_[FUNDAMENTALS]_dossiers_et_catgories.md +89 -0
- package/dist/starting-doc/images/admin_screenshot.png +0 -0
- package/dist/starting-doc/images/ajout-document.png +0 -0
- package/dist/starting-doc/images/ajouter-document-categorie.png +0 -0
- package/dist/starting-doc/images/ajouter_un_document_dans_un_dossier.png +0 -0
- package/dist/starting-doc/images/architecturer_une_documentation_reference.png +0 -0
- package/dist/starting-doc/images/cr_er_un_document.png +0 -0
- package/dist/starting-doc/images/creation-nouveau-dossier.png +0 -0
- package/dist/starting-doc/images/creer-document-context-engineering.png +0 -0
- package/dist/starting-doc/images/creer-dossier-only-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-dossier-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-dossiers-done.png +0 -0
- package/dist/starting-doc/images/creer-un-document.png +0 -0
- package/dist/starting-doc/images/creer-vos-dossiers-tutoriel.png +0 -0
- package/dist/starting-doc/images/creer-vos-dossiers.png +0 -0
- package/dist/starting-doc/images/decouverte_adrs.png +0 -0
- package/dist/starting-doc/images/diataxis.png +0 -0
- package/dist/starting-doc/images/diataxis_callout.png +0 -0
- package/dist/starting-doc/images/document-cree.png +0 -0
- package/dist/starting-doc/images/liens_snippets.png +0 -0
- package/dist/starting-doc/images/living_documentation.png +0 -0
- package/dist/starting-doc/images/npm_logo.png +0 -0
- package/dist/starting-doc/images/popup-creer-document.png +0 -0
- package/dist/starting-doc/images/popup-creer-dossier.png +0 -0
- package/dist/starting-doc/images/popup-dossier-cree.png +0 -0
- package/dist/starting-doc/images/quatre-dossiers-crees.png +0 -0
- package/dist/starting-doc/images/screenshot-living-doc.png +0 -0
- package/dist/starting-doc/images/the_living_documentation_tool.png +0 -0
- package/package.json +49 -0
|
@@ -0,0 +1,1073 @@
|
|
|
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="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 class="text-base font-semibold tracking-tight" data-i18n="admin.title">Admin Panel</h1>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="flex items-center gap-3">
|
|
33
|
+
<button
|
|
34
|
+
id="dark-toggle"
|
|
35
|
+
data-i18n-title="admin.toggle_dark"
|
|
36
|
+
class="p-2 rounded-lg text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
37
|
+
>
|
|
38
|
+
<span id="dark-icon" class="text-lg leading-none">☾</span>
|
|
39
|
+
</button>
|
|
40
|
+
<span class="text-gray-300 dark:text-gray-700">|</span>
|
|
41
|
+
<a
|
|
42
|
+
href="/"
|
|
43
|
+
class="text-blue-600 dark:text-blue-400 hover:underline text-sm"
|
|
44
|
+
>
|
|
45
|
+
<span data-i18n="admin.back_to_docs">Back to docs →</span>
|
|
46
|
+
</a>
|
|
47
|
+
</div>
|
|
48
|
+
</header>
|
|
49
|
+
|
|
50
|
+
<!-- ── Page ── -->
|
|
51
|
+
<main class="max-w-6xl mx-auto px-6 py-10">
|
|
52
|
+
<!-- ── Two-column layout ── -->
|
|
53
|
+
<form id="config-form" novalidate>
|
|
54
|
+
<!-- Title + Save on same row -->
|
|
55
|
+
<div class="flex items-center justify-between mb-8">
|
|
56
|
+
<div>
|
|
57
|
+
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-50" data-i18n="admin.config.title">
|
|
58
|
+
Configuration
|
|
59
|
+
</h2>
|
|
60
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400" data-i18n="admin.config.description">
|
|
61
|
+
Settings are saved to .living-doc.json in your docs folder.
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="flex items-center gap-4 shrink-0">
|
|
65
|
+
<div id="save-msg" class="text-sm"></div>
|
|
66
|
+
<button
|
|
67
|
+
type="submit"
|
|
68
|
+
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"
|
|
69
|
+
>
|
|
70
|
+
<span data-i18n="admin.config.save_btn">Save changes</span>
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="grid grid-cols-2 gap-8 items-start">
|
|
76
|
+
<!-- ── Left column ── -->
|
|
77
|
+
<div class="space-y-6">
|
|
78
|
+
<!-- Server Info -->
|
|
79
|
+
<div
|
|
80
|
+
class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5"
|
|
81
|
+
>
|
|
82
|
+
<h3
|
|
83
|
+
class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-4"
|
|
84
|
+
>
|
|
85
|
+
<span data-i18n="admin.server_info.title">Server Info</span>
|
|
86
|
+
</h3>
|
|
87
|
+
<dl class="grid grid-cols-[1fr_auto] gap-x-6 gap-y-3 text-sm">
|
|
88
|
+
<div>
|
|
89
|
+
<dt
|
|
90
|
+
class="text-xs text-gray-400 uppercase tracking-wide mb-0.5"
|
|
91
|
+
>
|
|
92
|
+
<span data-i18n="admin.server_info.docs_folder">Docs Folder</span>
|
|
93
|
+
</dt>
|
|
94
|
+
<dd
|
|
95
|
+
id="info-folder"
|
|
96
|
+
class="font-mono text-xs text-gray-700 dark:text-gray-300 break-all"
|
|
97
|
+
>
|
|
98
|
+
—
|
|
99
|
+
</dd>
|
|
100
|
+
</div>
|
|
101
|
+
<div>
|
|
102
|
+
<dt
|
|
103
|
+
class="text-xs text-gray-400 uppercase tracking-wide mb-0.5"
|
|
104
|
+
>
|
|
105
|
+
<span data-i18n="admin.server_info.port">Port</span>
|
|
106
|
+
</dt>
|
|
107
|
+
<dd
|
|
108
|
+
id="info-port"
|
|
109
|
+
class="font-mono text-xs text-gray-700 dark:text-gray-300"
|
|
110
|
+
>
|
|
111
|
+
—
|
|
112
|
+
</dd>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="col-span-2 space-y-1.5">
|
|
115
|
+
<label
|
|
116
|
+
class="block text-xs text-gray-400 uppercase tracking-wide mb-0.5"
|
|
117
|
+
for="field-source-root"
|
|
118
|
+
data-i18n="admin.server_info.source_root"
|
|
119
|
+
>Source Root</label
|
|
120
|
+
>
|
|
121
|
+
<input
|
|
122
|
+
id="field-source-root"
|
|
123
|
+
name="sourceRoot"
|
|
124
|
+
type="text"
|
|
125
|
+
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"
|
|
126
|
+
placeholder="/absolute/path"
|
|
127
|
+
/>
|
|
128
|
+
<p
|
|
129
|
+
class="text-xs text-gray-400 dark:text-gray-500"
|
|
130
|
+
data-i18n="admin.server_info.source_root_hint"
|
|
131
|
+
>
|
|
132
|
+
Absolute path used by MCP source tools. Defaults to the parent of Docs Folder when empty.
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
</dl>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<!-- Extra Files -->
|
|
139
|
+
<div
|
|
140
|
+
class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5 space-y-4"
|
|
141
|
+
>
|
|
142
|
+
<div>
|
|
143
|
+
<h3
|
|
144
|
+
class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
|
145
|
+
>
|
|
146
|
+
<span data-i18n="admin.extra_files.title">General — Extra Files</span>
|
|
147
|
+
</h3>
|
|
148
|
+
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400" data-i18n="admin.extra_files.description">
|
|
149
|
+
Add Markdown files from outside the docs folder to the General section.
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<!-- File browser -->
|
|
154
|
+
<div
|
|
155
|
+
class="rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"
|
|
156
|
+
>
|
|
157
|
+
<div
|
|
158
|
+
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"
|
|
159
|
+
>
|
|
160
|
+
<button
|
|
161
|
+
id="browse-up"
|
|
162
|
+
onclick="browseUp()"
|
|
163
|
+
class="text-xs text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-30 disabled:pointer-events-none shrink-0"
|
|
164
|
+
>
|
|
165
|
+
<span data-i18n="common.up">↑ Up</span>
|
|
166
|
+
</button>
|
|
167
|
+
<span
|
|
168
|
+
id="browse-path"
|
|
169
|
+
class="font-mono text-xs text-gray-400 dark:text-gray-500 truncate flex-1 text-right"
|
|
170
|
+
></span>
|
|
171
|
+
</div>
|
|
172
|
+
<div
|
|
173
|
+
id="browse-list"
|
|
174
|
+
class="divide-y divide-gray-100 dark:divide-gray-800 max-h-52 overflow-y-auto"
|
|
175
|
+
></div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<!-- Added files -->
|
|
179
|
+
<div>
|
|
180
|
+
<p
|
|
181
|
+
class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2"
|
|
182
|
+
>
|
|
183
|
+
Added files
|
|
184
|
+
</p>
|
|
185
|
+
<div id="extra-files-list" class="space-y-1"></div>
|
|
186
|
+
<p id="extra-files-empty" class="text-xs text-gray-400 italic">
|
|
187
|
+
<span data-i18n="admin.extra_files.empty">No extra files added yet.</span>
|
|
188
|
+
</p>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<!-- ── Right column ── -->
|
|
194
|
+
<div class="space-y-6">
|
|
195
|
+
<!-- Appearance & Metadata -->
|
|
196
|
+
<div
|
|
197
|
+
class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 divide-y divide-gray-100 dark:divide-gray-800"
|
|
198
|
+
>
|
|
199
|
+
<div class="px-5 py-4">
|
|
200
|
+
<h3
|
|
201
|
+
class="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
|
202
|
+
>
|
|
203
|
+
Appearance & Metadata
|
|
204
|
+
</h3>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="px-5 py-4 space-y-1.5">
|
|
207
|
+
<label
|
|
208
|
+
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
209
|
+
for="field-title"
|
|
210
|
+
data-i18n="admin.appearance.site_title_label">Site Title</label
|
|
211
|
+
>
|
|
212
|
+
<input
|
|
213
|
+
id="field-title"
|
|
214
|
+
name="title"
|
|
215
|
+
type="text"
|
|
216
|
+
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"
|
|
217
|
+
placeholder="Living Documentation"
|
|
218
|
+
/>
|
|
219
|
+
<p class="text-xs text-gray-400 dark:text-gray-500" data-i18n="admin.appearance.site_title_hint">
|
|
220
|
+
Displayed in the browser tab and sidebar header.
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="px-5 py-4 space-y-1.5">
|
|
224
|
+
<label
|
|
225
|
+
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
226
|
+
for="field-theme"
|
|
227
|
+
data-i18n="admin.appearance.theme_label">Default Theme</label
|
|
228
|
+
>
|
|
229
|
+
<select
|
|
230
|
+
id="field-theme"
|
|
231
|
+
name="theme"
|
|
232
|
+
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"
|
|
233
|
+
>
|
|
234
|
+
<option value="system" data-i18n="admin.appearance.theme_system">System (follow OS preference)</option>
|
|
235
|
+
<option value="light" data-i18n="admin.appearance.theme_light">Light</option>
|
|
236
|
+
<option value="dark" data-i18n="admin.appearance.theme_dark">Dark</option>
|
|
237
|
+
</select>
|
|
238
|
+
<p class="text-xs text-gray-400 dark:text-gray-500" data-i18n="admin.appearance.theme_hint">
|
|
239
|
+
Users can always override with the toggle button.
|
|
240
|
+
</p>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="px-5 py-4 space-y-1.5">
|
|
243
|
+
<label
|
|
244
|
+
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
245
|
+
for="field-language"
|
|
246
|
+
data-i18n="admin.appearance.language_label">Language</label>
|
|
247
|
+
<select
|
|
248
|
+
id="field-language"
|
|
249
|
+
name="language"
|
|
250
|
+
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"
|
|
251
|
+
>
|
|
252
|
+
<option value="en" data-i18n="admin.appearance.language_en">English</option>
|
|
253
|
+
<option value="fr" data-i18n="admin.appearance.language_fr">Français</option>
|
|
254
|
+
</select>
|
|
255
|
+
<p class="text-xs text-gray-400 dark:text-gray-500" data-i18n="admin.appearance.language_hint">
|
|
256
|
+
Interface language for all pages.
|
|
257
|
+
</p>
|
|
258
|
+
</div>
|
|
259
|
+
<div class="px-5 py-4">
|
|
260
|
+
<label class="flex items-center gap-3 cursor-pointer">
|
|
261
|
+
<input
|
|
262
|
+
id="field-debug"
|
|
263
|
+
name="showDiagramDebug"
|
|
264
|
+
type="checkbox"
|
|
265
|
+
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
|
266
|
+
/>
|
|
267
|
+
<span
|
|
268
|
+
class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
269
|
+
data-i18n="admin.appearance.debug_label">Show debug button in diagram editor</span
|
|
270
|
+
>
|
|
271
|
+
</label>
|
|
272
|
+
<p class="mt-1 text-xs text-gray-400 dark:text-gray-500 ml-7">
|
|
273
|
+
Displays a Debug button that overlays raw position and size
|
|
274
|
+
info on each node — useful for diagnosing snap-to-grid issues.
|
|
275
|
+
</p>
|
|
276
|
+
</div>
|
|
277
|
+
<div class="px-5 py-4 space-y-1.5">
|
|
278
|
+
<label
|
|
279
|
+
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
280
|
+
for="field-pattern"
|
|
281
|
+
data-i18n="admin.pattern.label">Filename Pattern</label
|
|
282
|
+
>
|
|
283
|
+
<input
|
|
284
|
+
id="field-pattern"
|
|
285
|
+
name="filenamePattern"
|
|
286
|
+
type="text"
|
|
287
|
+
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"
|
|
288
|
+
placeholder="YYYY_MM_DD_HH_mm_[Category]_title"
|
|
289
|
+
/>
|
|
290
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">
|
|
291
|
+
Parsed for date, category, and title.
|
|
292
|
+
<span
|
|
293
|
+
id="pattern-desc-example"
|
|
294
|
+
class="font-mono bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 px-1 rounded"
|
|
295
|
+
>YYYY_MM_DD_HH_mm_[Category]_title.md</span
|
|
296
|
+
>
|
|
297
|
+
</p>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<!-- Pattern Preview -->
|
|
302
|
+
<div
|
|
303
|
+
id="pattern-preview"
|
|
304
|
+
class="rounded-xl border border-blue-100 dark:border-blue-900 bg-blue-50 dark:bg-blue-950/30 p-5"
|
|
305
|
+
>
|
|
306
|
+
<h3
|
|
307
|
+
class="text-sm font-semibold text-blue-700 dark:text-blue-400 mb-3"
|
|
308
|
+
>
|
|
309
|
+
Pattern Preview
|
|
310
|
+
</h3>
|
|
311
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
|
312
|
+
How your pattern parses example filenames:
|
|
313
|
+
</p>
|
|
314
|
+
<div id="preview-rows" class="space-y-2 text-sm"></div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<!-- ── Diagram Palettes (full width) ── -->
|
|
320
|
+
<div
|
|
321
|
+
class="mt-8 rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 divide-y divide-gray-100 dark:divide-gray-800"
|
|
322
|
+
>
|
|
323
|
+
<div class="px-5 py-4">
|
|
324
|
+
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
|
325
|
+
Diagram Color Palettes
|
|
326
|
+
</h3>
|
|
327
|
+
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
328
|
+
Customize the colors available in the diagram editor. Changes take
|
|
329
|
+
effect immediately.
|
|
330
|
+
</p>
|
|
331
|
+
</div>
|
|
332
|
+
<div
|
|
333
|
+
class="grid grid-cols-2 divide-x divide-gray-100 dark:divide-gray-800"
|
|
334
|
+
>
|
|
335
|
+
<!-- Node palette -->
|
|
336
|
+
<div class="px-5 py-4 space-y-3">
|
|
337
|
+
<div class="flex items-center justify-between">
|
|
338
|
+
<h4
|
|
339
|
+
class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide"
|
|
340
|
+
>
|
|
341
|
+
Shape colors
|
|
342
|
+
</h4>
|
|
343
|
+
<button
|
|
344
|
+
type="button"
|
|
345
|
+
onclick="resetNodePalette()"
|
|
346
|
+
class="text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
|
347
|
+
>
|
|
348
|
+
Reset
|
|
349
|
+
</button>
|
|
350
|
+
</div>
|
|
351
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">
|
|
352
|
+
Click a color to show or hide it in the node panel. Dimmed =
|
|
353
|
+
hidden.
|
|
354
|
+
</p>
|
|
355
|
+
<div id="nodeSwatches" class="flex flex-wrap gap-2"></div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<!-- Edge palette -->
|
|
359
|
+
<div class="px-5 py-4 space-y-3">
|
|
360
|
+
<div class="flex items-center justify-between">
|
|
361
|
+
<h4
|
|
362
|
+
class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide"
|
|
363
|
+
>
|
|
364
|
+
Arrow colors
|
|
365
|
+
</h4>
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
onclick="resetEdgePalette()"
|
|
369
|
+
class="text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
|
370
|
+
>
|
|
371
|
+
Reset
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
<p class="text-xs text-gray-400 dark:text-gray-500">
|
|
375
|
+
Add or remove colors available for arrows. Click ✕ to remove.
|
|
376
|
+
</p>
|
|
377
|
+
<div
|
|
378
|
+
id="edgeSwatches"
|
|
379
|
+
class="flex flex-wrap gap-3 items-end"
|
|
380
|
+
></div>
|
|
381
|
+
<div class="flex items-center gap-2 pt-1">
|
|
382
|
+
<input
|
|
383
|
+
type="color"
|
|
384
|
+
id="newEdgeColor"
|
|
385
|
+
value="#3b82f6"
|
|
386
|
+
class="w-8 h-8 rounded cursor-pointer border border-gray-300 dark:border-gray-600 bg-transparent"
|
|
387
|
+
/>
|
|
388
|
+
<button
|
|
389
|
+
type="button"
|
|
390
|
+
onclick="addEdgeColor()"
|
|
391
|
+
class="px-3 py-1.5 text-xs rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-medium transition-colors"
|
|
392
|
+
>
|
|
393
|
+
Add color
|
|
394
|
+
</button>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
</form>
|
|
400
|
+
|
|
401
|
+
<!-- ── AGPL-3.0 License ── -->
|
|
402
|
+
<div class="mt-12 rounded-2xl overflow-hidden border border-blue-100 dark:border-blue-900/40 shadow-sm">
|
|
403
|
+
<!-- Gradient banner -->
|
|
404
|
+
<div class="bg-gradient-to-r from-blue-600 via-blue-500 to-indigo-500 px-6 py-5 flex items-center gap-4">
|
|
405
|
+
<div class="text-white text-3xl select-none" aria-hidden="true">📖</div>
|
|
406
|
+
<div>
|
|
407
|
+
<h3 class="text-white font-bold text-base tracking-tight" data-i18n="admin.license.title">
|
|
408
|
+
AGPL-3.0 License — Open Source
|
|
409
|
+
</h3>
|
|
410
|
+
<p class="text-blue-100 text-xs mt-0.5" data-i18n="admin.license.subtitle">
|
|
411
|
+
Free to use, fork, and build upon — derivatives and network-hosted services must remain under AGPL (copyleft + SaaS clause).
|
|
412
|
+
</p>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<!-- Body -->
|
|
417
|
+
<div class="bg-white dark:bg-gray-900 px-6 py-5 space-y-4">
|
|
418
|
+
<p class="text-sm text-gray-700 dark:text-gray-300 leading-relaxed" data-i18n-html="admin.license.body">
|
|
419
|
+
This open source project is gracefully provided to you by its conceptors
|
|
420
|
+
<strong>Youssef MEDAGHRI-ALAOUI</strong> & <strong>Claude Code</strong>.
|
|
421
|
+
</p>
|
|
422
|
+
|
|
423
|
+
<div class="flex flex-wrap gap-3 pt-1">
|
|
424
|
+
<a
|
|
425
|
+
href="https://www.linkedin.com/in/youssef-medaghri-alaoui-93b2922/"
|
|
426
|
+
target="_blank"
|
|
427
|
+
rel="noopener noreferrer"
|
|
428
|
+
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"
|
|
429
|
+
>
|
|
430
|
+
<svg class="w-4 h-4 shrink-0" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
431
|
+
<path 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"/>
|
|
432
|
+
</svg>
|
|
433
|
+
<span data-i18n="admin.license.linkedin_btn">Youssef on LinkedIn</span>
|
|
434
|
+
</a>
|
|
435
|
+
|
|
436
|
+
<a
|
|
437
|
+
href="https://www.npmjs.com/package/living-documentation"
|
|
438
|
+
target="_blank"
|
|
439
|
+
rel="noopener noreferrer"
|
|
440
|
+
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"
|
|
441
|
+
>
|
|
442
|
+
<svg class="w-4 h-4 shrink-0" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
443
|
+
<path d="M0 0v24h24V0H0Zm19.2 19.2H13.6V8h-3.2v11.2H4.8V4.8h14.4v14.4Z"/>
|
|
444
|
+
</svg>
|
|
445
|
+
<span data-i18n="admin.license.npm_btn">living-documentation on npm</span>
|
|
446
|
+
</a>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
</main>
|
|
451
|
+
|
|
452
|
+
<script>
|
|
453
|
+
// ── Boot ───────────────────────────────────────────────────
|
|
454
|
+
document.addEventListener("DOMContentLoaded", async () => {
|
|
455
|
+
applyDarkMode(loadDarkPref());
|
|
456
|
+
setupDarkToggle();
|
|
457
|
+
await loadConfig();
|
|
458
|
+
setupPatternPreview();
|
|
459
|
+
document
|
|
460
|
+
.getElementById("config-form")
|
|
461
|
+
.addEventListener("submit", saveConfig);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// ── Dark mode ──────────────────────────────────────────────
|
|
465
|
+
function loadDarkPref() {
|
|
466
|
+
const saved = localStorage.getItem("ld-dark");
|
|
467
|
+
if (saved !== null) return saved === "true";
|
|
468
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
469
|
+
}
|
|
470
|
+
function applyDarkMode(dark) {
|
|
471
|
+
document.documentElement.classList.toggle("dark", dark);
|
|
472
|
+
document.getElementById("dark-icon").textContent = dark ? "☀" : "☾";
|
|
473
|
+
}
|
|
474
|
+
function setupDarkToggle() {
|
|
475
|
+
document.getElementById("dark-toggle").addEventListener("click", () => {
|
|
476
|
+
const isDark = document.documentElement.classList.toggle("dark");
|
|
477
|
+
localStorage.setItem("ld-dark", isDark);
|
|
478
|
+
document.getElementById("dark-icon").textContent = isDark ? "☀" : "☾";
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ── Config ─────────────────────────────────────────────────
|
|
483
|
+
async function loadConfig() {
|
|
484
|
+
try {
|
|
485
|
+
const cfg = await fetch("/api/config").then((r) => r.json());
|
|
486
|
+
await window.initI18n(cfg.language || 'en');
|
|
487
|
+
window.applyI18n();
|
|
488
|
+
document.getElementById("info-folder").textContent =
|
|
489
|
+
cfg.docsFolder || "—";
|
|
490
|
+
document.getElementById("info-port").textContent = cfg.port || "—";
|
|
491
|
+
document.getElementById("field-source-root").value =
|
|
492
|
+
cfg.sourceRoot || "";
|
|
493
|
+
document.getElementById("field-title").value = cfg.title || "";
|
|
494
|
+
document.getElementById("field-theme").value = cfg.theme || "system";
|
|
495
|
+
document.getElementById("field-language").value = cfg.language || "en";
|
|
496
|
+
document.getElementById("field-pattern").value =
|
|
497
|
+
cfg.filenamePattern || "";
|
|
498
|
+
document.getElementById("field-debug").checked =
|
|
499
|
+
!!cfg.showDiagramDebug;
|
|
500
|
+
updatePreview(cfg.filenamePattern);
|
|
501
|
+
initExtraFiles(cfg);
|
|
502
|
+
initPalettes(cfg);
|
|
503
|
+
} catch {
|
|
504
|
+
showMsg(window.t('admin.msg.load_failed'), "error");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function saveConfig(e) {
|
|
509
|
+
e.preventDefault();
|
|
510
|
+
const filenamePattern = document
|
|
511
|
+
.getElementById("field-pattern")
|
|
512
|
+
.value.trim();
|
|
513
|
+
if (filenamePattern) {
|
|
514
|
+
const catCount = (filenamePattern.match(/\[Category\]/gi) || [])
|
|
515
|
+
.length;
|
|
516
|
+
if (catCount === 0) {
|
|
517
|
+
showMsg(window.t('admin.msg.pattern_no_category'), "error");
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (catCount > 1) {
|
|
521
|
+
showMsg(window.t('admin.msg.pattern_one_category'), "error");
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const sourceRootRaw = document
|
|
526
|
+
.getElementById("field-source-root")
|
|
527
|
+
.value.trim();
|
|
528
|
+
const payload = {
|
|
529
|
+
title: document.getElementById("field-title").value.trim(),
|
|
530
|
+
theme: document.getElementById("field-theme").value,
|
|
531
|
+
language: document.getElementById("field-language").value,
|
|
532
|
+
filenamePattern,
|
|
533
|
+
showDiagramDebug: document.getElementById("field-debug").checked,
|
|
534
|
+
diagramNodePalette: [...nodePalette],
|
|
535
|
+
diagramEdgePalette: [...edgePalette],
|
|
536
|
+
sourceRoot: sourceRootRaw === "" ? null : sourceRootRaw,
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
const res = await fetch("/api/config", {
|
|
541
|
+
method: "PUT",
|
|
542
|
+
headers: { "Content-Type": "application/json" },
|
|
543
|
+
body: JSON.stringify(payload),
|
|
544
|
+
});
|
|
545
|
+
if (!res.ok) throw new Error(await res.text());
|
|
546
|
+
showMsg(window.t('admin.msg.saved'), "ok");
|
|
547
|
+
} catch (err) {
|
|
548
|
+
showMsg(window.t('admin.msg.save_failed') + err.message, "error");
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function showMsg(text, type) {
|
|
553
|
+
const el = document.getElementById("save-msg");
|
|
554
|
+
el.textContent = text;
|
|
555
|
+
el.className =
|
|
556
|
+
"text-sm " +
|
|
557
|
+
(type === "ok"
|
|
558
|
+
? "text-green-600 dark:text-green-400"
|
|
559
|
+
: "text-red-600 dark:text-red-400");
|
|
560
|
+
if (type === "ok")
|
|
561
|
+
setTimeout(() => {
|
|
562
|
+
el.textContent = "";
|
|
563
|
+
}, 3000);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ── Pattern preview ────────────────────────────────────────
|
|
567
|
+
const EXAMPLES = [
|
|
568
|
+
"2024_01_15_09_30_[DevOps]_deploy_pipeline.md",
|
|
569
|
+
"2023_11_03_14_45_[Frontend]_react_hooks_guide.md",
|
|
570
|
+
"2025_06_20_08_00_meeting_notes.md",
|
|
571
|
+
"readme.md",
|
|
572
|
+
];
|
|
573
|
+
|
|
574
|
+
function dateStrToISO(s) {
|
|
575
|
+
const p = s.split("_");
|
|
576
|
+
return p.length === 5
|
|
577
|
+
? p[0] + "-" + p[1] + "-" + p[2] + "T" + p[3] + ":" + p[4]
|
|
578
|
+
: p[0] + "-" + p[1] + "-" + p[2];
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function buildPatternsFromFormat(patternStr) {
|
|
582
|
+
if (!patternStr) patternStr = "YYYY_MM_DD_HH_mm_[Category]_title";
|
|
583
|
+
const hasDate = /YYYY.*MM.*DD/.test(patternStr);
|
|
584
|
+
const hasTime = hasDate && /HH.*mm/.test(patternStr);
|
|
585
|
+
const hasCategory = /\[Category\]/i.test(patternStr);
|
|
586
|
+
const dateGroup = hasTime
|
|
587
|
+
? "(\\d{4}_\\d{2}_\\d{2}(?:_\\d{2}_\\d{2})?)"
|
|
588
|
+
: "(\\d{4}_\\d{2}_\\d{2})";
|
|
589
|
+
const catGroup = "\\[([^\\]]+)\\]";
|
|
590
|
+
const catBeforeDate =
|
|
591
|
+
hasDate &&
|
|
592
|
+
hasCategory &&
|
|
593
|
+
patternStr.search(/\[Category\]/i) < patternStr.search(/YYYY/i);
|
|
594
|
+
let full = null,
|
|
595
|
+
dateOnly = null;
|
|
596
|
+
if (hasDate && hasCategory) {
|
|
597
|
+
const ordered = catBeforeDate
|
|
598
|
+
? catGroup + "_" + dateGroup
|
|
599
|
+
: dateGroup + "_" + catGroup;
|
|
600
|
+
full = new RegExp("^" + ordered + "_(.+)\\.md$", "i");
|
|
601
|
+
dateOnly = new RegExp("^" + dateGroup + "_(.+)\\.md$", "i");
|
|
602
|
+
} else if (hasDate) {
|
|
603
|
+
dateOnly = new RegExp("^" + dateGroup + "_(.+)\\.md$", "i");
|
|
604
|
+
} else if (hasCategory) {
|
|
605
|
+
full = new RegExp("^" + catGroup + "_(.+)\\.md$", "i");
|
|
606
|
+
}
|
|
607
|
+
return { full, dateOnly, hasDate, hasCategory, catBeforeDate };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function parsePreview(filename, patterns) {
|
|
611
|
+
if (patterns.full) {
|
|
612
|
+
const m = filename.match(patterns.full);
|
|
613
|
+
if (m) {
|
|
614
|
+
if (patterns.hasDate && patterns.hasCategory) {
|
|
615
|
+
const dateStr = patterns.catBeforeDate ? m[2] : m[1];
|
|
616
|
+
const category = patterns.catBeforeDate ? m[1] : m[2];
|
|
617
|
+
return {
|
|
618
|
+
date: dateStrToISO(dateStr),
|
|
619
|
+
category,
|
|
620
|
+
title: titleCase(m[3]),
|
|
621
|
+
match: true,
|
|
622
|
+
};
|
|
623
|
+
} else if (patterns.hasCategory) {
|
|
624
|
+
return {
|
|
625
|
+
date: null,
|
|
626
|
+
category: m[1],
|
|
627
|
+
title: titleCase(m[2]),
|
|
628
|
+
match: true,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (patterns.dateOnly) {
|
|
634
|
+
const m = filename.match(patterns.dateOnly);
|
|
635
|
+
if (m) {
|
|
636
|
+
return {
|
|
637
|
+
date: dateStrToISO(m[1]),
|
|
638
|
+
category: "General",
|
|
639
|
+
title: titleCase(m[2]),
|
|
640
|
+
match: true,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return {
|
|
645
|
+
date: null,
|
|
646
|
+
category: "General",
|
|
647
|
+
title: filename.replace(".md", ""),
|
|
648
|
+
match: false,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function titleCase(s) {
|
|
653
|
+
return s
|
|
654
|
+
.split("_")
|
|
655
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
656
|
+
.join(" ");
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function updatePreview() {
|
|
660
|
+
const patternVal = document
|
|
661
|
+
.getElementById("field-pattern")
|
|
662
|
+
.value.trim();
|
|
663
|
+
const descSpan = document.getElementById("pattern-desc-example");
|
|
664
|
+
descSpan.textContent =
|
|
665
|
+
(patternVal || "YYYY_MM_DD_HH_mm_[Category]_title") + ".md";
|
|
666
|
+
const patterns = buildPatternsFromFormat(patternVal);
|
|
667
|
+
const rows = document.getElementById("preview-rows");
|
|
668
|
+
rows.innerHTML = EXAMPLES.map((f) => {
|
|
669
|
+
const p = parsePreview(f, patterns);
|
|
670
|
+
return `
|
|
671
|
+
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-3">
|
|
672
|
+
<p class="font-mono text-xs text-gray-500 dark:text-gray-400 mb-2 break-all">${esc(f)}</p>
|
|
673
|
+
<div class="grid grid-cols-3 gap-2 text-xs">
|
|
674
|
+
<div>
|
|
675
|
+
<span class="text-gray-400 block mb-0.5">Date</span>
|
|
676
|
+
<span class="${p.date ? "text-green-600 dark:text-green-400" : "text-gray-400"}">${p.date || "none"}</span>
|
|
677
|
+
</div>
|
|
678
|
+
<div>
|
|
679
|
+
<span class="text-gray-400 block mb-0.5">Category</span>
|
|
680
|
+
<span class="text-blue-600 dark:text-blue-400">${esc(p.category)}</span>
|
|
681
|
+
</div>
|
|
682
|
+
<div>
|
|
683
|
+
<span class="text-gray-400 block mb-0.5">Title</span>
|
|
684
|
+
<span class="text-gray-700 dark:text-gray-300 truncate block">${esc(p.title)}</span>
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
</div>`;
|
|
688
|
+
}).join("");
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function setupPatternPreview() {
|
|
692
|
+
document
|
|
693
|
+
.getElementById("field-pattern")
|
|
694
|
+
.addEventListener("input", updatePreview);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function esc(s) {
|
|
698
|
+
return String(s)
|
|
699
|
+
.replace(/&/g, "&")
|
|
700
|
+
.replace(/</g, "<")
|
|
701
|
+
.replace(/>/g, ">")
|
|
702
|
+
.replace(/"/g, """)
|
|
703
|
+
.replace(/'/g, "'");
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ── Extra Files / Browser ──────────────────────────────────
|
|
707
|
+
let extraFiles = [];
|
|
708
|
+
let browseParent = null;
|
|
709
|
+
let browseCurrent = "";
|
|
710
|
+
let docsFolder = "";
|
|
711
|
+
|
|
712
|
+
function initExtraFiles(cfg) {
|
|
713
|
+
extraFiles = cfg.extraFiles || [];
|
|
714
|
+
docsFolder = cfg.docsFolder || "";
|
|
715
|
+
renderExtraFiles();
|
|
716
|
+
loadBrowse(cfg.docsFolder || "/");
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function relativeDisplay(filePath) {
|
|
720
|
+
if (!docsFolder) return filePath;
|
|
721
|
+
const fromParts = docsFolder.split("/").filter(Boolean);
|
|
722
|
+
const toParts = filePath.split("/").filter(Boolean);
|
|
723
|
+
let common = 0;
|
|
724
|
+
while (
|
|
725
|
+
common < fromParts.length &&
|
|
726
|
+
common < toParts.length &&
|
|
727
|
+
fromParts[common] === toParts[common]
|
|
728
|
+
)
|
|
729
|
+
common++;
|
|
730
|
+
const ups = fromParts.length - common;
|
|
731
|
+
const downs = toParts.slice(common).join("/");
|
|
732
|
+
const rel = "../".repeat(ups) + downs;
|
|
733
|
+
return "$DOCS_FOLDER/" + rel;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function loadBrowse(dirPath) {
|
|
737
|
+
const list = document.getElementById("browse-list");
|
|
738
|
+
list.innerHTML =
|
|
739
|
+
'<p class="px-3 py-4 text-xs text-gray-400 text-center">Loading…</p>';
|
|
740
|
+
try {
|
|
741
|
+
const data = await fetch(
|
|
742
|
+
"/api/browse?path=" + encodeURIComponent(dirPath),
|
|
743
|
+
).then((r) => r.json());
|
|
744
|
+
browseCurrent = data.current;
|
|
745
|
+
browseParent = data.parent;
|
|
746
|
+
document.getElementById("browse-path").textContent = relativeDisplay(
|
|
747
|
+
data.current,
|
|
748
|
+
);
|
|
749
|
+
document.getElementById("browse-up").disabled = !data.parent;
|
|
750
|
+
|
|
751
|
+
const rows = [];
|
|
752
|
+
|
|
753
|
+
for (const dir of data.dirs) {
|
|
754
|
+
rows.push(`
|
|
755
|
+
<button data-path="${esc(dir.path)}" onclick="loadBrowse(this.dataset.path)"
|
|
756
|
+
class="w-full flex items-center gap-2 px-3 py-2 text-sm text-left
|
|
757
|
+
hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
758
|
+
<span class="text-gray-400 shrink-0">📁</span>
|
|
759
|
+
<span class="text-gray-700 dark:text-gray-300 truncate">${esc(dir.name)}</span>
|
|
760
|
+
</button>`);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
for (const file of data.files) {
|
|
764
|
+
const added = extraFiles.includes(file.path);
|
|
765
|
+
rows.push(`
|
|
766
|
+
<div class="flex items-center justify-between gap-2 px-3 py-2 text-sm
|
|
767
|
+
hover:bg-gray-50 dark:hover:bg-gray-800">
|
|
768
|
+
<span class="flex items-center gap-2 text-gray-700 dark:text-gray-300 min-w-0">
|
|
769
|
+
<span class="text-gray-400 shrink-0">📄</span>
|
|
770
|
+
<span class="truncate">${esc(file.name)}</span>
|
|
771
|
+
</span>
|
|
772
|
+
${
|
|
773
|
+
added
|
|
774
|
+
? '<span class="text-xs text-green-600 dark:text-green-400 shrink-0">Added</span>'
|
|
775
|
+
: `<button data-path="${esc(file.path)}" onclick="addExtraFile(this.dataset.path)"
|
|
776
|
+
class="text-xs text-blue-600 dark:text-blue-400 hover:underline shrink-0">+ Add</button>`
|
|
777
|
+
}
|
|
778
|
+
</div>`);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (rows.length === 0) {
|
|
782
|
+
list.innerHTML =
|
|
783
|
+
'<p class="px-3 py-4 text-xs text-gray-400 text-center">Empty directory</p>';
|
|
784
|
+
} else {
|
|
785
|
+
list.innerHTML = rows.join("");
|
|
786
|
+
}
|
|
787
|
+
} catch {
|
|
788
|
+
list.innerHTML =
|
|
789
|
+
'<p class="px-3 py-4 text-xs text-red-400 text-center">Cannot read directory</p>';
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function browseUp() {
|
|
794
|
+
if (browseParent) loadBrowse(browseParent);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
async function addExtraFile(filePath) {
|
|
798
|
+
if (extraFiles.includes(filePath)) return;
|
|
799
|
+
extraFiles = [...extraFiles, filePath];
|
|
800
|
+
await saveExtraFiles();
|
|
801
|
+
renderExtraFiles();
|
|
802
|
+
loadBrowse(browseCurrent);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async function removeExtraFile(filePath) {
|
|
806
|
+
extraFiles = extraFiles.filter((f) => f !== filePath);
|
|
807
|
+
await saveExtraFiles();
|
|
808
|
+
renderExtraFiles();
|
|
809
|
+
loadBrowse(browseCurrent);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async function saveExtraFiles() {
|
|
813
|
+
try {
|
|
814
|
+
const res = await fetch("/api/config", {
|
|
815
|
+
method: "PUT",
|
|
816
|
+
headers: { "Content-Type": "application/json" },
|
|
817
|
+
body: JSON.stringify({ extraFiles }),
|
|
818
|
+
});
|
|
819
|
+
if (!res.ok) throw new Error(await res.text());
|
|
820
|
+
} catch (err) {
|
|
821
|
+
showMsg("Failed to save: " + err.message, "error");
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function moveExtraFile(index, direction) {
|
|
826
|
+
const target = index + direction;
|
|
827
|
+
if (target < 0 || target >= extraFiles.length) return;
|
|
828
|
+
const next = [...extraFiles];
|
|
829
|
+
[next[index], next[target]] = [next[target], next[index]];
|
|
830
|
+
extraFiles = next;
|
|
831
|
+
saveExtraFiles();
|
|
832
|
+
renderExtraFiles();
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// ── Diagram Palettes ───────────────────────────────────────
|
|
836
|
+
// NODE_COLORS_ADMIN mirrors constants.js NODE_COLORS (default bg values).
|
|
837
|
+
// Mirrors NODE_COLORS from constants.js (bg + original border).
|
|
838
|
+
const NODE_COLORS_ADMIN = {
|
|
839
|
+
"c-white": { bg: "#ffffff", border: "#dbd5d1", label: "White" },
|
|
840
|
+
"c-gray": { bg: "#f5f5f4", border: "#a8a29e", label: "Gris" },
|
|
841
|
+
"c-slate": { bg: "#f1f5f9", border: "#64748b", label: "Ardoise" },
|
|
842
|
+
"c-blue": { bg: "#dbeafe", border: "#3b82f6", label: "Bleu" },
|
|
843
|
+
"c-sky": { bg: "#e0f2fe", border: "#0ea5e9", label: "Ciel" },
|
|
844
|
+
"c-cyan": { bg: "#cffafe", border: "#06b6d4", label: "Cyan" },
|
|
845
|
+
"c-teal": { bg: "#ccfbf1", border: "#14b8a6", label: "Teal" },
|
|
846
|
+
"c-green": { bg: "#dcfce7", border: "#22c55e", label: "Vert" },
|
|
847
|
+
"c-lime": { bg: "#ecfccb", border: "#84cc16", label: "Lime" },
|
|
848
|
+
"c-amber": { bg: "#fef9c3", border: "#f59e0b", label: "Ambre" },
|
|
849
|
+
"c-orange": { bg: "#ffedd5", border: "#f97316", label: "Orange" },
|
|
850
|
+
"c-red": { bg: "#fee2e2", border: "#ef4444", label: "Rouge" },
|
|
851
|
+
"c-rose": { bg: "#ffe4e6", border: "#f43f5e", label: "Rose" },
|
|
852
|
+
"c-pink": { bg: "#fce7f3", border: "#ec4899", label: "Fuschia" },
|
|
853
|
+
"c-purple": { bg: "#ede9fe", border: "#8b5cf6", label: "Violet" },
|
|
854
|
+
};
|
|
855
|
+
const NODE_PALETTE_KEYS = Object.keys(NODE_COLORS_ADMIN); // fixed order of 15 slots
|
|
856
|
+
const DEFAULT_EDGE_PALETTE_ADMIN = [
|
|
857
|
+
"#ffffff",
|
|
858
|
+
"#a8a29e",
|
|
859
|
+
"#374151",
|
|
860
|
+
"#3b82f6",
|
|
861
|
+
"#14b8a6",
|
|
862
|
+
"#22c55e",
|
|
863
|
+
"#f97316",
|
|
864
|
+
"#ef4444",
|
|
865
|
+
"#a855f7",
|
|
866
|
+
];
|
|
867
|
+
|
|
868
|
+
// nodePalette: array of 15 bg hex strings, one per slot (positional).
|
|
869
|
+
let nodePalette = NODE_PALETTE_KEYS.map((k) => NODE_COLORS_ADMIN[k].bg);
|
|
870
|
+
let edgePalette = [...DEFAULT_EDGE_PALETTE_ADMIN];
|
|
871
|
+
|
|
872
|
+
// Per-slot lightness ratio (border_L / bg_L) — mirrors NODE_L_RATIOS from constants.js.
|
|
873
|
+
const NODE_L_RATIOS_ADMIN = {
|
|
874
|
+
"c-gray": 0.667,
|
|
875
|
+
"c-slate": 0.488,
|
|
876
|
+
"c-blue": 0.645,
|
|
877
|
+
"c-sky": 0.518,
|
|
878
|
+
"c-cyan": 0.473,
|
|
879
|
+
"c-teal": 0.448,
|
|
880
|
+
"c-green": 0.489,
|
|
881
|
+
"c-lime": 0.496,
|
|
882
|
+
"c-amber": 0.57,
|
|
883
|
+
"c-orange": 0.578,
|
|
884
|
+
"c-red": 0.64,
|
|
885
|
+
"c-rose": 0.636,
|
|
886
|
+
"c-pink": 0.638,
|
|
887
|
+
"c-purple": 0.694,
|
|
888
|
+
"c-indigo": 0.71,
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
// Mirrors deriveNodeColors from constants.js (no ES module in admin.html).
|
|
892
|
+
function deriveNodeColors(bgHex, lRatio = 0.6) {
|
|
893
|
+
const r = parseInt(bgHex.slice(1, 3), 16) / 255;
|
|
894
|
+
const g = parseInt(bgHex.slice(3, 5), 16) / 255;
|
|
895
|
+
const b = parseInt(bgHex.slice(5, 7), 16) / 255;
|
|
896
|
+
const max = Math.max(r, g, b),
|
|
897
|
+
min = Math.min(r, g, b);
|
|
898
|
+
const l = (max + min) / 2;
|
|
899
|
+
const d = max - min;
|
|
900
|
+
let h = 0,
|
|
901
|
+
s = 0;
|
|
902
|
+
if (d > 0) {
|
|
903
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
904
|
+
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
905
|
+
else if (max === g) h = ((b - r) / d + 2) / 6;
|
|
906
|
+
else h = ((r - g) / d + 4) / 6;
|
|
907
|
+
}
|
|
908
|
+
function hslToHex(hh, ss, ll) {
|
|
909
|
+
let rr, gg, bb;
|
|
910
|
+
if (ss === 0) {
|
|
911
|
+
rr = gg = bb = ll;
|
|
912
|
+
} else {
|
|
913
|
+
const hue2rgb = (p, q, t) => {
|
|
914
|
+
if (t < 0) t += 1;
|
|
915
|
+
if (t > 1) t -= 1;
|
|
916
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
917
|
+
if (t < 1 / 2) return q;
|
|
918
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
919
|
+
return p;
|
|
920
|
+
};
|
|
921
|
+
const q = ll < 0.5 ? ll * (1 + ss) : ll + ss - ll * ss;
|
|
922
|
+
const p = 2 * ll - q;
|
|
923
|
+
rr = hue2rgb(p, q, hh + 1 / 3);
|
|
924
|
+
gg = hue2rgb(p, q, hh);
|
|
925
|
+
bb = hue2rgb(p, q, hh - 1 / 3);
|
|
926
|
+
}
|
|
927
|
+
return (
|
|
928
|
+
"#" +
|
|
929
|
+
[rr, gg, bb]
|
|
930
|
+
.map((v) =>
|
|
931
|
+
Math.max(0, Math.min(255, Math.round(v * 255)))
|
|
932
|
+
.toString(16)
|
|
933
|
+
.padStart(2, "0"),
|
|
934
|
+
)
|
|
935
|
+
.join("")
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
const border = hslToHex(h, s, l * lRatio);
|
|
939
|
+
return { bg: bgHex, border };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function initPalettes(cfg) {
|
|
943
|
+
// nodePalette: positional array of 15 bg hex strings
|
|
944
|
+
if (
|
|
945
|
+
Array.isArray(cfg.diagramNodePalette) &&
|
|
946
|
+
cfg.diagramNodePalette.length
|
|
947
|
+
) {
|
|
948
|
+
nodePalette = NODE_PALETTE_KEYS.map(
|
|
949
|
+
(k, i) => cfg.diagramNodePalette[i] || NODE_COLORS_ADMIN[k].bg,
|
|
950
|
+
);
|
|
951
|
+
} else {
|
|
952
|
+
nodePalette = NODE_PALETTE_KEYS.map((k) => NODE_COLORS_ADMIN[k].bg);
|
|
953
|
+
}
|
|
954
|
+
edgePalette =
|
|
955
|
+
Array.isArray(cfg.diagramEdgePalette) && cfg.diagramEdgePalette.length
|
|
956
|
+
? [...cfg.diagramEdgePalette]
|
|
957
|
+
: [...DEFAULT_EDGE_PALETTE_ADMIN];
|
|
958
|
+
renderNodePalette();
|
|
959
|
+
renderEdgePalette();
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function swatchBorder(key, bg) {
|
|
963
|
+
// Use original hand-crafted border if bg is unchanged; derive for custom colours.
|
|
964
|
+
const def = NODE_COLORS_ADMIN[key];
|
|
965
|
+
return bg === def.bg
|
|
966
|
+
? def.border
|
|
967
|
+
: deriveNodeColors(bg, NODE_L_RATIOS_ADMIN[key] || 0.6).border;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function renderNodePalette() {
|
|
971
|
+
const container = document.getElementById("nodeSwatches");
|
|
972
|
+
container.innerHTML = NODE_PALETTE_KEYS.map((key, i) => {
|
|
973
|
+
const bg = nodePalette[i] || NODE_COLORS_ADMIN[key].bg;
|
|
974
|
+
const border = swatchBorder(key, bg);
|
|
975
|
+
return `<div style="position:relative;width:32px;height:32px" title="${esc(NODE_COLORS_ADMIN[key].label)}">
|
|
976
|
+
<div style="width:32px;height:32px;border-radius:50%;background:${bg};border:3px solid ${border};pointer-events:none"></div>
|
|
977
|
+
<input type="color" value="${bg}" data-i="${i}"
|
|
978
|
+
oninput="previewNodeColor(${i},this.value)"
|
|
979
|
+
onchange="commitNodeColor(${i},this.value)"
|
|
980
|
+
style="position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;cursor:pointer;border:none;padding:0" />
|
|
981
|
+
</div>`;
|
|
982
|
+
}).join("");
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function previewNodeColor(i, hex) {
|
|
986
|
+
nodePalette[i] = hex.toLowerCase();
|
|
987
|
+
const key = NODE_PALETTE_KEYS[i];
|
|
988
|
+
const border = swatchBorder(key, hex.toLowerCase());
|
|
989
|
+
const wrapper = document.getElementById("nodeSwatches").children[i];
|
|
990
|
+
if (wrapper) {
|
|
991
|
+
const swatch = wrapper.querySelector("div");
|
|
992
|
+
if (swatch) {
|
|
993
|
+
swatch.style.background = hex;
|
|
994
|
+
swatch.style.borderColor = border;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function commitNodeColor(i, hex) {
|
|
1000
|
+
previewNodeColor(i, hex);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function resetNodePalette() {
|
|
1004
|
+
nodePalette = NODE_PALETTE_KEYS.map((k) => NODE_COLORS_ADMIN[k].bg);
|
|
1005
|
+
renderNodePalette();
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function renderEdgePalette() {
|
|
1009
|
+
const container = document.getElementById("edgeSwatches");
|
|
1010
|
+
container.innerHTML = edgePalette
|
|
1011
|
+
.map(
|
|
1012
|
+
(hex, i) => `
|
|
1013
|
+
<div style="display:flex;flex-direction:column;align-items:center;gap:3px">
|
|
1014
|
+
<div style="width:26px;height:26px;border-radius:50%;background:${hex};border:2px solid rgba(0,0,0,0.15)"></div>
|
|
1015
|
+
<button type="button" data-i="${i}" onclick="removeEdgeColor(+this.dataset.i)"
|
|
1016
|
+
style="font-size:10px;color:#ef4444;cursor:pointer;line-height:1;background:none;border:none"
|
|
1017
|
+
title="Supprimer">✕</button>
|
|
1018
|
+
</div>`,
|
|
1019
|
+
)
|
|
1020
|
+
.join("");
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function addEdgeColor() {
|
|
1024
|
+
const hex = document.getElementById("newEdgeColor").value.toLowerCase();
|
|
1025
|
+
if (edgePalette.includes(hex)) return;
|
|
1026
|
+
edgePalette = [...edgePalette, hex];
|
|
1027
|
+
renderEdgePalette();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function removeEdgeColor(index) {
|
|
1031
|
+
if (edgePalette.length <= 1) return;
|
|
1032
|
+
edgePalette = edgePalette.filter((_, i) => i !== index);
|
|
1033
|
+
renderEdgePalette();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function resetEdgePalette() {
|
|
1037
|
+
edgePalette = [...DEFAULT_EDGE_PALETTE_ADMIN];
|
|
1038
|
+
renderEdgePalette();
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function renderExtraFiles() {
|
|
1042
|
+
const list = document.getElementById("extra-files-list");
|
|
1043
|
+
const empty = document.getElementById("extra-files-empty");
|
|
1044
|
+
if (extraFiles.length === 0) {
|
|
1045
|
+
list.innerHTML = "";
|
|
1046
|
+
empty.style.display = "";
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
empty.style.display = "none";
|
|
1050
|
+
list.innerHTML = extraFiles
|
|
1051
|
+
.map(
|
|
1052
|
+
(f, i) => `
|
|
1053
|
+
<div class="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 dark:bg-gray-800">
|
|
1054
|
+
<div class="flex flex-col shrink-0">
|
|
1055
|
+
<button data-i="${i}" data-d="-1" onclick="moveExtraFile(+this.dataset.i, +this.dataset.d)"
|
|
1056
|
+
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 leading-none disabled:opacity-20"
|
|
1057
|
+
${i === 0 ? "disabled" : ""}>↑</button>
|
|
1058
|
+
<button data-i="${i}" data-d="1" onclick="moveExtraFile(+this.dataset.i, +this.dataset.d)"
|
|
1059
|
+
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 leading-none disabled:opacity-20"
|
|
1060
|
+
${i === extraFiles.length - 1 ? "disabled" : ""}>↓</button>
|
|
1061
|
+
</div>
|
|
1062
|
+
<div class="overflow-x-auto flex-1 min-w-0">
|
|
1063
|
+
<span class="font-mono text-xs text-gray-600 dark:text-gray-400 whitespace-nowrap" title="${esc(f)}">${esc(relativeDisplay(f))}</span>
|
|
1064
|
+
</div>
|
|
1065
|
+
<button data-path="${esc(f)}" onclick="removeExtraFile(this.dataset.path)"
|
|
1066
|
+
class="text-xs text-red-500 hover:text-red-700 dark:hover:text-red-400 shrink-0">Remove</button>
|
|
1067
|
+
</div>`,
|
|
1068
|
+
)
|
|
1069
|
+
.join("");
|
|
1070
|
+
}
|
|
1071
|
+
</script>
|
|
1072
|
+
</body>
|
|
1073
|
+
</html>
|