living-ai-documentation 2.0.0 → 2.3.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 (31) hide show
  1. package/dist/src/frontend/agents.js +288 -0
  2. package/dist/src/frontend/config.js +3 -2
  3. package/dist/src/frontend/diagram/color-picker.js +72 -0
  4. package/dist/src/frontend/diagram/defaults-modal.js +302 -0
  5. package/dist/src/frontend/diagram/edge-panel.js +110 -11
  6. package/dist/src/frontend/diagram/main.js +5 -8
  7. package/dist/src/frontend/diagram/network.js +16 -7
  8. package/dist/src/frontend/diagram/node-panel.js +104 -2
  9. package/dist/src/frontend/diagram.html +52 -29
  10. package/dist/src/frontend/i18n/en.json +43 -1
  11. package/dist/src/frontend/i18n/fr.json +43 -1
  12. package/dist/src/frontend/index.html +156 -33
  13. package/dist/src/frontend/workspace/app.js +439 -67
  14. package/dist/src/frontend/workspace/app.js.map +1 -1
  15. package/dist/src/frontend/workspace/app.ts +582 -84
  16. package/dist/src/frontend/workspace/index.html +232 -82
  17. package/dist/src/frontend/workspace/persistence.js +61 -0
  18. package/dist/src/frontend/workspace/persistence.js.map +1 -1
  19. package/dist/src/frontend/workspace/persistence.ts +111 -0
  20. package/dist/src/frontend/workspace/styles.css +293 -34
  21. package/dist/src/lib/config.d.ts +23 -0
  22. package/dist/src/lib/config.d.ts.map +1 -1
  23. package/dist/src/lib/config.js +16 -0
  24. package/dist/src/lib/config.js.map +1 -1
  25. package/dist/src/routes/config.d.ts.map +1 -1
  26. package/dist/src/routes/config.js +13 -0
  27. package/dist/src/routes/config.js.map +1 -1
  28. package/dist/src/routes/workspace.d.ts.map +1 -1
  29. package/dist/src/routes/workspace.js +528 -6
  30. package/dist/src/routes/workspace.js.map +1 -1
  31. package/package.json +2 -2
