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.
Files changed (203) hide show
  1. package/LICENSE +661 -0
  2. package/README.fr.md +344 -0
  3. package/README.md +344 -0
  4. package/dist/bin/cli.d.ts +3 -0
  5. package/dist/bin/cli.d.ts.map +1 -0
  6. package/dist/bin/cli.js +262 -0
  7. package/dist/bin/cli.js.map +1 -0
  8. package/dist/src/frontend/accuracy-gauge.js +70 -0
  9. package/dist/src/frontend/admin.html +1532 -0
  10. package/dist/src/frontend/annotations.js +585 -0
  11. package/dist/src/frontend/boot.js +101 -0
  12. package/dist/src/frontend/config.js +29 -0
  13. package/dist/src/frontend/confirm-modal.js +82 -0
  14. package/dist/src/frontend/context.html +1252 -0
  15. package/dist/src/frontend/dark-mode.js +20 -0
  16. package/dist/src/frontend/diagram/alignment.js +161 -0
  17. package/dist/src/frontend/diagram/clipboard.js +187 -0
  18. package/dist/src/frontend/diagram/constants.js +109 -0
  19. package/dist/src/frontend/diagram/custom-shapes.js +104 -0
  20. package/dist/src/frontend/diagram/debug.js +43 -0
  21. package/dist/src/frontend/diagram/drawio-export.js +649 -0
  22. package/dist/src/frontend/diagram/edge-panel.js +293 -0
  23. package/dist/src/frontend/diagram/edge-rendering.js +12 -0
  24. package/dist/src/frontend/diagram/evidence.js +146 -0
  25. package/dist/src/frontend/diagram/grid.js +78 -0
  26. package/dist/src/frontend/diagram/groups.js +102 -0
  27. package/dist/src/frontend/diagram/history.js +157 -0
  28. package/dist/src/frontend/diagram/image-name-modal.js +48 -0
  29. package/dist/src/frontend/diagram/image-upload.js +36 -0
  30. package/dist/src/frontend/diagram/label-editor.js +115 -0
  31. package/dist/src/frontend/diagram/link-panel.js +144 -0
  32. package/dist/src/frontend/diagram/main.js +364 -0
  33. package/dist/src/frontend/diagram/network.js +2214 -0
  34. package/dist/src/frontend/diagram/node-panel.js +389 -0
  35. package/dist/src/frontend/diagram/node-rendering.js +964 -0
  36. package/dist/src/frontend/diagram/persistence.js +168 -0
  37. package/dist/src/frontend/diagram/ports.js +421 -0
  38. package/dist/src/frontend/diagram/selection-overlay.js +387 -0
  39. package/dist/src/frontend/diagram/state.js +43 -0
  40. package/dist/src/frontend/diagram/t.js +3 -0
  41. package/dist/src/frontend/diagram/toast.js +21 -0
  42. package/dist/src/frontend/diagram/unlock-hold.js +206 -0
  43. package/dist/src/frontend/diagram/zoom.js +20 -0
  44. package/dist/src/frontend/diagram-link-modal.js +137 -0
  45. package/dist/src/frontend/diagram.html +1494 -0
  46. package/dist/src/frontend/documents.js +479 -0
  47. package/dist/src/frontend/export.js +338 -0
  48. package/dist/src/frontend/file-attach.js +178 -0
  49. package/dist/src/frontend/files-modal.js +243 -0
  50. package/dist/src/frontend/i18n/en.json +624 -0
  51. package/dist/src/frontend/i18n/fr.json +624 -0
  52. package/dist/src/frontend/i18n.js +32 -0
  53. package/dist/src/frontend/image-paste.js +126 -0
  54. package/dist/src/frontend/index.html +2806 -0
  55. package/dist/src/frontend/local-search.js +476 -0
  56. package/dist/src/frontend/metadata.js +318 -0
  57. package/dist/src/frontend/misc.js +92 -0
  58. package/dist/src/frontend/new-doc-modal.js +285 -0
  59. package/dist/src/frontend/new-folder-modal.js +169 -0
  60. package/dist/src/frontend/search.js +194 -0
  61. package/dist/src/frontend/shape-editor.html +685 -0
  62. package/dist/src/frontend/sidebar-helpers.js +96 -0
  63. package/dist/src/frontend/sidebar-resize.js +98 -0
  64. package/dist/src/frontend/sidebar.js +351 -0
  65. package/dist/src/frontend/snippet-detect.js +25 -0
  66. package/dist/src/frontend/snippet-table.js +85 -0
  67. package/dist/src/frontend/snippet-tree.js +94 -0
  68. package/dist/src/frontend/snippets.js +1146 -0
  69. package/dist/src/frontend/state.js +46 -0
  70. package/dist/src/frontend/utils.js +21 -0
  71. package/dist/src/frontend/validate.js +107 -0
  72. package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
  73. package/dist/src/frontend/wordcloud.js +693 -0
  74. package/dist/src/lib/config.d.ts +26 -0
  75. package/dist/src/lib/config.d.ts.map +1 -0
  76. package/dist/src/lib/config.js +195 -0
  77. package/dist/src/lib/config.js.map +1 -0
  78. package/dist/src/lib/hash.d.ts +2 -0
  79. package/dist/src/lib/hash.d.ts.map +1 -0
  80. package/dist/src/lib/hash.js +18 -0
  81. package/dist/src/lib/hash.js.map +1 -0
  82. package/dist/src/lib/metadata.d.ts +31 -0
  83. package/dist/src/lib/metadata.d.ts.map +1 -0
  84. package/dist/src/lib/metadata.js +128 -0
  85. package/dist/src/lib/metadata.js.map +1 -0
  86. package/dist/src/lib/parser.d.ts +11 -0
  87. package/dist/src/lib/parser.d.ts.map +1 -0
  88. package/dist/src/lib/parser.js +111 -0
  89. package/dist/src/lib/parser.js.map +1 -0
  90. package/dist/src/lib/status.d.ts +9 -0
  91. package/dist/src/lib/status.d.ts.map +1 -0
  92. package/dist/src/lib/status.js +72 -0
  93. package/dist/src/lib/status.js.map +1 -0
  94. package/dist/src/mcp/server.d.ts +3 -0
  95. package/dist/src/mcp/server.d.ts.map +1 -0
  96. package/dist/src/mcp/server.js +2046 -0
  97. package/dist/src/mcp/server.js.map +1 -0
  98. package/dist/src/mcp/tools/diagrams.d.ts +82 -0
  99. package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
  100. package/dist/src/mcp/tools/diagrams.js +594 -0
  101. package/dist/src/mcp/tools/diagrams.js.map +1 -0
  102. package/dist/src/mcp/tools/documents.d.ts +44 -0
  103. package/dist/src/mcp/tools/documents.d.ts.map +1 -0
  104. package/dist/src/mcp/tools/documents.js +186 -0
  105. package/dist/src/mcp/tools/documents.js.map +1 -0
  106. package/dist/src/mcp/tools/git.d.ts +10 -0
  107. package/dist/src/mcp/tools/git.d.ts.map +1 -0
  108. package/dist/src/mcp/tools/git.js +217 -0
  109. package/dist/src/mcp/tools/git.js.map +1 -0
  110. package/dist/src/mcp/tools/metadata.d.ts +57 -0
  111. package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
  112. package/dist/src/mcp/tools/metadata.js +222 -0
  113. package/dist/src/mcp/tools/metadata.js.map +1 -0
  114. package/dist/src/mcp/tools/source.d.ts +29 -0
  115. package/dist/src/mcp/tools/source.d.ts.map +1 -0
  116. package/dist/src/mcp/tools/source.js +196 -0
  117. package/dist/src/mcp/tools/source.js.map +1 -0
  118. package/dist/src/routes/annotations.d.ts +3 -0
  119. package/dist/src/routes/annotations.d.ts.map +1 -0
  120. package/dist/src/routes/annotations.js +83 -0
  121. package/dist/src/routes/annotations.js.map +1 -0
  122. package/dist/src/routes/browse-source.d.ts +3 -0
  123. package/dist/src/routes/browse-source.d.ts.map +1 -0
  124. package/dist/src/routes/browse-source.js +79 -0
  125. package/dist/src/routes/browse-source.js.map +1 -0
  126. package/dist/src/routes/browse.d.ts +3 -0
  127. package/dist/src/routes/browse.d.ts.map +1 -0
  128. package/dist/src/routes/browse.js +91 -0
  129. package/dist/src/routes/browse.js.map +1 -0
  130. package/dist/src/routes/config.d.ts +3 -0
  131. package/dist/src/routes/config.d.ts.map +1 -0
  132. package/dist/src/routes/config.js +145 -0
  133. package/dist/src/routes/config.js.map +1 -0
  134. package/dist/src/routes/context.d.ts +3 -0
  135. package/dist/src/routes/context.d.ts.map +1 -0
  136. package/dist/src/routes/context.js +287 -0
  137. package/dist/src/routes/context.js.map +1 -0
  138. package/dist/src/routes/diagrams.d.ts +3 -0
  139. package/dist/src/routes/diagrams.d.ts.map +1 -0
  140. package/dist/src/routes/diagrams.js +69 -0
  141. package/dist/src/routes/diagrams.js.map +1 -0
  142. package/dist/src/routes/documents.d.ts +11 -0
  143. package/dist/src/routes/documents.d.ts.map +1 -0
  144. package/dist/src/routes/documents.js +450 -0
  145. package/dist/src/routes/documents.js.map +1 -0
  146. package/dist/src/routes/export.d.ts +3 -0
  147. package/dist/src/routes/export.d.ts.map +1 -0
  148. package/dist/src/routes/export.js +280 -0
  149. package/dist/src/routes/export.js.map +1 -0
  150. package/dist/src/routes/files.d.ts +3 -0
  151. package/dist/src/routes/files.d.ts.map +1 -0
  152. package/dist/src/routes/files.js +180 -0
  153. package/dist/src/routes/files.js.map +1 -0
  154. package/dist/src/routes/images.d.ts +3 -0
  155. package/dist/src/routes/images.d.ts.map +1 -0
  156. package/dist/src/routes/images.js +49 -0
  157. package/dist/src/routes/images.js.map +1 -0
  158. package/dist/src/routes/metadata.d.ts +3 -0
  159. package/dist/src/routes/metadata.d.ts.map +1 -0
  160. package/dist/src/routes/metadata.js +131 -0
  161. package/dist/src/routes/metadata.js.map +1 -0
  162. package/dist/src/routes/shape-libraries.d.ts +3 -0
  163. package/dist/src/routes/shape-libraries.d.ts.map +1 -0
  164. package/dist/src/routes/shape-libraries.js +118 -0
  165. package/dist/src/routes/shape-libraries.js.map +1 -0
  166. package/dist/src/routes/wordcloud.d.ts +3 -0
  167. package/dist/src/routes/wordcloud.d.ts.map +1 -0
  168. package/dist/src/routes/wordcloud.js +95 -0
  169. package/dist/src/routes/wordcloud.js.map +1 -0
  170. package/dist/src/server.d.ts +7 -0
  171. package/dist/src/server.d.ts.map +1 -0
  172. package/dist/src/server.js +93 -0
  173. package/dist/src/server.js.map +1 -0
  174. package/dist/starter-doc/.living-doc.json +52 -0
  175. package/dist/starter-doc/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
  176. package/dist/starter-doc/AI/2026_01_01_how_to.md +112 -0
  177. package/dist/starter-doc/AI/PROJECT-INSTRUCTIONS.md +172 -0
  178. package/dist/starter-doc/AI/PROJECT-STACK.md +77 -0
  179. package/dist/starter-doc/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
  180. package/dist/starter-doc/AI/default/AGENTS.md +31 -0
  181. package/dist/starter-doc/AI/default/CLAUDE.md +31 -0
  182. package/dist/starter-doc/AI/default/MEMORY.md +24 -0
  183. package/dist/starter-doc/AI/rules/no-magic-numbers.md +18 -0
  184. package/dist/starter-doc/AI/rules/track-current-work.md +23 -0
  185. package/dist/starter-doc/WORKLOG/current-task.md +57 -0
  186. package/dist/starter-doc-fr/.living-doc.json +52 -0
  187. package/dist/starter-doc-fr/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
  188. package/dist/starter-doc-fr/AI/2026_01_01_how_to.md +100 -0
  189. package/dist/starter-doc-fr/AI/PROJECT-INSTRUCTIONS.md +172 -0
  190. package/dist/starter-doc-fr/AI/PROJECT-STACK.md +77 -0
  191. package/dist/starter-doc-fr/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
  192. package/dist/starter-doc-fr/AI/default/AGENTS.md +31 -0
  193. package/dist/starter-doc-fr/AI/default/CLAUDE.md +31 -0
  194. package/dist/starter-doc-fr/AI/default/MEMORY.md +24 -0
  195. package/dist/starter-doc-fr/AI/rules/no-magic-numbers.md +18 -0
  196. package/dist/starter-doc-fr/AI/rules/track-current-work.md +23 -0
  197. package/dist/starter-doc-fr/WORKLOG/current-task.md +57 -0
  198. package/images/living_documentation.jpg +0 -0
  199. package/images/readme-extra-files.png +0 -0
  200. package/images/readme-filename-pattern.png +0 -0
  201. package/images/readme-intelligent-search-demo.jpg +0 -0
  202. package/images/readme-sidebar.png +0 -0
  203. package/package.json +72 -0