@@ -0,0 +1,288 @@
1
+ // ── Workspace AI agents launcher ────────────────────────────────────────────
2
+ // Depends on i18n.js, documents.js, and the workspace API.
3
+ (function () {
4
+ let workspaceAgents = [];
5
+ let selectedAgentId = null;
6
+ let createdDocumentId = null;
7
+
8
+ function tr(key) {
9
+ return typeof window.t === "function" ? window.t(key) : key;
10
+ }
11
+
12
+ function esc(value) {
13
+ return String(value ?? "")
14
+ .replaceAll("&", "&")
15
+ .replaceAll("<", "&lt;")
16
+ .replaceAll(">", "&gt;")
17
+ .replaceAll('"', "&quot;")
18
+ .replaceAll("'", "&#039;");
19
+ }
20
+
21
+ function agentElements() {
22
+ return {
23
+ button: document.getElementById("ai-agents-btn"),
24
+ modal: document.getElementById("ai-agents-modal"),
25
+ grid: document.getElementById("ai-agents-grid"),
26
+ inputModal: document.getElementById("ai-agent-input-modal"),
27
+ inputTitle: document.getElementById("ai-agent-input-title"),
28
+ inputHint: document.getElementById("ai-agent-input-hint"),
29
+ input: document.getElementById("ai-agent-user-input"),
30
+ runInputButton: document.getElementById("ai-agent-input-run"),
31
+ toast: document.getElementById("ai-agent-toast"),
32
+ toastIcon: document.getElementById("ai-agent-toast-icon"),
33
+ toastTitle: document.getElementById("ai-agent-toast-title"),
34
+ toastMessage: document.getElementById("ai-agent-toast-message"),
35
+ toastOpen: document.getElementById("ai-agent-toast-open"),
36
+ toastClose: document.getElementById("ai-agent-toast-close"),
37
+ };
38
+ }
39
+
40
+ async function loadWorkspaceAgents() {
41
+ const response = await fetch("/api/workspace", {
42
+ headers: { Accept: "application/json" },
43
+ });
44
+ if (!response.ok) {
45
+ throw new Error(`Workspace load failed (${response.status})`);
46
+ }
47
+
48
+ const workspace = await response.json();
49
+ const entities = Array.isArray(workspace.entities) ? workspace.entities : [];
50
+ const byId = new Map(entities.map((entity) => [entity.id, entity]));
51
+ workspaceAgents = entities
52
+ .filter((entity) => entity.kind === "agent")
53
+ .map((agent) => ({
54
+ ...agent,
55
+ provider: byId.get(agent.parentId),
56
+ }));
57
+ return workspaceAgents;
58
+ }
59
+
60
+ function renderAgents() {
61
+ const { grid } = agentElements();
62
+ if (!grid) return;
63
+
64
+ if (!workspaceAgents.length) {
65
+ grid.className = "p-5";
66
+ grid.innerHTML = `<p class="text-sm text-gray-500 dark:text-gray-400">${esc(tr("agents.empty"))}</p>`;
67
+ return;
68
+ }
69
+
70
+ grid.className =
71
+ "p-5 overflow-y-auto grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3";
72
+ grid.innerHTML = workspaceAgents.map(agentCardHtml).join("");
73
+ grid.querySelectorAll("[data-agent-id]").forEach((button) => {
74
+ button.addEventListener("click", () => selectAgent(button.dataset.agentId));
75
+ });
76
+ }
77
+
78
+ function agentCardHtml(agent) {
79
+ const provider = agent.provider;
80
+ const disabled =
81
+ !provider ||
82
+ provider.kind !== "llm" ||
83
+ !provider.config?.model ||
84
+ !agent.config?.systemPrompt ||
85
+ (agent.config?.requiresUserInput &&
86
+ !agent.config?.userInputDescription?.trim());
87
+ const status = disabled
88
+ ? tr("agents.not_ready")
89
+ : agent.config?.requiresUserInput
90
+ ? tr("agents.requires_input")
91
+ : tr("agents.ready");
92
+
93
+ return `
94
+ <button
95
+ type="button"
96
+ data-agent-id="${esc(agent.id)}"
97
+ ${disabled ? "disabled" : ""}
98
+ class="text-left rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-950 px-4 py-3 transition-colors ${disabled ? "opacity-55 cursor-not-allowed" : "hover:border-blue-500 hover:shadow-sm"}"
99
+ >
100
+ <span class="block text-sm font-bold text-gray-900 dark:text-gray-100">${esc(agent.label)}</span>
101
+ <span class="block text-xs text-gray-500 dark:text-gray-400 mt-1">${esc(provider?.label ?? tr("agents.no_provider"))}</span>
102
+ <span class="inline-flex mt-3 text-[11px] font-semibold rounded-full px-2 py-1 bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300">${esc(status)}</span>
103
+ </button>
104
+ `;
105
+ }
106
+
107
+ async function openAiAgentsModal() {
108
+ const { modal, grid } = agentElements();
109
+ if (!modal || !grid) return;
110
+
111
+ modal.classList.remove("hidden");
112
+ grid.className = "p-5";
113
+ grid.innerHTML = `<p class="text-sm text-gray-400">${esc(tr("common.loading"))}</p>`;
114
+
115
+ try {
116
+ await loadWorkspaceAgents();
117
+ renderAgents();
118
+ } catch (error) {
119
+ grid.innerHTML = `<p class="text-sm text-red-500">${esc(error instanceof Error ? error.message : tr("agents.load_failed"))}</p>`;
120
+ }
121
+ }
122
+
123
+ function closeAiAgentsModal() {
124
+ const { modal } = agentElements();
125
+ modal?.classList.add("hidden");
126
+ }
127
+
128
+ function selectAgent(agentId) {
129
+ const agent = workspaceAgents.find((item) => item.id === agentId);
130
+ if (!agent) return;
131
+
132
+ closeAiAgentsModal();
133
+ if (agent.config?.requiresUserInput) {
134
+ openAiAgentInputModal(agent);
135
+ return;
136
+ }
137
+
138
+ void executeAgent(agent.id, "");
139
+ }
140
+
141
+ function openAiAgentInputModal(agent) {
142
+ const { inputModal, inputTitle, inputHint, input, runInputButton } =
143
+ agentElements();
144
+ selectedAgentId = agent.id;
145
+ if (inputTitle) inputTitle.textContent = agent.label;
146
+ if (inputHint) {
147
+ inputHint.textContent =
148
+ agent.config?.userInputDescription?.trim() || tr("agents.input_hint");
149
+ }
150
+ if (input) {
151
+ input.value = "";
152
+ window.setTimeout(() => input.focus(), 0);
153
+ }
154
+ runInputButton?.removeAttribute("disabled");
155
+ inputModal?.classList.remove("hidden");
156
+ }
157
+
158
+ function closeAiAgentInputModal() {
159
+ const { inputModal } = agentElements();
160
+ inputModal?.classList.add("hidden");
161
+ selectedAgentId = null;
162
+ }
163
+
164
+ async function executeAgent(agentId, userInput) {
165
+ const agent = workspaceAgents.find((item) => item.id === agentId);
166
+ createdDocumentId = null;
167
+ showAgentToast("loading", tr("agents.loading"), agent?.label ?? "");
168
+
169
+ try {
170
+ const response = await fetch("/api/workspace/run-agent-document", {
171
+ method: "POST",
172
+ headers: {
173
+ Accept: "application/json",
174
+ "Content-Type": "application/json",
175
+ },
176
+ body: JSON.stringify({ agentId, userInput }),
177
+ });
178
+ const result = await response.json();
179
+ if (result.document?.id) {
180
+ createdDocumentId = result.document.id;
181
+ }
182
+
183
+ if (response.ok && result.ok) {
184
+ showAgentToast(
185
+ "success",
186
+ tr("agents.success"),
187
+ result.document?.filename ?? "",
188
+ createdDocumentId,
189
+ );
190
+ } else {
191
+ showAgentToast(
192
+ "error",
193
+ tr("agents.failure"),
194
+ result.error ?? tr("agents.run_failed"),
195
+ createdDocumentId,
196
+ );
197
+ }
198
+
199
+ if (createdDocumentId && typeof loadDocuments === "function") {
200
+ await loadDocuments();
201
+ }
202
+ } catch (error) {
203
+ showAgentToast(
204
+ "error",
205
+ tr("agents.failure"),
206
+ error instanceof Error ? error.message : tr("agents.run_failed"),
207
+ );
208
+ }
209
+ }
210
+
211
+ function showAgentToast(state, title, message, documentId) {
212
+ const { toast, toastIcon, toastTitle, toastMessage, toastOpen } =
213
+ agentElements();
214
+ if (!toast || !toastIcon || !toastTitle || !toastMessage || !toastOpen) {
215
+ return;
216
+ }
217
+
218
+ toast.classList.remove("hidden");
219
+ toastTitle.textContent = title;
220
+ toastMessage.textContent = message;
221
+ toastOpen.classList.toggle("hidden", !documentId);
222
+
223
+ toastIcon.className = "mt-0.5 h-5 w-5 rounded-full shrink-0";
224
+ if (state === "loading") {
225
+ toastIcon.className +=
226
+ " border-2 border-blue-500 border-t-transparent animate-spin";
227
+ toastIcon.textContent = "";
228
+ } else if (state === "success") {
229
+ toastIcon.className +=
230
+ " bg-green-500 text-white text-xs flex items-center justify-center";
231
+ toastIcon.textContent = "✓";
232
+ } else {
233
+ toastIcon.className +=
234
+ " bg-red-500 text-white text-xs flex items-center justify-center";
235
+ toastIcon.textContent = "!";
236
+ }
237
+ }
238
+
239
+ function closeAgentToast() {
240
+ const { toast } = agentElements();
241
+ toast?.classList.add("hidden");
242
+ }
243
+
244
+ async function openCreatedAgentDocument() {
245
+ if (!createdDocumentId) return;
246
+ if (typeof loadDocuments === "function") {
247
+ await loadDocuments();
248
+ }
249
+ if (typeof openDocument === "function") {
250
+ await openDocument(createdDocumentId, false);
251
+ closeAgentToast();
252
+ } else {
253
+ closeAgentToast();
254
+ location.href = `/?doc=${encodeURIComponent(createdDocumentId)}`;
255
+ }
256
+ }
257
+
258
+ document.addEventListener("DOMContentLoaded", () => {
259
+ const {
260
+ button,
261
+ runInputButton,
262
+ input,
263
+ toastOpen,
264
+ toastClose,
265
+ } = agentElements();
266
+
267
+ button?.addEventListener("click", () => {
268
+ void openAiAgentsModal();
269
+ });
270
+ runInputButton?.addEventListener("click", () => {
271
+ if (!selectedAgentId) return;
272
+ const agentId = selectedAgentId;
273
+ const value = input?.value ?? "";
274
+ closeAiAgentInputModal();
275
+ void executeAgent(agentId, value);
276
+ });
277
+ toastOpen?.addEventListener("click", () => {
278
+ void openCreatedAgentDocument();
279
+ });
280
+ toastClose?.addEventListener("click", () => {
281
+ closeAgentToast();
282
+ });
283
+ });
284
+
285
+ window.openAiAgentsModal = openAiAgentsModal;
286
+ window.closeAiAgentsModal = closeAiAgentsModal;
287
+ window.closeAiAgentInputModal = closeAiAgentInputModal;
288
+ })();
@@ -6,8 +6,9 @@ async function loadConfig() {
6
6
  await window.initI18n(cfg.language || 'en');
7
7
  window.applyI18n();
8
8
  if (cfg.title) document.title = cfg.title;
9
- document.getElementById("app-title").textContent =
10
- cfg.title || "Living Documentation";
9
+ document.getElementById("app-title").textContent = "Living AI Documentation";
10
+ const subtitle = document.getElementById("app-subtitle");
11
+ if (subtitle) subtitle.textContent = cfg.title || "";
11
12
  if (cfg.filenamePattern) {
12
13
  document.getElementById("welcome-pattern").textContent =
13
14
  cfg.filenamePattern + ".md";
@@ -0,0 +1,72 @@
1
+ // ── Shared color picker popup ─────────────────────────────────────────────────
2
+ // Opens a mini popup with rectangular swatches anchored below a trigger element.
3
+ // Used by node-panel, edge-panel and defaults-modal.
4
+
5
+ export function closeAllColorPickerPopups() {
6
+ document.querySelectorAll('.ld-color-popup').forEach((p) => p.remove());
7
+ }
8
+
9
+ /**
10
+ * @param {HTMLElement} trigger — the swatch button that was clicked
11
+ * @param {Array<{bg: string, border: string, value: string, label?: string}>} entries
12
+ * @param {string} selectedValue — currently selected value (matched against entry.value)
13
+ * @param {function(entry): void} onSelect — called when user picks a color
14
+ * @param {{ columns?: number }} [opts]
15
+ */
16
+ export function openColorPickerPopup(trigger, entries, selectedValue, onSelect, opts = {}) {
17
+ closeAllColorPickerPopups();
18
+
19
+ const cols = opts.columns || 5;
20
+ const isDark = document.documentElement.classList.contains('dark');
21
+
22
+ const popup = document.createElement('div');
23
+ popup.className = 'ld-color-popup';
24
+ popup.style.cssText = `
25
+ position:fixed;
26
+ z-index:2000;
27
+ background:${isDark ? '#1f2937' : 'white'};
28
+ border:1px solid ${isDark ? '#4b5563' : '#e5e7eb'};
29
+ border-radius:0.5rem;
30
+ padding:0.5rem;
31
+ box-shadow:0 8px 24px rgba(0,0,0,0.2);
32
+ display:grid;
33
+ grid-template-columns:repeat(${cols},1.5rem);
34
+ gap:0.3rem;
35
+ `;
36
+
37
+ entries.forEach((entry) => {
38
+ const isSelected = entry.value === selectedValue;
39
+ const btn = document.createElement('button');
40
+ btn.type = 'button';
41
+ btn.title = entry.label || entry.value;
42
+ btn.style.cssText = `
43
+ width:1.5rem;height:1.5rem;border-radius:0.25rem;
44
+ background:${entry.bg};border:2px solid ${entry.border};
45
+ cursor:pointer;
46
+ ${isSelected ? 'outline:2px solid #f97316;outline-offset:1px;' : ''}
47
+ `;
48
+ btn.addEventListener('click', (e) => {
49
+ e.stopPropagation();
50
+ popup.remove();
51
+ onSelect(entry);
52
+ });
53
+ popup.appendChild(btn);
54
+ });
55
+
56
+ // Position below trigger, shift left if near right edge
57
+ const rect = trigger.getBoundingClientRect();
58
+ const popupW = cols * (24 + 4.8); // approx
59
+ const left = Math.min(rect.left, window.innerWidth - popupW - 8);
60
+ popup.style.top = (rect.bottom + 4) + 'px';
61
+ popup.style.left = Math.max(4, left) + 'px';
62
+
63
+ document.body.appendChild(popup);
64
+
65
+ const onOutside = (e) => {
66
+ if (!popup.contains(e.target) && e.target !== trigger) {
67
+ popup.remove();
68
+ document.removeEventListener('click', onOutside, true);
69
+ }
70
+ };
71
+ setTimeout(() => document.addEventListener('click', onOutside, true), 0);
72
+ }
@@ -0,0 +1,302 @@
1
+ // ── Diagram Defaults Modal ─────────────────────────────────────────────────────
2
+ // Manages project-level creation defaults stored in .living-doc.json under
3
+ // the `diagramDefaults` key. Provides fallback values for node-panel and
4
+ // edge-panel when no last-used localStorage style exists.
5
+
6
+ import { t } from './t.js';
7
+ import { NODE_COLORS, DEFAULT_NODE_PALETTE } from './constants.js';
8
+ import { openColorPickerPopup, closeAllColorPickerPopups } from './color-picker.js';
9
+ import { st } from './state.js';
10
+
11
+ function effectiveNodeColor(key) {
12
+ return st.nodeColorOverrides[key] || NODE_COLORS[key] || NODE_COLORS['c-gray'];
13
+ }
14
+
15
+ const SHAPE_KEYS = ['box', 'ellipse', 'circle', 'database', 'actor', 'post-it', 'text-free'];
16
+
17
+ const SHAPE_SYSTEM_DEFAULTS = {
18
+ box: { width: 100, height: 40, colorKey: 'c-gray', fontSize: 13 },
19
+ ellipse: { width: 110, height: 50, colorKey: 'c-gray', fontSize: 13 },
20
+ circle: { width: 55, height: 55, colorKey: 'c-gray', fontSize: 13 },
21
+ database: { width: 50, height: 70, colorKey: 'c-gray', fontSize: 13 },
22
+ actor: { width: 30, height: 52, colorKey: 'c-gray', fontSize: 13 },
23
+ 'post-it': { width: 120, height: 100, colorKey: 'c-amber', fontSize: 13 },
24
+ 'text-free':{ width: 80, height: 30, colorKey: 'c-gray', fontSize: 13 },
25
+ };
26
+
27
+ const ARROW_SYSTEM_DEFAULTS = { fontSize: 11, arrowDir: 'to', dashes: false };
28
+
29
+ let _cache = null; // null = not yet loaded from config
30
+
31
+ export function initDiagramDefaults(cfg) {
32
+ _cache = cfg.diagramDefaults || null;
33
+ }
34
+
35
+ export function getDiagramDefaults() {
36
+ return _cache;
37
+ }
38
+
39
+ // Returns the effective defaults for a shape, merging project defaults over system defaults.
40
+ export function getShapeDefaults(shapeType) {
41
+ const system = SHAPE_SYSTEM_DEFAULTS[shapeType] || SHAPE_SYSTEM_DEFAULTS.box;
42
+ const project = _cache?.shapes?.[shapeType] || {};
43
+ return { ...system, ...project };
44
+ }
45
+
46
+ // Returns the effective defaults for arrows.
47
+ export function getArrowDefaults() {
48
+ const project = _cache?.arrows || {};
49
+ return { ...ARROW_SYSTEM_DEFAULTS, ...project };
50
+ }
51
+
52
+ // ── Modal ─────────────────────────────────────────────────────────────────────
53
+
54
+ function buildNodeColorEntries() {
55
+ return DEFAULT_NODE_PALETTE.map((key) => {
56
+ const c = effectiveNodeColor(key);
57
+ return { value: key, bg: c.bg, border: c.border, label: key.replace('c-', '') };
58
+ });
59
+ }
60
+
61
+ function buildColorSwatch(selectedKey, shape) {
62
+ const c = effectiveNodeColor(selectedKey);
63
+ return `
64
+ <button type="button" class="ld-color-swatch-trigger" data-shape="${shape}"
65
+ style="width:2rem;height:1.4rem;border-radius:0.25rem;border:2px solid ${c.border};background:${c.bg};cursor:pointer;flex-shrink:0;"
66
+ title="${selectedKey.replace('c-', '')}">
67
+ </button>`;
68
+ }
69
+
70
+ function renderModal() {
71
+ const existing = document.getElementById('diagramDefaultsModal');
72
+ if (existing) existing.remove();
73
+
74
+ const effective = {
75
+ arrows: getArrowDefaults(),
76
+ shapes: Object.fromEntries(SHAPE_KEYS.map((k) => [k, getShapeDefaults(k)])),
77
+ };
78
+
79
+ const shapeLabels = {
80
+ box: t('diagram.defaults.shape.box'),
81
+ ellipse: t('diagram.defaults.shape.ellipse'),
82
+ circle: t('diagram.defaults.shape.circle'),
83
+ database: t('diagram.defaults.shape.database'),
84
+ actor: t('diagram.defaults.shape.actor'),
85
+ 'post-it': t('diagram.defaults.shape.postit'),
86
+ 'text-free': t('diagram.defaults.shape.text_free'),
87
+ };
88
+
89
+ const arrowDirOptions = ['none', 'from', 'to', 'both'].map((d) => {
90
+ const labels = { none: '—', from: '←', to: '→', both: '←→' };
91
+ const sel = d === effective.arrows.arrowDir ? 'selected' : '';
92
+ return `<option value="${d}" ${sel}>${labels[d]}</option>`;
93
+ }).join('');
94
+
95
+ const inputStyle = 'width:3.5rem;font-size:0.75rem;border:1px solid #d1d5db;border-radius:0.375rem;padding:0.2rem 0.4rem;background:inherit;color:inherit;text-align:center;';
96
+
97
+ const shapeRows = SHAPE_KEYS.map((key) => {
98
+ const sd = effective.shapes[key];
99
+ return `
100
+ <tr data-shape="${key}" style="border-top:1px solid var(--ld-modal-sep,#f3f4f6);">
101
+ <td style="padding:0.45rem 0.5rem 0.45rem 0;font-size:0.8rem;font-weight:500;white-space:nowrap;">${shapeLabels[key]}</td>
102
+ <td style="padding:0.45rem 0.25rem;text-align:center;">
103
+ <input type="number" class="ld-defaults-input ld-defaults-width" data-shape="${key}" min="10" max="800" value="${sd.width}" style="${inputStyle}" />
104
+ </td>
105
+ <td style="padding:0.45rem 0.25rem;text-align:center;">
106
+ <input type="number" class="ld-defaults-input ld-defaults-height" data-shape="${key}" min="10" max="800" value="${sd.height}" style="${inputStyle}" />
107
+ </td>
108
+ <td style="padding:0.45rem 0.25rem;text-align:center;">
109
+ <input type="number" class="ld-defaults-input ld-defaults-font" data-shape="${key}" min="6" max="72" value="${sd.fontSize}" style="${inputStyle}" />
110
+ </td>
111
+ <td style="padding:0.45rem 0.25rem;text-align:center;">
112
+ ${buildColorSwatch(sd.colorKey, key)}
113
+ <input type="hidden" class="ld-defaults-color-key" data-shape="${key}" value="${sd.colorKey}" />
114
+ </td>
115
+ </tr>`;
116
+ }).join('');
117
+
118
+ const modal = document.createElement('div');
119
+ modal.id = 'diagramDefaultsModal';
120
+ modal.style.cssText = 'position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;';
121
+ modal.innerHTML = `
122
+ <div class="ld-defaults-panel" style="
123
+ background:var(--ld-modal-bg,white);
124
+ border-radius:0.75rem;
125
+ box-shadow:0 8px 32px rgba(0,0,0,0.22);
126
+ width:min(560px,95vw);
127
+ max-height:85vh;
128
+ display:flex;flex-direction:column;
129
+ overflow:hidden;
130
+ color:var(--ld-modal-color,#111827);
131
+ ">
132
+ <!-- Header -->
133
+ <div style="display:flex;align-items:center;justify-content:space-between;padding:0.875rem 1rem;border-bottom:1px solid var(--ld-modal-sep,#e5e7eb);flex-shrink:0;">
134
+ <span style="font-size:0.875rem;font-weight:600;">${t('diagram.defaults.title')}</span>
135
+ <button id="btnDefaultsClose" style="background:none;border:none;cursor:pointer;font-size:1rem;color:currentColor;padding:0.25rem;">✕</button>
136
+ </div>
137
+
138
+ <!-- Body -->
139
+ <div style="overflow-y:auto;padding:1rem;display:flex;flex-direction:column;gap:1.25rem;">
140
+
141
+ <!-- Arrows section -->
142
+ <section>
143
+ <div style="font-size:0.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin-bottom:0.5rem;">${t('diagram.defaults.section.arrows')}</div>
144
+ <div style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;">
145
+ <div style="display:flex;align-items:center;gap:0.375rem;">
146
+ <label style="font-size:0.75rem;">${t('diagram.defaults.field.direction')}</label>
147
+ <select id="defaultArrowDir" style="font-size:0.75rem;border:1px solid #d1d5db;border-radius:0.375rem;padding:0.2rem 0.4rem;background:inherit;color:inherit;">
148
+ ${arrowDirOptions}
149
+ </select>
150
+ </div>
151
+ <div style="display:flex;align-items:center;gap:0.375rem;">
152
+ <label style="font-size:0.75rem;">${t('diagram.defaults.field.style')}</label>
153
+ <select id="defaultArrowDashes" style="font-size:0.75rem;border:1px solid #d1d5db;border-radius:0.375rem;padding:0.2rem 0.4rem;background:inherit;color:inherit;">
154
+ <option value="false" ${!effective.arrows.dashes ? 'selected' : ''}>${t('diagram.defaults.style.solid')}</option>
155
+ <option value="true" ${effective.arrows.dashes ? 'selected' : ''}>${t('diagram.defaults.style.dashed')}</option>
156
+ </select>
157
+ </div>
158
+ <div style="display:flex;align-items:center;gap:0.375rem;">
159
+ <label style="font-size:0.75rem;">${t('diagram.defaults.field.font_size')}</label>
160
+ <input id="defaultArrowFontSize" type="number" min="6" max="72" value="${effective.arrows.fontSize}"
161
+ style="width:3.5rem;font-size:0.75rem;border:1px solid #d1d5db;border-radius:0.375rem;padding:0.2rem 0.4rem;background:inherit;color:inherit;" />
162
+ </div>
163
+ </div>
164
+ </section>
165
+
166
+ <!-- Shapes section -->
167
+ <section>
168
+ <div style="font-size:0.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin-bottom:0.5rem;">${t('diagram.defaults.section.shapes')}</div>
169
+ <table style="width:100%;border-collapse:collapse;">
170
+ <thead>
171
+ <tr>
172
+ <th style="font-size:0.7rem;font-weight:500;color:#9ca3af;text-align:left;padding:0 0.5rem 0.4rem 0;"></th>
173
+ <th style="font-size:0.7rem;font-weight:500;color:#9ca3af;text-align:center;padding:0 0.25rem 0.4rem;">${t('diagram.defaults.field.width')}</th>
174
+ <th style="font-size:0.7rem;font-weight:500;color:#9ca3af;text-align:center;padding:0 0.25rem 0.4rem;">${t('diagram.defaults.field.height')}</th>
175
+ <th style="font-size:0.7rem;font-weight:500;color:#9ca3af;text-align:center;padding:0 0.25rem 0.4rem;">${t('diagram.defaults.field.font_size')}</th>
176
+ <th style="font-size:0.7rem;font-weight:500;color:#9ca3af;text-align:center;padding:0 0.25rem 0.4rem;">${t('diagram.defaults.field.color')}</th>
177
+ </tr>
178
+ </thead>
179
+ <tbody>
180
+ ${shapeRows}
181
+ </tbody>
182
+ </table>
183
+ </section>
184
+ </div>
185
+
186
+ <!-- Footer -->
187
+ <div style="display:flex;justify-content:flex-end;gap:0.5rem;padding:0.75rem 1rem;border-top:1px solid var(--ld-modal-sep,#e5e7eb);flex-shrink:0;">
188
+ <button id="btnDefaultsReset" style="font-size:0.75rem;padding:0.375rem 0.75rem;border:1px solid #d1d5db;border-radius:0.5rem;background:none;cursor:pointer;color:currentColor;">${t('common.reset')}</button>
189
+ <button id="btnDefaultsSave" style="font-size:0.75rem;padding:0.375rem 0.875rem;border-radius:0.5rem;background:#3b82f6;color:white;border:none;cursor:pointer;font-weight:600;">${t('common.save')}</button>
190
+ </div>
191
+ </div>
192
+ `;
193
+
194
+ // Adapt to dark mode
195
+ const isDark = document.documentElement.classList.contains('dark');
196
+ if (isDark) {
197
+ const panel = modal.querySelector('.ld-defaults-panel');
198
+ panel.style.setProperty('--ld-modal-bg', '#1f2937');
199
+ panel.style.setProperty('--ld-modal-color', '#f3f4f6');
200
+ panel.style.setProperty('--ld-modal-sep', '#374151');
201
+ panel.style.background = '#1f2937';
202
+ panel.style.color = '#f3f4f6';
203
+ modal.querySelectorAll('input,select').forEach((el) => {
204
+ el.style.background = '#111827';
205
+ el.style.color = '#f3f4f6';
206
+ el.style.borderColor = '#4b5563';
207
+ });
208
+ }
209
+
210
+ document.body.appendChild(modal);
211
+
212
+ // Wire color swatch triggers
213
+ modal.querySelectorAll('.ld-color-swatch-trigger').forEach((trigger) => {
214
+ trigger.addEventListener('click', (e) => {
215
+ e.stopPropagation();
216
+ const shape = trigger.dataset.shape;
217
+ const currentKey = modal.querySelector(`.ld-defaults-color-key[data-shape="${shape}"]`).value;
218
+ openColorPickerPopup(trigger, buildNodeColorEntries(), currentKey, (entry) => {
219
+ const c = effectiveNodeColor(entry.value);
220
+ modal.querySelector(`.ld-defaults-color-key[data-shape="${shape}"]`).value = entry.value;
221
+ trigger.style.background = c.bg;
222
+ trigger.style.borderColor = c.border;
223
+ trigger.title = entry.label;
224
+ });
225
+ });
226
+ });
227
+
228
+ // Close
229
+ const closeModal = () => { closeAllColorPickerPopups(); modal.remove(); };
230
+ modal.querySelector('#btnDefaultsClose').addEventListener('click', closeModal);
231
+ modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); });
232
+
233
+ // Reset to system defaults
234
+ modal.querySelector('#btnDefaultsReset').addEventListener('click', async () => {
235
+ try {
236
+ await fetch('/api/config', {
237
+ method: 'PUT',
238
+ headers: { 'Content-Type': 'application/json' },
239
+ body: JSON.stringify({ diagramDefaults: null }),
240
+ });
241
+ _cache = null;
242
+ // Vider les clés localStorage pour revenir aux valeurs système
243
+ SHAPE_KEYS.forEach((key) => localStorage.removeItem('ld-node-style-' + key));
244
+ localStorage.removeItem('ld-free-arrow-style');
245
+ modal.remove();
246
+ } catch (err) {
247
+ console.error('Failed to reset diagram defaults', err);
248
+ }
249
+ });
250
+
251
+ // Save
252
+ modal.querySelector('#btnDefaultsSave').addEventListener('click', async () => {
253
+ const arrows = {
254
+ fontSize: parseInt(modal.querySelector('#defaultArrowFontSize').value, 10) || 11,
255
+ arrowDir: modal.querySelector('#defaultArrowDir').value,
256
+ dashes: modal.querySelector('#defaultArrowDashes').value === 'true',
257
+ };
258
+ const shapes = {};
259
+ SHAPE_KEYS.forEach((key) => {
260
+ shapes[key] = {
261
+ width: parseInt(modal.querySelector(`.ld-defaults-width[data-shape="${key}"]`).value, 10) || SHAPE_SYSTEM_DEFAULTS[key].width,
262
+ height: parseInt(modal.querySelector(`.ld-defaults-height[data-shape="${key}"]`).value, 10) || SHAPE_SYSTEM_DEFAULTS[key].height,
263
+ fontSize: parseInt(modal.querySelector(`.ld-defaults-font[data-shape="${key}"]`).value, 10) || SHAPE_SYSTEM_DEFAULTS[key].fontSize,
264
+ colorKey: modal.querySelector(`.ld-defaults-color-key[data-shape="${key}"]`).value || SHAPE_SYSTEM_DEFAULTS[key].colorKey,
265
+ };
266
+ });
267
+ try {
268
+ const res = await fetch('/api/config', {
269
+ method: 'PUT',
270
+ headers: { 'Content-Type': 'application/json' },
271
+ body: JSON.stringify({ diagramDefaults: { arrows, shapes } }),
272
+ });
273
+ if (!res.ok) throw new Error(await res.text());
274
+ _cache = { arrows, shapes };
275
+ // Sync localStorage: chaque clé de forme + flèches
276
+ SHAPE_KEYS.forEach((key) => {
277
+ localStorage.setItem('ld-node-style-' + key, JSON.stringify({
278
+ colorKey: shapes[key].colorKey,
279
+ fontSize: shapes[key].fontSize,
280
+ width: shapes[key].width,
281
+ height: shapes[key].height,
282
+ textAlign: null,
283
+ textValign: null,
284
+ }));
285
+ });
286
+ localStorage.setItem('ld-free-arrow-style', JSON.stringify({
287
+ arrowDir: arrows.arrowDir,
288
+ dashes: arrows.dashes,
289
+ fontSize: arrows.fontSize,
290
+ edgeColor: null,
291
+ edgeWidth: null,
292
+ }));
293
+ modal.remove();
294
+ } catch (err) {
295
+ console.error('Failed to save diagram defaults', err);
296
+ }
297
+ });
298
+ }
299
+
300
+ export function openDefaultsModal() {
301
+ renderModal();
302
+ }