@@ -0,0 +1,685 @@
1
+ <!doctype html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Shape editor — Living Documentation</title>
7
+ <script src="https://cdn.tailwindcss.com?plugins=typography"></script>
8
+ <script src="/i18n.js"></script>
9
+ <script>
10
+ tailwind.config = { darkMode: "class", theme: { extend: {} } };
11
+ </script>
12
+ <style>
13
+ .btn {
14
+ display: inline-flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+ height: 2rem;
18
+ padding: 0 0.75rem;
19
+ border-radius: 0.375rem;
20
+ font-size: 0.8125rem;
21
+ font-weight: 600;
22
+ border: 1px solid #d1d5db;
23
+ background: #fff;
24
+ color: #374151;
25
+ }
26
+ .btn:hover {
27
+ background: #f3f4f6;
28
+ }
29
+ .btn-primary {
30
+ border-color: #2563eb;
31
+ background: #2563eb;
32
+ color: #fff;
33
+ }
34
+ .btn-primary:hover {
35
+ background: #1d4ed8;
36
+ }
37
+ .btn-danger {
38
+ border-color: #fecaca;
39
+ color: #b91c1c;
40
+ }
41
+ .btn-danger:hover {
42
+ background: #fef2f2;
43
+ }
44
+ .field {
45
+ height: 2rem;
46
+ border: 1px solid #d1d5db;
47
+ border-radius: 0.375rem;
48
+ padding: 0 0.5rem;
49
+ font-size: 0.875rem;
50
+ background: #fff;
51
+ }
52
+ .shape-item.active {
53
+ background: #eff6ff;
54
+ color: #1d4ed8;
55
+ }
56
+ #previewStage {
57
+ position: relative;
58
+ width: min(34rem, 100%);
59
+ aspect-ratio: 1 / 1;
60
+ border: 1px solid #d1d5db;
61
+ border-radius: 0.5rem;
62
+ background:
63
+ linear-gradient(45deg, #f9fafb 25%, transparent 25%),
64
+ linear-gradient(-45deg, #f9fafb 25%, transparent 25%),
65
+ linear-gradient(45deg, transparent 75%, #f9fafb 75%),
66
+ linear-gradient(-45deg, transparent 75%, #f9fafb 75%);
67
+ background-size: 1.25rem 1.25rem;
68
+ background-position:
69
+ 0 0,
70
+ 0 0.625rem,
71
+ 0.625rem -0.625rem,
72
+ -0.625rem 0;
73
+ overflow: hidden;
74
+ }
75
+ #previewImage {
76
+ position: absolute;
77
+ inset: 0;
78
+ width: 100%;
79
+ height: 100%;
80
+ object-fit: fill;
81
+ pointer-events: none;
82
+ }
83
+ .anchor-dot {
84
+ position: absolute;
85
+ width: 0.9rem;
86
+ height: 0.9rem;
87
+ transform: translate(-50%, -50%);
88
+ border-radius: 999px;
89
+ border: 2px solid #fff;
90
+ background: #f97316;
91
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
92
+ cursor: grab;
93
+ }
94
+ .anchor-dot:active {
95
+ cursor: grabbing;
96
+ }
97
+ .anchor-dot span {
98
+ position: absolute;
99
+ left: 50%;
100
+ top: calc(100% + 0.2rem);
101
+ transform: translateX(-50%);
102
+ font:
103
+ 10px/1 system-ui,
104
+ sans-serif;
105
+ color: #374151;
106
+ background: rgba(255, 255, 255, 0.9);
107
+ padding: 1px 3px;
108
+ border-radius: 3px;
109
+ white-space: nowrap;
110
+ }
111
+ </style>
112
+ </head>
113
+ <body class="h-screen overflow-hidden bg-gray-50 text-gray-900">
114
+ <header
115
+ class="h-12 border-b border-gray-200 bg-white flex items-center gap-2 px-3"
116
+ >
117
+ <button class="btn" onclick="history.back()">← Back</button>
118
+ <h1 class="text-sm font-semibold">Shape editor</h1>
119
+ <span id="saveState" class="ml-auto text-xs text-gray-500"></span>
120
+ </header>
121
+
122
+ <main
123
+ class="h-[calc(100vh-3rem)] grid grid-cols-[17rem_1fr] overflow-hidden"
124
+ >
125
+ <aside class="border-r border-gray-200 bg-white flex flex-col min-h-0">
126
+ <div class="p-3 border-b border-gray-200 space-y-2">
127
+ <label class="block text-xs font-semibold text-gray-500"
128
+ >Library</label
129
+ >
130
+ <div class="flex gap-2">
131
+ <select id="librarySelect" class="field flex-1"></select>
132
+ <button id="btnAddLibrary" class="btn">+</button>
133
+ </div>
134
+ <input
135
+ id="libraryName"
136
+ class="field w-full"
137
+ placeholder="Library name"
138
+ />
139
+ </div>
140
+ <div class="p-3 border-b border-gray-200 flex gap-2">
141
+ <button id="btnNewShape" class="btn btn-primary flex-1">
142
+ New shape
143
+ </button>
144
+ <button id="btnDeleteShape" class="btn btn-danger">Delete</button>
145
+ </div>
146
+ <div id="shapeList" class="flex-1 overflow-y-auto p-2 space-y-1"></div>
147
+ </aside>
148
+
149
+ <section class="min-w-0 min-h-0 overflow-y-auto p-5">
150
+ <div
151
+ class="grid grid-cols-[minmax(20rem,34rem)_minmax(20rem,1fr)] gap-5 items-start"
152
+ >
153
+ <div class="space-y-3">
154
+ <div id="previewStage">
155
+ <img id="previewImage" alt="" />
156
+ </div>
157
+ <p class="text-xs text-gray-500">
158
+ Click on the image to add an anchor. Drag anchors to refine their
159
+ position.
160
+ </p>
161
+ </div>
162
+
163
+ <div class="space-y-4">
164
+ <div class="grid grid-cols-2 gap-3">
165
+ <label class="space-y-1">
166
+ <span class="block text-xs font-semibold text-gray-500"
167
+ >Shape name</span
168
+ >
169
+ <input
170
+ id="shapeName"
171
+ class="field w-full"
172
+ placeholder="Shape name"
173
+ />
174
+ </label>
175
+ <label class="space-y-1">
176
+ <span class="block text-xs font-semibold text-gray-500"
177
+ >Image / SVG</span
178
+ >
179
+ <input
180
+ id="imageFile"
181
+ type="file"
182
+ accept="image/*,.svg"
183
+ class="block w-full text-sm"
184
+ />
185
+ </label>
186
+ <label class="space-y-1">
187
+ <span class="block text-xs font-semibold text-gray-500"
188
+ >Width</span
189
+ >
190
+ <input
191
+ id="shapeWidth"
192
+ type="number"
193
+ min="16"
194
+ max="1200"
195
+ class="field w-full"
196
+ />
197
+ </label>
198
+ <label class="space-y-1">
199
+ <span class="block text-xs font-semibold text-gray-500"
200
+ >Height</span
201
+ >
202
+ <input
203
+ id="shapeHeight"
204
+ type="number"
205
+ min="16"
206
+ max="1200"
207
+ class="field w-full"
208
+ />
209
+ </label>
210
+ <label class="space-y-1 col-span-2">
211
+ <span class="block text-xs font-semibold text-gray-500"
212
+ >Text placement</span
213
+ >
214
+ <select id="labelPlacement" class="field w-full">
215
+ <option
216
+ value="below"
217
+ data-i18n="shape_editor.label_placement.below"
218
+ >
219
+ Below the shape
220
+ </option>
221
+ <option
222
+ value="above"
223
+ data-i18n="shape_editor.label_placement.above"
224
+ >
225
+ Above the shape
226
+ </option>
227
+ <option
228
+ value="right"
229
+ data-i18n="shape_editor.label_placement.right"
230
+ >
231
+ To the right
232
+ </option>
233
+ <option
234
+ value="left"
235
+ data-i18n="shape_editor.label_placement.left"
236
+ >
237
+ To the left
238
+ </option>
239
+ <option
240
+ value="center"
241
+ data-i18n="shape_editor.label_placement.center"
242
+ >
243
+ Centered in the shape
244
+ </option>
245
+ </select>
246
+ </label>
247
+ <label
248
+ class="col-span-2 flex items-start gap-2 rounded-md border border-gray-200 bg-white px-2 py-2 text-sm"
249
+ >
250
+ <input
251
+ id="shapeShowInDiagram"
252
+ type="checkbox"
253
+ class="mt-0.5 h-4 w-4 rounded border-gray-300 text-blue-600"
254
+ />
255
+ <span class="space-y-0.5">
256
+ <span
257
+ class="block font-semibold text-gray-700"
258
+ data-i18n="shape_editor.show_in_diagram_label"
259
+ >Show in diagram palette</span
260
+ >
261
+ <span
262
+ class="block text-xs text-gray-500"
263
+ data-i18n="shape_editor.show_in_diagram_hint"
264
+ >When disabled, existing diagram nodes still render but this
265
+ shape is hidden from the bottom palette.</span
266
+ >
267
+ </span>
268
+ </label>
269
+ </div>
270
+
271
+ <div class="flex flex-wrap gap-2">
272
+ <button id="btnDefaultAnchors" class="btn">
273
+ Use 8 default anchors
274
+ </button>
275
+ <button id="btnClearAnchors" class="btn">Clear anchors</button>
276
+ <button id="btnSaveShape" class="btn btn-primary">
277
+ Save shape
278
+ </button>
279
+ </div>
280
+
281
+ <div>
282
+ <div class="flex items-center justify-between mb-2">
283
+ <h2
284
+ class="text-xs font-semibold uppercase tracking-wide text-gray-500"
285
+ >
286
+ Anchors
287
+ </h2>
288
+ <span id="anchorCount" class="text-xs text-gray-500"></span>
289
+ </div>
290
+ <div id="anchorList" class="space-y-1"></div>
291
+ </div>
292
+ </div>
293
+ </div>
294
+ </section>
295
+ </main>
296
+
297
+ <script type="module">
298
+ import {
299
+ DEFAULT_CUSTOM_ANCHORS,
300
+ CUSTOM_SHAPE_DEFAULT_SIZE,
301
+ } from "/diagram/custom-shapes.js";
302
+
303
+ const els = {
304
+ saveState: document.getElementById("saveState"),
305
+ librarySelect: document.getElementById("librarySelect"),
306
+ libraryName: document.getElementById("libraryName"),
307
+ shapeList: document.getElementById("shapeList"),
308
+ previewStage: document.getElementById("previewStage"),
309
+ previewImage: document.getElementById("previewImage"),
310
+ shapeName: document.getElementById("shapeName"),
311
+ imageFile: document.getElementById("imageFile"),
312
+ shapeWidth: document.getElementById("shapeWidth"),
313
+ shapeHeight: document.getElementById("shapeHeight"),
314
+ labelPlacement: document.getElementById("labelPlacement"),
315
+ shapeShowInDiagram: document.getElementById("shapeShowInDiagram"),
316
+ anchorList: document.getElementById("anchorList"),
317
+ anchorCount: document.getElementById("anchorCount"),
318
+ };
319
+
320
+ let store = { libraries: [] };
321
+ let activeLibraryId = null;
322
+ let activeShapeId = null;
323
+ let draft = null;
324
+ let draggingAnchorId = null;
325
+
326
+ const uid = (prefix) =>
327
+ `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
328
+ const activeLibrary = () =>
329
+ store.libraries.find((lib) => lib.id === activeLibraryId) || null;
330
+ const activeShape = () =>
331
+ activeLibrary()?.shapes.find((shape) => shape.id === activeShapeId) ||
332
+ null;
333
+ const clamp01 = (n) => Math.max(0, Math.min(1, n));
334
+ const normalizeLabelPlacement = (value) =>
335
+ ["below", "above", "right", "left", "center"].includes(value)
336
+ ? value
337
+ : "below";
338
+ const escapeAttr = (value) =>
339
+ String(value ?? "").replace(
340
+ /[&<>"']/g,
341
+ (char) =>
342
+ ({
343
+ "&": "&amp;",
344
+ "<": "&lt;",
345
+ ">": "&gt;",
346
+ '"': "&quot;",
347
+ "'": "&#39;",
348
+ })[char],
349
+ );
350
+
351
+ function setStatus(text) {
352
+ els.saveState.textContent = text;
353
+ if (text)
354
+ setTimeout(() => {
355
+ if (els.saveState.textContent === text)
356
+ els.saveState.textContent = "";
357
+ }, 1800);
358
+ }
359
+
360
+ async function loadStore() {
361
+ const res = await fetch("/api/shape-libraries");
362
+ store = await res.json();
363
+ if (!Array.isArray(store.libraries)) store.libraries = [];
364
+ if (!store.libraries.length) {
365
+ store.libraries.push({
366
+ id: uid("lib"),
367
+ name: "My shapes",
368
+ shapes: [],
369
+ });
370
+ }
371
+ activeLibraryId = store.libraries[0].id;
372
+ activeShapeId = store.libraries[0].shapes[0]?.id || null;
373
+ draft = activeShape()
374
+ ? structuredClone(activeShape())
375
+ : newShapeDraft();
376
+ renderAll();
377
+ }
378
+
379
+ async function saveStore() {
380
+ const res = await fetch("/api/shape-libraries", {
381
+ method: "PUT",
382
+ headers: { "Content-Type": "application/json" },
383
+ body: JSON.stringify(store),
384
+ });
385
+ store = await res.json();
386
+ setStatus("Saved");
387
+ }
388
+
389
+ function newShapeDraft() {
390
+ return {
391
+ id: uid("shape"),
392
+ name: "New shape",
393
+ imageSrc: "",
394
+ width: CUSTOM_SHAPE_DEFAULT_SIZE,
395
+ height: CUSTOM_SHAPE_DEFAULT_SIZE,
396
+ labelPlacement: "below",
397
+ showInDiagram: true,
398
+ anchors: structuredClone(DEFAULT_CUSTOM_ANCHORS),
399
+ };
400
+ }
401
+
402
+ function renderAll() {
403
+ renderLibraries();
404
+ renderShapeList();
405
+ renderEditor();
406
+ }
407
+
408
+ function renderLibraries() {
409
+ els.librarySelect.innerHTML = "";
410
+ store.libraries.forEach((lib) => {
411
+ const opt = document.createElement("option");
412
+ opt.value = lib.id;
413
+ opt.textContent = lib.name;
414
+ opt.selected = lib.id === activeLibraryId;
415
+ els.librarySelect.appendChild(opt);
416
+ });
417
+ els.libraryName.value = activeLibrary()?.name || "";
418
+ }
419
+
420
+ function renderShapeList() {
421
+ const lib = activeLibrary();
422
+ els.shapeList.innerHTML = "";
423
+ (lib?.shapes || []).forEach((shape) => {
424
+ const btn = document.createElement("button");
425
+ btn.type = "button";
426
+ btn.className = `shape-item w-full flex items-center gap-2 px-2 py-1.5 rounded text-sm text-left hover:bg-gray-100 ${shape.id === activeShapeId ? "active" : ""}`;
427
+ const img = document.createElement("img");
428
+ img.src = shape.imageSrc;
429
+ img.alt = "";
430
+ img.className = "w-6 h-6 object-contain shrink-0";
431
+ const label = document.createElement("span");
432
+ label.className = "truncate";
433
+ label.textContent = shape.name;
434
+ btn.append(img, label);
435
+ btn.addEventListener("click", () => {
436
+ activeShapeId = shape.id;
437
+ draft = structuredClone(shape);
438
+ renderAll();
439
+ });
440
+ els.shapeList.appendChild(btn);
441
+ });
442
+ if (!lib?.shapes?.length) {
443
+ els.shapeList.innerHTML =
444
+ '<p class="text-xs text-gray-400 px-2 py-2">No shape yet.</p>';
445
+ }
446
+ }
447
+
448
+ function renderEditor() {
449
+ if (!draft) draft = newShapeDraft();
450
+ els.previewStage.style.aspectRatio = `${Math.max(16, Number(draft.width) || CUSTOM_SHAPE_DEFAULT_SIZE)} / ${Math.max(16, Number(draft.height) || CUSTOM_SHAPE_DEFAULT_SIZE)}`;
451
+ els.previewImage.src = draft.imageSrc || "";
452
+ els.previewImage.style.display = draft.imageSrc ? "block" : "none";
453
+ els.shapeName.value = draft.name || "";
454
+ els.shapeWidth.value = draft.width || CUSTOM_SHAPE_DEFAULT_SIZE;
455
+ els.shapeHeight.value = draft.height || CUSTOM_SHAPE_DEFAULT_SIZE;
456
+ els.labelPlacement.value = normalizeLabelPlacement(
457
+ draft.labelPlacement,
458
+ );
459
+ els.shapeShowInDiagram.checked = draft.showInDiagram !== false;
460
+ renderAnchors();
461
+ }
462
+
463
+ function renderAnchors() {
464
+ els.previewStage
465
+ .querySelectorAll(".anchor-dot")
466
+ .forEach((dot) => dot.remove());
467
+ els.anchorList.innerHTML = "";
468
+ const anchors = draft.anchors || [];
469
+ els.anchorCount.textContent = `${anchors.length} anchor${anchors.length > 1 ? "s" : ""}`;
470
+ anchors.forEach((anchor, index) => {
471
+ const dot = document.createElement("button");
472
+ dot.type = "button";
473
+ dot.className = "anchor-dot";
474
+ dot.style.left = `${anchor.x * 100}%`;
475
+ dot.style.top = `${anchor.y * 100}%`;
476
+ dot.dataset.anchorId = anchor.id;
477
+ dot.innerHTML = `<span>${anchor.id}</span>`;
478
+ dot.addEventListener("pointerdown", (event) => {
479
+ event.preventDefault();
480
+ draggingAnchorId = anchor.id;
481
+ dot.setPointerCapture(event.pointerId);
482
+ });
483
+ els.previewStage.appendChild(dot);
484
+
485
+ const row = document.createElement("div");
486
+ row.className =
487
+ "grid grid-cols-[4.5rem_1fr_1fr_2rem] gap-2 items-center";
488
+ row.innerHTML = `
489
+ <input class="field !h-7 text-xs" value="${escapeAttr(anchor.id)}">
490
+ <input class="field !h-7 text-xs" type="number" min="0" max="100" step="1" value="${Math.round(anchor.x * 100)}">
491
+ <input class="field !h-7 text-xs" type="number" min="0" max="100" step="1" value="${Math.round(anchor.y * 100)}">
492
+ <button class="btn btn-danger !h-7 !px-0">×</button>
493
+ `;
494
+ const [idInput, xInput, yInput] = row.querySelectorAll("input");
495
+ idInput.addEventListener("input", () => {
496
+ anchor.id = idInput.value.trim() || `p${index + 1}`;
497
+ renderAnchors();
498
+ });
499
+ xInput.addEventListener("input", () => {
500
+ anchor.x = clamp01(Number(xInput.value) / 100);
501
+ renderAnchors();
502
+ });
503
+ yInput.addEventListener("input", () => {
504
+ anchor.y = clamp01(Number(yInput.value) / 100);
505
+ renderAnchors();
506
+ });
507
+ row.querySelector("button").addEventListener("click", () => {
508
+ draft.anchors = draft.anchors.filter((item) => item !== anchor);
509
+ renderAnchors();
510
+ });
511
+ els.anchorList.appendChild(row);
512
+ });
513
+ }
514
+
515
+ function setAnchorFromEvent(anchor, event) {
516
+ const rect = els.previewStage.getBoundingClientRect();
517
+ anchor.x = clamp01((event.clientX - rect.left) / rect.width);
518
+ anchor.y = clamp01((event.clientY - rect.top) / rect.height);
519
+ }
520
+
521
+ els.previewStage.addEventListener("pointermove", (event) => {
522
+ if (!draggingAnchorId || !draft) return;
523
+ const anchor = draft.anchors.find(
524
+ (item) => item.id === draggingAnchorId,
525
+ );
526
+ if (!anchor) return;
527
+ setAnchorFromEvent(anchor, event);
528
+ renderAnchors();
529
+ });
530
+ window.addEventListener("pointerup", () => {
531
+ draggingAnchorId = null;
532
+ });
533
+ els.previewStage.addEventListener("click", (event) => {
534
+ if (event.target.closest(".anchor-dot") || draggingAnchorId || !draft)
535
+ return;
536
+ const anchor = {
537
+ id: `p${(draft.anchors || []).length + 1}`,
538
+ x: 0.5,
539
+ y: 0.5,
540
+ };
541
+ setAnchorFromEvent(anchor, event);
542
+ draft.anchors = [...(draft.anchors || []), anchor];
543
+ renderAnchors();
544
+ });
545
+
546
+ document
547
+ .getElementById("btnAddLibrary")
548
+ .addEventListener("click", async () => {
549
+ const lib = { id: uid("lib"), name: "New library", shapes: [] };
550
+ store.libraries.push(lib);
551
+ activeLibraryId = lib.id;
552
+ activeShapeId = null;
553
+ draft = newShapeDraft();
554
+ renderAll();
555
+ await saveStore();
556
+ });
557
+ els.librarySelect.addEventListener("change", () => {
558
+ activeLibraryId = els.librarySelect.value;
559
+ activeShapeId = activeLibrary()?.shapes[0]?.id || null;
560
+ draft = activeShape()
561
+ ? structuredClone(activeShape())
562
+ : newShapeDraft();
563
+ renderAll();
564
+ });
565
+ els.libraryName.addEventListener("change", async () => {
566
+ const lib = activeLibrary();
567
+ if (!lib) return;
568
+ lib.name = els.libraryName.value.trim() || lib.name;
569
+ renderLibraries();
570
+ await saveStore();
571
+ });
572
+ document.getElementById("btnNewShape").addEventListener("click", () => {
573
+ activeShapeId = null;
574
+ draft = newShapeDraft();
575
+ renderAll();
576
+ });
577
+ document
578
+ .getElementById("btnDeleteShape")
579
+ .addEventListener("click", async () => {
580
+ const lib = activeLibrary();
581
+ if (!lib || !activeShapeId) return;
582
+ lib.shapes = lib.shapes.filter((shape) => shape.id !== activeShapeId);
583
+ activeShapeId = lib.shapes[0]?.id || null;
584
+ draft = activeShape()
585
+ ? structuredClone(activeShape())
586
+ : newShapeDraft();
587
+ renderAll();
588
+ await saveStore();
589
+ });
590
+ document
591
+ .getElementById("btnDefaultAnchors")
592
+ .addEventListener("click", () => {
593
+ draft.anchors = structuredClone(DEFAULT_CUSTOM_ANCHORS);
594
+ renderAnchors();
595
+ });
596
+ document
597
+ .getElementById("btnClearAnchors")
598
+ .addEventListener("click", () => {
599
+ draft.anchors = [];
600
+ renderAnchors();
601
+ });
602
+ document
603
+ .getElementById("btnSaveShape")
604
+ .addEventListener("click", async () => {
605
+ const lib = activeLibrary();
606
+ if (!lib || !draft || !draft.imageSrc) return;
607
+ draft.name = els.shapeName.value.trim() || "Untitled shape";
608
+ draft.width = Math.max(
609
+ 16,
610
+ Math.min(1200, Math.round(Number(els.shapeWidth.value) || CUSTOM_SHAPE_DEFAULT_SIZE)),
611
+ );
612
+ draft.height = Math.max(
613
+ 16,
614
+ Math.min(1200, Math.round(Number(els.shapeHeight.value) || CUSTOM_SHAPE_DEFAULT_SIZE)),
615
+ );
616
+ draft.labelPlacement = normalizeLabelPlacement(
617
+ els.labelPlacement.value,
618
+ );
619
+ draft.showInDiagram = els.shapeShowInDiagram.checked;
620
+ const idx = lib.shapes.findIndex((shape) => shape.id === draft.id);
621
+ if (idx >= 0) lib.shapes[idx] = structuredClone(draft);
622
+ else lib.shapes.push(structuredClone(draft));
623
+ activeShapeId = draft.id;
624
+ renderAll();
625
+ await saveStore();
626
+ });
627
+ [
628
+ "shapeName",
629
+ "shapeWidth",
630
+ "shapeHeight",
631
+ "labelPlacement",
632
+ "shapeShowInDiagram",
633
+ ].forEach((id) => {
634
+ document.getElementById(id).addEventListener("input", () => {
635
+ draft.name = els.shapeName.value;
636
+ draft.width = Number(els.shapeWidth.value) || CUSTOM_SHAPE_DEFAULT_SIZE;
637
+ draft.height = Number(els.shapeHeight.value) || CUSTOM_SHAPE_DEFAULT_SIZE;
638
+ draft.labelPlacement = normalizeLabelPlacement(
639
+ els.labelPlacement.value,
640
+ );
641
+ draft.showInDiagram = els.shapeShowInDiagram.checked;
642
+ });
643
+ });
644
+ els.imageFile.addEventListener("change", async () => {
645
+ const file = els.imageFile.files && els.imageFile.files[0];
646
+ if (!file || !draft) return;
647
+ const data = await new Promise((resolve, reject) => {
648
+ const reader = new FileReader();
649
+ reader.onload = () => resolve(reader.result);
650
+ reader.onerror = reject;
651
+ reader.readAsDataURL(file);
652
+ });
653
+ const ext =
654
+ (file.name.split(".").pop() || "png")
655
+ .toLowerCase()
656
+ .replace(/[^a-z0-9]/g, "") || "png";
657
+ const name = file.name.replace(/\.[^.]+$/, "").slice(0, 60);
658
+ const res = await fetch("/api/images/upload", {
659
+ method: "POST",
660
+ headers: { "Content-Type": "application/json" },
661
+ body: JSON.stringify({ data, ext, name }),
662
+ });
663
+ const uploaded = await res.json();
664
+ draft.imageSrc = `/images/${uploaded.filename}`;
665
+ if (!els.shapeName.value || els.shapeName.value === "New shape")
666
+ draft.name = name || draft.name;
667
+ renderEditor();
668
+ });
669
+
670
+ async function initShapeEditorI18n() {
671
+ try {
672
+ const res = await fetch("/api/config");
673
+ const cfg = res.ok ? await res.json() : {};
674
+ await window.initI18n(cfg.language || "en");
675
+ } catch {
676
+ await window.initI18n("en");
677
+ }
678
+ window.applyI18n();
679
+ }
680
+
681
+ await initShapeEditorI18n();
682
+ await loadStore();
683
+ </script>
684
+ </body>
685
+ </html>