tadforce 0.0.0-stage → 0.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 (141) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +202 -0
  4. package/NOTICE +11 -0
  5. package/README.md +266 -2
  6. package/assets/logo-mark.svg +24 -0
  7. package/assets/logo.svg +31 -0
  8. package/bin/tadforce.js +4 -0
  9. package/commands/tadforce-review.md +8 -0
  10. package/commands/tadforce-update.md +6 -0
  11. package/commands/tadforce.md +10 -0
  12. package/package.json +60 -4
  13. package/skills/tadforce/SKILL.md +73 -0
  14. package/skills/tadforce/references/axes-rules.md +29 -0
  15. package/skills/tadforce/references/evidence-rules.md +40 -0
  16. package/skills/tadforce/references/exclusion-heuristics.md +40 -0
  17. package/skills/tadforce/references/figure-rules.md +42 -0
  18. package/skills/tadforce/references/markdown-conventions.md +67 -0
  19. package/skills/tadforce/references/modules/agentforce-ai.md +24 -0
  20. package/skills/tadforce/references/modules/analytics.md +19 -0
  21. package/skills/tadforce/references/modules/commerce.md +25 -0
  22. package/skills/tadforce/references/modules/data-cloud.md +23 -0
  23. package/skills/tadforce/references/modules/experience-cloud.md +24 -0
  24. package/skills/tadforce/references/modules/field-service.md +25 -0
  25. package/skills/tadforce/references/modules/industries.md +21 -0
  26. package/skills/tadforce/references/modules/integration-platforms.md +19 -0
  27. package/skills/tadforce/references/modules/marketing.md +24 -0
  28. package/skills/tadforce/references/modules/omnistudio.md +24 -0
  29. package/skills/tadforce/references/modules/platform-events.md +19 -0
  30. package/skills/tadforce/references/modules/revenue-cpq.md +25 -0
  31. package/skills/tadforce/references/modules/sales-cloud.md +26 -0
  32. package/skills/tadforce/references/modules/security-shield.md +18 -0
  33. package/skills/tadforce/references/modules/service-cloud.md +27 -0
  34. package/skills/tadforce/references/naming-external-systems.md +32 -0
  35. package/skills/tadforce/references/questions.md +59 -0
  36. package/skills/tadforce/references/salesforce-skills.md +27 -0
  37. package/skills/tadforce/references/workflow.md +95 -0
  38. package/skills/tadforce/references/writing-rules.md +47 -0
  39. package/skills/tadforce-review/SKILL.md +53 -0
  40. package/skills/tadforce-update/SKILL.md +48 -0
  41. package/src/build/brand.js +107 -0
  42. package/src/build/browser.js +70 -0
  43. package/src/build/directives.js +293 -0
  44. package/src/build/export.js +60 -0
  45. package/src/build/figures.js +119 -0
  46. package/src/build/mermaid.js +107 -0
  47. package/src/build/render.js +299 -0
  48. package/src/build/serve.js +59 -0
  49. package/src/cli.js +68 -0
  50. package/src/commands/docs.js +204 -0
  51. package/src/commands/doctor.js +370 -0
  52. package/src/commands/init.js +132 -0
  53. package/src/commands/org.js +151 -0
  54. package/src/commands/review.js +35 -0
  55. package/src/commands/spec.js +91 -0
  56. package/src/commands/update.js +154 -0
  57. package/src/facts/extractors/apex.js +111 -0
  58. package/src/facts/extractors/automation.js +85 -0
  59. package/src/facts/extractors/data.js +94 -0
  60. package/src/facts/extractors/integration.js +76 -0
  61. package/src/facts/extractors/misc.js +100 -0
  62. package/src/facts/extractors/security.js +92 -0
  63. package/src/facts/extractors/ui.js +120 -0
  64. package/src/facts/fingerprints.js +32 -0
  65. package/src/facts/heuristics/exclusions.js +143 -0
  66. package/src/facts/heuristics/modules.js +85 -0
  67. package/src/facts/heuristics/naming.js +31 -0
  68. package/src/facts/heuristics/signals.js +76 -0
  69. package/src/facts/index.js +197 -0
  70. package/src/facts/org.js +87 -0
  71. package/src/facts/xml.js +85 -0
  72. package/src/sf/sf.js +120 -0
  73. package/src/util/args.js +21 -0
  74. package/src/util/config.js +104 -0
  75. package/src/util/fsx.js +58 -0
  76. package/src/util/i18n.js +49 -0
  77. package/src/util/log.js +31 -0
  78. package/src/util/paths.js +11 -0
  79. package/src/validate/index.js +126 -0
  80. package/templates/axes/en/00-document-control.md +3 -0
  81. package/templates/axes/en/01-summary.md +7 -0
  82. package/templates/axes/en/02-method.md +9 -0
  83. package/templates/axes/en/03-findings.md +19 -0
  84. package/templates/axes/en/04-register.md +3 -0
  85. package/templates/axes/en/05-roadmap.md +3 -0
  86. package/templates/axes/en/99-appendix.md +5 -0
  87. package/templates/axes/fr/00-document-control.md +3 -0
  88. package/templates/axes/fr/01-summary.md +7 -0
  89. package/templates/axes/fr/02-method.md +9 -0
  90. package/templates/axes/fr/03-findings.md +19 -0
  91. package/templates/axes/fr/04-register.md +3 -0
  92. package/templates/axes/fr/05-roadmap.md +3 -0
  93. package/templates/axes/fr/99-appendix.md +5 -0
  94. package/templates/html/cover.html +12 -0
  95. package/templates/html/editor.js +502 -0
  96. package/templates/html/style.css +255 -0
  97. package/templates/html/template.html +88 -0
  98. package/templates/i18n/en.json +366 -0
  99. package/templates/i18n/fr.json +366 -0
  100. package/templates/manifest/package-tad.xml +543 -0
  101. package/templates/specs/sections.yml +494 -0
  102. package/templates/tad/en/00-document-control.md +7 -0
  103. package/templates/tad/en/01-summary.md +17 -0
  104. package/templates/tad/en/02-purpose.md +35 -0
  105. package/templates/tad/en/03-context.md +17 -0
  106. package/templates/tad/en/04-general.md +29 -0
  107. package/templates/tad/en/05-functional.md +7 -0
  108. package/templates/tad/en/06-data.md +71 -0
  109. package/templates/tad/en/07-application.md +49 -0
  110. package/templates/tad/en/08-ui.md +41 -0
  111. package/templates/tad/en/09-integrations.md +53 -0
  112. package/templates/tad/en/10-security.md +59 -0
  113. package/templates/tad/en/11-reporting.md +13 -0
  114. package/templates/tad/en/12-operations.md +25 -0
  115. package/templates/tad/en/13-nfr.md +9 -0
  116. package/templates/tad/en/14-decisions.md +3 -0
  117. package/templates/tad/en/15-outlook.md +3 -0
  118. package/templates/tad/en/99-appendices.md +119 -0
  119. package/templates/tad/fr/00-document-control.md +7 -0
  120. package/templates/tad/fr/01-summary.md +17 -0
  121. package/templates/tad/fr/02-purpose.md +35 -0
  122. package/templates/tad/fr/03-context.md +17 -0
  123. package/templates/tad/fr/04-general.md +29 -0
  124. package/templates/tad/fr/05-functional.md +7 -0
  125. package/templates/tad/fr/06-data.md +71 -0
  126. package/templates/tad/fr/07-application.md +49 -0
  127. package/templates/tad/fr/08-ui.md +41 -0
  128. package/templates/tad/fr/09-integrations.md +53 -0
  129. package/templates/tad/fr/10-security.md +59 -0
  130. package/templates/tad/fr/11-reporting.md +13 -0
  131. package/templates/tad/fr/12-operations.md +25 -0
  132. package/templates/tad/fr/13-nfr.md +9 -0
  133. package/templates/tad/fr/14-decisions.md +3 -0
  134. package/templates/tad/fr/15-outlook.md +3 -0
  135. package/templates/tad/fr/99-appendices.md +119 -0
  136. package/templates/vendor/LICENSE-html-docx-js.txt +21 -0
  137. package/templates/vendor/LICENSE-html2canvas.txt +22 -0
  138. package/templates/vendor/LICENSE-mermaid.txt +21 -0
  139. package/templates/vendor/html-docx.js +13216 -0
  140. package/templates/vendor/html2canvas.min.js +20 -0
  141. package/templates/vendor/mermaid.min.js +2314 -0
@@ -0,0 +1,502 @@
1
+ /* TADForce — in-place editing, targeted comments, saving and PDF / DOCX export. */
2
+ (function () {
3
+ 'use strict';
4
+ var I18N = (window.TAD_I18N && window.TAD_I18N.editor) || {};
5
+ var BRAND = window.TAD_BRAND || {};
6
+ function T(k, vars) {
7
+ var s = I18N[k] !== undefined ? I18N[k] : k;
8
+ return String(s).replace(/\{(\w+)\}/g, function (m, v) { return vars && vars[v] !== undefined ? vars[v] : m; });
9
+ }
10
+ function statusLabel(s) { return T('status_' + (s === 'resolved' ? 'resolved' : 'open')); }
11
+
12
+ var main = document.getElementById('main');
13
+ var zones = [document.querySelector('.cover'), document.querySelector('article.doc')];
14
+ var registryEl = document.getElementById('tad-comments');
15
+ var comments = [];
16
+ try { comments = JSON.parse(registryEl.textContent || '[]'); } catch (e) { comments = []; }
17
+ var editing = false, dirty = false, fileHandle = null, activeCid = null;
18
+
19
+ var $ = function (id) { return document.getElementById(id); };
20
+ var btnEdit = $('btnEdit'), btnComment = $('btnComment'), btnPanel = $('btnPanel'), btnSave = $('btnSave'), btnPrint = $('btnPrint');
21
+ var panel = $('cmtPanel'), list = $('cmtList'), countEl = $('cmtCount'), pop = $('cmtPop'), popText = $('cmtPopText'), menu = $('printMenu');
22
+
23
+ function toast(msg, ms) {
24
+ var t = $('toast'); t.textContent = msg; t.classList.add('on');
25
+ clearTimeout(toast._t); toast._t = setTimeout(function () { t.classList.remove('on'); }, ms || 2600);
26
+ }
27
+ function setDirty(v) { dirty = v; btnSave.classList.toggle('dirty', v); }
28
+ function inZone(node) {
29
+ var el = node && (node.nodeType === 1 ? node : node.parentElement);
30
+ return !!(el && zones.some(function (z) { return z && z.contains(el); }) && !el.closest('svg'));
31
+ }
32
+ function sectionOf(node) {
33
+ var el = node.nodeType === 1 ? node : node.parentElement;
34
+ var heads = Array.prototype.slice.call(document.querySelectorAll('.doc h1, .doc h2, .doc h3'));
35
+ var found = null;
36
+ heads.forEach(function (h) { if (h.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING || h.contains(el)) found = h; });
37
+ return found ? found.textContent.replace(/#$/, '').trim() : (el.closest('.cover') ? T('cover') : T('docStart'));
38
+ }
39
+
40
+ /* ---------- Edit mode ---------- */
41
+ function setEditing(on) {
42
+ editing = on;
43
+ if (on && commentMode) setCommentMode(false);
44
+ zones.forEach(function (z) { if (z) z.contentEditable = on ? 'true' : 'false'; });
45
+ document.querySelectorAll('figure.diagram, .fig-body, .anchor').forEach(function (f) { f.contentEditable = 'false'; });
46
+ document.body.classList.toggle('editing', on);
47
+ btnEdit.innerHTML = on ? T('done') : T('edit');
48
+ if (on) toast(T('editToast'), 4000);
49
+ }
50
+ btnEdit.addEventListener('click', function () { setEditing(!editing); });
51
+ zones.forEach(function (z) {
52
+ if (!z) return;
53
+ z.addEventListener('input', function () {
54
+ var s = window.getSelection();
55
+ var el = s.anchorNode && (s.anchorNode.nodeType === 1 ? s.anchorNode : s.anchorNode.parentElement);
56
+ var blk = el && el.closest('p, li, td, th, h1, h2, h3, h4, figcaption, .kpi, .card, .layer, .meta div');
57
+ if (blk) blk.setAttribute('data-edited', '1');
58
+ setDirty(true);
59
+ });
60
+ });
61
+
62
+ /* ---------- Comments ---------- */
63
+ /* Flow: click « Comment » → select text (the input opens on mouse release)
64
+ or « Capture an area » → draw a rectangle captured as an image. */
65
+ var pending = null, commentMode = false, cap = null;
66
+ function setCommentMode(on) {
67
+ commentMode = on;
68
+ document.body.classList.toggle('commenting', on);
69
+ btnComment.classList.toggle('on', on);
70
+ if (on) { window.getSelection().removeAllRanges(); if (editing) setEditing(false); }
71
+ }
72
+ btnComment.addEventListener('click', function () { if (pop.classList.contains('on')) return; setCommentMode(!commentMode); });
73
+ $('cbarCancel').addEventListener('click', function () { setCommentMode(false); });
74
+ $('cbarCapture').addEventListener('click', function () { setCommentMode(false); startCapture(null); });
75
+ document.addEventListener('mouseup', function (e) {
76
+ if (!commentMode || e.target.closest('#cmtBar, #side, #cmtPanel, #cmtPop')) return;
77
+ setTimeout(function () {
78
+ var s = window.getSelection();
79
+ if (!s.rangeCount || s.isCollapsed || !inZone(s.anchorNode) || !inZone(s.focusNode) || !s.toString().trim()) return;
80
+ var range = s.getRangeAt(0).cloneRange();
81
+ pending = { range: range, quote: range.toString(), section: sectionOf(range.startContainer), image: null, anchor: null };
82
+ setCommentMode(false);
83
+ openPop(range.getBoundingClientRect());
84
+ }, 10);
85
+ });
86
+ function openPop(r) {
87
+ pop.style.top = Math.min(window.innerHeight - 360, Math.max(12, r.bottom + 8)) + 'px';
88
+ pop.style.left = Math.min(window.innerWidth - 380, Math.max(312, r.left)) + 'px';
89
+ $('cmtPopQuote').textContent = pending.range ? '« ' + excerpt(pending.quote, 140) + ' »' : T('zoneCaptured', { section: pending.section });
90
+ refreshPopImg();
91
+ if (!pop.classList.contains('on') && !pending.keepText) popText.value = '';
92
+ pending.keepText = false;
93
+ pop.classList.add('on'); popText.focus();
94
+ }
95
+ function closePop() { pop.classList.remove('on'); pending = null; window.getSelection().removeAllRanges(); }
96
+ $('cmtCancel').addEventListener('click', closePop);
97
+ $('cmtOk').addEventListener('click', addComment);
98
+ $('cmtAddCap').addEventListener('click', function () { pending.keepText = true; pop.classList.remove('on'); startCapture(pending); });
99
+ popText.addEventListener('keydown', function (e) {
100
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) addComment();
101
+ if (e.key === 'Escape') closePop();
102
+ });
103
+
104
+ var ov = $('capOverlay'), box = $('capBox');
105
+ function startCapture(p) {
106
+ cap = { pending: p, drag: false };
107
+ ov.classList.add('on'); box.style.display = 'none';
108
+ }
109
+ function cancelCapture() {
110
+ ov.classList.remove('on'); box.style.display = 'none';
111
+ var p = cap && cap.pending; cap = null;
112
+ if (p) openPop({ bottom: window.innerHeight / 3, left: 340 });
113
+ }
114
+ ov.addEventListener('mousedown', function (e) {
115
+ e.preventDefault(); cap.drag = true; cap.x0 = e.clientX; cap.y0 = e.clientY;
116
+ box.style.display = 'block'; box.style.left = e.clientX + 'px'; box.style.top = e.clientY + 'px'; box.style.width = '0px'; box.style.height = '0px';
117
+ });
118
+ ov.addEventListener('mousemove', function (e) {
119
+ if (!cap || !cap.drag) return;
120
+ box.style.left = Math.min(e.clientX, cap.x0) + 'px'; box.style.top = Math.min(e.clientY, cap.y0) + 'px';
121
+ box.style.width = Math.abs(e.clientX - cap.x0) + 'px'; box.style.height = Math.abs(e.clientY - cap.y0) + 'px';
122
+ });
123
+ ov.addEventListener('mouseup', function (e) {
124
+ if (!cap || !cap.drag) return;
125
+ var r = { left: Math.min(e.clientX, cap.x0), top: Math.min(e.clientY, cap.y0), width: Math.abs(e.clientX - cap.x0), height: Math.abs(e.clientY - cap.y0) };
126
+ var p = cap.pending; cap = null;
127
+ ov.classList.remove('on'); box.style.display = 'none';
128
+ if (r.width < 12 || r.height < 12) { toast(T('zoneTooSmall')); startCapture(p); return; }
129
+ doCapture(r, p);
130
+ });
131
+ function blockAt(x, y) {
132
+ var el = document.elementFromPoint(x, y);
133
+ if (el && el.closest('svg')) el = el.closest('svg').parentElement;
134
+ if (!el || !inZone(el)) return null;
135
+ var b = el.closest('figure.diagram, .fig-body, .tw, .layers, .kpis, .cards, aside.callout, p, li, h1, h2, h3, h4, .meta div');
136
+ if (!b) return null;
137
+ if (b.matches('figure.diagram')) return b.querySelector('figcaption');
138
+ if (b.matches('.fig-body, .layers')) { var n = b.nextElementSibling; return n && n.matches('.figcap') ? n : b.parentElement; }
139
+ if (b.matches('.tw')) return b.querySelector('th, td');
140
+ if (b.matches('.kpis, .cards')) return b.firstElementChild;
141
+ return b;
142
+ }
143
+ /* The comment box opens immediately on mouse release; the image is rendered in the background
144
+ and attached as soon as it is ready (to the pending box or to the already-saved comment). */
145
+ function doCapture(r, p) {
146
+ if (!p) {
147
+ var target = blockAt(r.left + r.width / 2, r.top + Math.min(r.height / 2, 30));
148
+ p = { range: null, quote: '', section: target ? sectionOf(target) : 'Document', image: null, anchor: target };
149
+ }
150
+ p.capturing = true;
151
+ pending = p;
152
+ openPop({ bottom: Math.min(r.top + r.height, window.innerHeight - 380), left: r.left });
153
+ // Let the browser paint the comment box before the (CPU-heavy) rendering starts.
154
+ var plan = capturePlan(r);
155
+ new Promise(function (res) { requestAnimationFrame(function () { setTimeout(res, 30); }); })
156
+ .then(function () { return grab(r, plan); }).then(function (img) {
157
+ p.image = img; p.capturing = false;
158
+ if (pending === p) refreshPopImg();
159
+ if (p.cid) { comments.forEach(function (c) { if (c.id === p.cid) c.image = img; }); persist(); render(); setDirty(true); }
160
+ }, function (err) {
161
+ p.capturing = false; if (pending === p) refreshPopImg();
162
+ toast(T('captureFailed', { error: err && err.message }), 5000);
163
+ });
164
+ }
165
+ function refreshPopImg() {
166
+ $('cmtPopImg').innerHTML = pending.image ? '<img alt="' + T('captureAlt') + '" src="' + pending.image + '">' : (pending.capturing ? '<div class="cap-wait">' + T('captureWait') + '</div>' : '');
167
+ $('cmtAddCap').textContent = pending.image ? T('redoCapture') : T('attachCapture');
168
+ }
169
+ // Blocks of the document far from the zone: their content is neither cloned nor rendered
170
+ // (html2canvas otherwise copies the whole page, thousands of SVG nodes, synchronously).
171
+ function capturePlan(r) {
172
+ var near = function (el) { var b = el.getBoundingClientRect(); return b.bottom >= r.top - 60 && b.top <= r.top + r.height + 60; };
173
+ var far = new Set();
174
+ var plan = zones.map(function (z) {
175
+ if (!z) return null;
176
+ return Array.prototype.map.call(z.children, function (c) { if (near(c)) return null; far.add(c); return c.offsetHeight; });
177
+ });
178
+ plan.far = far;
179
+ // Page position of every kept block, used to re-align the clone exactly (see onclone).
180
+ plan.tops = zones.map(function (z, zi) {
181
+ if (!z || !plan[zi]) return null;
182
+ var px = z.getBoundingClientRect().left;
183
+ return Array.prototype.map.call(z.children, function (c, i) {
184
+ if (plan[zi][i] != null) return null;
185
+ var b = c.getBoundingClientRect(); return { top: b.top + window.scrollY, left: b.left - px, width: b.width };
186
+ });
187
+ });
188
+ return plan;
189
+ }
190
+ function grab(r, plan) {
191
+ if (typeof html2canvas === 'undefined') return Promise.reject(new Error(T('noCaptureLib')));
192
+ var far = plan.far;
193
+ return html2canvas(document.body, {
194
+ x: r.left + window.scrollX, y: r.top + window.scrollY, width: r.width, height: r.height,
195
+ // innerWidth includes the scrollbar: the clone then lays text out on exactly the same width as the page.
196
+ windowWidth: window.innerWidth, windowHeight: window.innerHeight,
197
+ scale: Math.min(2, window.devicePixelRatio || 1), backgroundColor: '#ffffff', logging: false,
198
+ ignoreElements: function (el) {
199
+ return far.has(el.parentElement) || el.tagName === 'SCRIPT' ||
200
+ /^(side|cmtPanel|cmtPop|toast|capOverlay|cmtBar|burger|lightbox)$/.test(el.id || '') || (el.classList && el.classList.contains('edit-banner'));
201
+ },
202
+ onclone: function (d) {
203
+ d.documentElement.style.scrollBehavior = 'auto';
204
+ [d.querySelector('.cover'), d.querySelector('article.doc')].forEach(function (z, zi) {
205
+ if (!z || !plan[zi]) return;
206
+ Array.prototype.forEach.call(z.children, function (c, i) {
207
+ var h = plan[zi][i];
208
+ if (h == null) return;
209
+ c.innerHTML = ''; c.style.boxSizing = 'border-box'; c.style.height = h + 'px'; c.style.minHeight = h + 'px';
210
+ });
211
+ // Any residual difference (margins dropped with emptied blocks, rounding) is cancelled by
212
+ // shifting each kept block back to its exact on-screen position.
213
+ Array.prototype.forEach.call(z.children, function (c, i) {
214
+ var o = plan.tops[zi][i];
215
+ if (!o) return;
216
+ c.style.width = o.width + 'px';
217
+ var dy = o.top - (c.getBoundingClientRect().top + d.defaultView.pageYOffset);
218
+ if (Math.abs(dy) > 0.5) { c.style.position = 'relative'; c.style.top = dy + 'px'; }
219
+ });
220
+ });
221
+ // The clone was scrolled while the emptied blocks had no height yet (scroll clamped):
222
+ // scroll it again now that the page has its real height, or deep zones are offset.
223
+ // The page uses smooth scrolling, which the clone inherits: force an instant jump, otherwise
224
+ // the clone is still mid-animation (or at the top) when html2canvas reads positions.
225
+ d.documentElement.style.scrollBehavior = 'auto';
226
+ d.defaultView.scrollTo({ left: window.scrollX, top: window.scrollY, behavior: 'instant' });
227
+ }
228
+ }).then(function (cv) {
229
+ var k = Math.min(1, 1100 / cv.width), o = document.createElement('canvas');
230
+ o.width = Math.round(cv.width * k); o.height = Math.round(cv.height * k);
231
+ var g = o.getContext('2d'); g.fillStyle = '#fff'; g.fillRect(0, 0, o.width, o.height); g.drawImage(cv, 0, 0, o.width, o.height);
232
+ return o.toDataURL('image/jpeg', 0.82);
233
+ });
234
+ }
235
+ function insertPin(el, cid) {
236
+ var s = document.createElement('span');
237
+ s.className = 'tad-pin'; s.setAttribute('data-cid', cid); s.setAttribute('contenteditable', 'false');
238
+ s.title = T('pinTitle'); s.textContent = '📷';
239
+ el.insertBefore(s, el.firstChild);
240
+ }
241
+ $('lightbox').addEventListener('click', function () { this.classList.remove('on'); });
242
+
243
+ function excerpt(t, n) { t = t.replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; }
244
+ function wrapRange(range, cid) {
245
+ var root = range.commonAncestorContainer;
246
+ var nodes = [];
247
+ if (root.nodeType === 3) nodes = [root];
248
+ else {
249
+ var w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
250
+ acceptNode: function (n) {
251
+ return range.intersectsNode(n) && n.nodeValue.trim() && !n.parentElement.closest('svg') ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
252
+ }
253
+ });
254
+ while (w.nextNode()) nodes.push(w.currentNode);
255
+ }
256
+ nodes.forEach(function (n) {
257
+ var start = n === range.startContainer ? range.startOffset : 0;
258
+ var end = n === range.endContainer ? range.endOffset : n.nodeValue.length;
259
+ if (start >= end) return;
260
+ var t = n;
261
+ if (start > 0) t = t.splitText(start);
262
+ if (end - start < t.nodeValue.length) t.splitText(end - start);
263
+ var m = document.createElement('mark');
264
+ m.className = 'tad-cmt'; m.setAttribute('data-cid', cid);
265
+ t.parentNode.insertBefore(m, t); m.appendChild(t);
266
+ });
267
+ }
268
+ function addComment() {
269
+ var txt = popText.value.trim();
270
+ if (!txt) { popText.focus(); return; }
271
+ var p = pending, cid = 'c' + Date.now().toString(36);
272
+ p.cid = cid;
273
+ if (p.range) wrapRange(p.range, cid); else if (p.anchor) insertPin(p.anchor, cid);
274
+ comments.push({ id: cid, kind: p.range ? 'text' : 'zone', comment: txt, quote: p.range ? excerpt(p.quote, 400) : '', section: p.section,
275
+ image: p.image || null, date: new Date().toISOString(), status: 'open' });
276
+ closePop();
277
+ persist(); render(); openPanel(true); setDirty(true);
278
+ toast(T('added'));
279
+ }
280
+ function persist() { registryEl.textContent = JSON.stringify(comments, null, 1).replace(/</g, '\\u003c'); }
281
+ function marksOf(cid) { return document.querySelectorAll('mark.tad-cmt[data-cid="' + cid + '"], .tad-pin[data-cid="' + cid + '"]'); }
282
+ function unwrap(m) {
283
+ if (m.classList.contains('tad-pin')) { m.remove(); return; }
284
+ var p = m.parentNode; while (m.firstChild) p.insertBefore(m.firstChild, m); p.removeChild(m); p.normalize();
285
+ }
286
+ function render() {
287
+ var open = comments.filter(function (c) { return c.status !== 'resolved'; }).length;
288
+ countEl.textContent = comments.length ? (open + '/' + comments.length) : '0';
289
+ var edited = document.querySelectorAll('[data-edited]').length;
290
+ $('cmtEdited').textContent = edited ? T('edited', { n: edited }) : '';
291
+ if (!comments.length) { list.innerHTML = '<p class="cmt-empty">' + T('empty') + '</p>'; return; }
292
+ list.innerHTML = '';
293
+ comments.forEach(function (c, i) {
294
+ var li = document.createElement('div');
295
+ li.className = 'cmt-item' + (c.status === 'resolved' ? ' done' : '') + (c.id === activeCid ? ' active' : '');
296
+ li.innerHTML = '<div class="cmt-h"><b>#' + (i + 1) + '</b> <span></span><em></em></div><blockquote></blockquote><div class="cmt-t" contenteditable="true"></div>' +
297
+ '<div class="cmt-a"><button data-a="go"></button><button data-a="st"></button><button data-a="del"></button></div>';
298
+ li.querySelector('.cmt-h span').textContent = c.section;
299
+ li.querySelector('.cmt-h em').textContent = statusLabel(c.status);
300
+ li.querySelector('[data-a="go"]').textContent = T('view');
301
+ li.querySelector('[data-a="del"]').textContent = T('delete');
302
+ var bq = li.querySelector('blockquote');
303
+ if (c.quote) bq.textContent = c.quote; else bq.remove();
304
+ if (c.image) {
305
+ var im = document.createElement('img'); im.className = 'cmt-img'; im.src = c.image; im.alt = T('captureAlt'); im.title = T('enlarge');
306
+ im.addEventListener('click', function (ev) { ev.stopPropagation(); $('lightboxImg').src = c.image; $('lightbox').classList.add('on'); });
307
+ li.insertBefore(im, li.querySelector('.cmt-t'));
308
+ }
309
+ li.querySelector('.cmt-t').textContent = c.comment;
310
+ if (c.response) { var rp = document.createElement('div'); rp.className = 'cmt-r'; rp.textContent = T('handled', { response: c.response }); li.insertBefore(rp, li.querySelector('.cmt-a')); }
311
+ li.querySelector('[data-a="st"]').textContent = c.status === 'resolved' ? T('reopen') : T('resolve');
312
+ li.querySelector('.cmt-t').addEventListener('input', function (e) { c.comment = e.target.textContent; persist(); setDirty(true); });
313
+ li.addEventListener('click', function (e) {
314
+ var a = e.target.getAttribute('data-a'); if (!a) return;
315
+ if (a === 'go') focusComment(c.id);
316
+ if (a === 'st') { c.status = c.status === 'resolved' ? 'open' : 'resolved'; marksOf(c.id).forEach(function (m) { m.classList.toggle('resolved', c.status === 'resolved'); }); }
317
+ if (a === 'del') { if (!confirm(T('confirmDelete'))) return; marksOf(c.id).forEach(unwrap); comments = comments.filter(function (x) { return x.id !== c.id; }); }
318
+ if (a !== 'go') { persist(); render(); setDirty(true); }
319
+ });
320
+ list.appendChild(li);
321
+ });
322
+ }
323
+ function focusComment(cid) {
324
+ activeCid = cid;
325
+ document.querySelectorAll('mark.tad-cmt.flash').forEach(function (m) { m.classList.remove('flash'); });
326
+ var ms = marksOf(cid);
327
+ if (ms.length) { ms[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); ms.forEach(function (m) { m.classList.add('flash'); }); }
328
+ render();
329
+ }
330
+ function openPanel(on) { document.body.classList.toggle('panel-open', on === undefined ? !document.body.classList.contains('panel-open') : on); }
331
+ btnPanel.addEventListener('click', function () { openPanel(); render(); });
332
+ $('cmtClose').addEventListener('click', function () { openPanel(false); });
333
+ main.addEventListener('click', function (e) {
334
+ var m = e.target.closest && e.target.closest('mark.tad-cmt, .tad-pin');
335
+ if (m && !editing && !commentMode) { openPanel(true); focusComment(m.getAttribute('data-cid')); }
336
+ });
337
+ $('cmtCopy').addEventListener('click', function () {
338
+ var open = comments.filter(function (c) { return c.status !== 'resolved'; });
339
+ var lines = [T('listTitle', { title: document.title, n: open.length }), ''];
340
+ open.forEach(function (c, i) {
341
+ lines.push((i + 1) + '. [' + c.section + '] ' + (c.quote ? '« ' + excerpt(c.quote, 200) + ' »' : T('capturedZone')) + (c.image ? T('captureInFile') : ''), ' → ' + c.comment, '');
342
+ });
343
+ var txt = lines.join('\n');
344
+ (navigator.clipboard ? navigator.clipboard.writeText(txt) : Promise.reject()).then(function () { toast(T('copied')); }, function () { prompt(T('copyPrompt'), txt); });
345
+ });
346
+
347
+ /* ---------- Saving ---------- */
348
+ function fileName() { var n = decodeURIComponent(location.pathname.split('/').pop() || ''); return /\.html?$/i.test(n) ? n : T('defaultFile'); }
349
+ function serialize() {
350
+ persist();
351
+ var clone = document.documentElement.cloneNode(true);
352
+ clone.querySelectorAll('[contenteditable]').forEach(function (e) { e.removeAttribute('contenteditable'); });
353
+ clone.querySelectorAll('.flash, .on').forEach(function (e) { e.classList.remove('flash', 'on'); });
354
+ var b = clone.querySelector('body'); b.classList.remove('editing', 'nav-open', 'panel-open', 'commenting');
355
+ clone.querySelector('#cmtPopImg').innerHTML = ''; clone.querySelector('#lightboxImg').removeAttribute('src');
356
+ clone.querySelector('#toast').textContent = '';
357
+ clone.querySelector('#cmtList').innerHTML = '';
358
+ return '<!doctype html>\n' + clone.outerHTML;
359
+ }
360
+ function download(blob, name) {
361
+ var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name;
362
+ document.body.appendChild(a); a.click(); setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1500);
363
+ }
364
+ // Opened through `tadforce review` / `build --open`: the local server writes back to the same file.
365
+ var served = location.protocol === 'http:' && location.hostname === '127.0.0.1';
366
+ async function save() {
367
+ var html = serialize();
368
+ if (served) {
369
+ try {
370
+ var r = await fetch(location.pathname, { method: 'PUT', headers: { 'content-type': 'text/html; charset=utf-8' }, body: html });
371
+ if (r.ok) { setDirty(false); toast(T('savedServer', { name: fileName() }), 3500); return; }
372
+ } catch (e) { /* server stopped: fall back to the file picker */ }
373
+ }
374
+ if (window.showSaveFilePicker) {
375
+ try {
376
+ if (!fileHandle) {
377
+ toast(T('pickFile'), 4000);
378
+ fileHandle = await window.showSaveFilePicker({ suggestedName: fileName(), types: [{ description: T('htmlPage'), accept: { 'text/html': ['.html'] } }] });
379
+ }
380
+ var w = await fileHandle.createWritable(); await w.write(html); await w.close();
381
+ setDirty(false); toast(T('saved', { name: fileHandle.name })); return;
382
+ } catch (e) { if (e && e.name === 'AbortError') return; fileHandle = null; }
383
+ }
384
+ download(new Blob([html], { type: 'text/html;charset=utf-8' }), fileName());
385
+ setDirty(false); toast(T('downloaded', { name: fileName() }), 5000);
386
+ }
387
+ btnSave.addEventListener('click', save);
388
+ document.addEventListener('keydown', function (e) {
389
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') { e.preventDefault(); save(); }
390
+ if (e.key === 'Escape') {
391
+ if (ov.classList.contains('on')) cancelCapture();
392
+ else if (commentMode) setCommentMode(false);
393
+ else if (editing && !pop.classList.contains('on')) setEditing(false);
394
+ }
395
+ });
396
+ window.addEventListener('beforeunload', function (e) { if (dirty) { e.preventDefault(); e.returnValue = ''; } });
397
+
398
+ /* ---------- Print / export ---------- */
399
+ btnPrint.addEventListener('click', function (e) { e.stopPropagation(); menu.classList.toggle('on'); });
400
+ document.addEventListener('click', function (e) { if (!menu.contains(e.target)) menu.classList.remove('on'); });
401
+ $('doPdf').addEventListener('click', function () { menu.classList.remove('on'); if (editing) setEditing(false); setTimeout(function () { window.print(); }, 50); });
402
+ $('doDocx').addEventListener('click', function () { menu.classList.remove('on'); exportDocx(); });
403
+
404
+ function svgToPng(svg, maxW) {
405
+ return new Promise(function (resolve) {
406
+ var vb = svg.viewBox && svg.viewBox.baseVal, bb = svg.getBoundingClientRect();
407
+ var w = (vb && vb.width) || bb.width || 800, h = (vb && vb.height) || bb.height || 400;
408
+ var c = svg.cloneNode(true);
409
+ c.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); c.setAttribute('width', w); c.setAttribute('height', h);
410
+ c.style.maxWidth = ''; c.removeAttribute('style');
411
+ if (svg.getAttribute('style') && /font-family/.test(svg.getAttribute('style'))) c.setAttribute('style', 'font-family:Segoe UI,Arial,sans-serif');
412
+ var url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(new XMLSerializer().serializeToString(c));
413
+ var img = new Image();
414
+ img.onload = function () {
415
+ var scale = Math.min(1.6, 1500 / w), cv = document.createElement('canvas'); cv.width = Math.round(w * scale); cv.height = Math.round(h * scale);
416
+ var ctx = cv.getContext('2d'); ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, cv.width, cv.height); ctx.drawImage(img, 0, 0, cv.width, cv.height);
417
+ var dw = Math.min(maxW, w); resolve({ src: cv.toDataURL('image/png'), w: Math.round(dw), h: Math.round(dw * h / w) });
418
+ };
419
+ img.onerror = function () { resolve(null); };
420
+ img.src = url;
421
+ });
422
+ }
423
+ function imgToPng(src, maxW) {
424
+ return new Promise(function (resolve) {
425
+ var img = new Image();
426
+ img.onload = function () {
427
+ var w = img.naturalWidth || 300, h = img.naturalHeight || 300, cv = document.createElement('canvas'); cv.width = w; cv.height = h;
428
+ var ctx = cv.getContext('2d'); ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, w, h); ctx.drawImage(img, 0, 0, w, h);
429
+ var dw = Math.min(maxW, w); resolve({ src: cv.toDataURL('image/png'), w: dw, h: Math.round(dw * h / w) });
430
+ };
431
+ img.onerror = function () { resolve(null); }; img.src = src;
432
+ });
433
+ }
434
+ function el(tag, html) { var e = document.createElement(tag); if (html !== undefined) e.innerHTML = html; return e; }
435
+ function tableOf(rows, cls) {
436
+ var t = el('table'); t.className = cls || '';
437
+ rows.forEach(function (r) { var tr = t.insertRow(); r.forEach(function (c) { var td = tr.insertCell(); td.innerHTML = c; }); });
438
+ return t;
439
+ }
440
+
441
+ async function exportDocx() {
442
+ if (typeof htmlDocx === 'undefined') { toast(T('docxMissing')); return; }
443
+ if (editing) setEditing(false);
444
+ toast(T('docxRunning'), 8000);
445
+ var live = main.querySelectorAll('svg'), src = main.cloneNode(true), cl = src.querySelectorAll('svg');
446
+ for (var i = 0; i < live.length; i++) {
447
+ var png = await svgToPng(live[i], 620);
448
+ if (png && cl[i]) { var im = el('img'); im.src = png.src; im.width = png.w; im.height = png.h; cl[i].parentNode.replaceChild(im, cl[i]); }
449
+ }
450
+ var imgs = src.querySelectorAll('.cover-logos img');
451
+ for (var k = 0; k < imgs.length; k++) { var p = await imgToPng(imgs[k].src, 110); if (p) { imgs[k].src = p.src; imgs[k].width = p.w; imgs[k].height = p.h; } }
452
+ src.querySelectorAll('.anchor, script, .doc-foot, .pending').forEach(function (e) { e.remove(); });
453
+ src.querySelectorAll('mark.tad-cmt, .tad-pin').forEach(unwrap);
454
+ src.querySelectorAll('[data-edited]').forEach(function (e) { e.removeAttribute('data-edited'); });
455
+ src.querySelectorAll('.tw').forEach(function (w) { w.replaceWith(w.firstElementChild); });
456
+ src.querySelectorAll('.kpis').forEach(function (k) {
457
+ var cells = Array.prototype.map.call(k.querySelectorAll('.kpi'), function (x) { return '<p class="kb">' + x.querySelector('b').innerHTML + '</p><p class="ks">' + x.querySelector('span').innerHTML + '</p>'; });
458
+ var rows = []; for (var j = 0; j < cells.length; j += 4) rows.push(cells.slice(j, j + 4));
459
+ k.replaceWith(tableOf(rows, 'kpit'));
460
+ });
461
+ src.querySelectorAll('.cards').forEach(function (k) {
462
+ var cells = Array.prototype.map.call(k.querySelectorAll('.card'), function (x) { return x.innerHTML; });
463
+ var rows = []; for (var j = 0; j < cells.length; j += 2) rows.push(cells.slice(j, j + 2));
464
+ k.replaceWith(tableOf(rows, 'cardt'));
465
+ });
466
+ src.querySelectorAll('.layers').forEach(function (k) {
467
+ k.replaceWith(tableOf(Array.prototype.map.call(k.querySelectorAll('.layer'), function (l) {
468
+ return ['<b>' + l.querySelector('.ln').textContent + '</b>', Array.prototype.map.call(l.querySelectorAll('.li span'), function (s) { return s.textContent; }).join(' · ')];
469
+ }), 'layert'));
470
+ });
471
+ src.querySelectorAll('.legend').forEach(function (k) { k.replaceWith(el('p', Array.prototype.map.call(k.querySelectorAll('.lg'), function (s) { return '■ ' + s.textContent; }).join(' &nbsp; '))); });
472
+ src.querySelectorAll('aside.callout').forEach(function (k) { var t = tableOf([[k.innerHTML]], 'calloutt'); k.replaceWith(t); });
473
+ src.querySelectorAll('.cover .meta').forEach(function (k) {
474
+ k.replaceWith(tableOf(Array.prototype.map.call(k.querySelectorAll('div'), function (d) { return ['<b>' + d.querySelector('b').textContent + '</b>', d.querySelector('span').innerHTML]; }), 'metat'));
475
+ });
476
+ src.querySelectorAll('.cover-logos').forEach(function (k) { k.replaceWith(tableOf([[k.children[0] ? k.children[0].outerHTML : '', k.children[1] ? '<p style="text-align:right">' + k.children[1].outerHTML + '</p>' : '']], 'logot')); });
477
+ src.querySelectorAll('figure.diagram').forEach(function (f) {
478
+ var d = el('div'); d.innerHTML = '<p class="fig">' + (f.querySelector('img') ? f.querySelector('img').outerHTML : '') + '</p><p class="figcap">' + f.querySelector('figcaption').innerHTML + '</p>'; f.replaceWith(d);
479
+ });
480
+ src.querySelectorAll('.fig-body').forEach(function (f) { var d = el('p'); d.className = 'fig'; d.innerHTML = f.innerHTML; f.replaceWith(d); });
481
+ var ink = BRAND.primary || '#0B2545', th = BRAND.tableHeader || '#0f2a4d', acc = BRAND.accent2 || '#7B3FE4';
482
+ var css = 'body{font-family:Calibri,"Segoe UI",Arial,sans-serif;font-size:10pt;color:#18212d}' +
483
+ 'h1{font-size:18pt;color:' + ink + ';page-break-before:always;margin:0 0 8pt}h2{font-size:13.5pt;color:' + ink + ';margin:14pt 0 6pt}h3{font-size:11.5pt;margin:10pt 0 4pt}h4{font-size:10.5pt;color:' + ink + ';margin:0 0 3pt}' +
484
+ '.num{color:' + acc + '}p,li{margin:0 0 5pt;line-height:1.35}table{border-collapse:collapse;width:100%;margin:4pt 0 9pt}' +
485
+ 'th{background:' + th + ';color:#ffffff;font-weight:bold;border:1px solid ' + th + ';padding:2pt 4pt;font-size:8.5pt;text-align:left;line-height:1.15}td{border:1px solid #cfd8e5;padding:2pt 4pt;font-size:8.5pt;vertical-align:top;line-height:1.15}td p{margin:0 0 2pt}' +
486
+ 'code{font-family:Consolas,monospace;font-size:8pt;color:#3b2a8f}th code{color:#ffffff}.fig{text-align:center;margin:6pt 0 2pt}.figcap{text-align:center;font-style:italic;color:#5a6878;font-size:8.5pt;margin-bottom:10pt}' +
487
+ 'table.kpit td,table.cardt td{border:1px solid #d6dde8;background:#f7f9fc;padding:6pt}.kb{font-size:16pt;font-weight:bold;color:' + ink + ';margin:0}.ks{font-size:8.5pt;color:#5a6878;margin:0}' +
488
+ 'table.layert td{background:#E8F1FF;border:1px solid #c9d8f2}table.calloutt td{background:#f3efff;border:0;border-left:4px solid #8b5cf6;padding:6pt 8pt}.ct{font-weight:bold;color:#6d3fe0;font-size:8.5pt;text-transform:uppercase}' +
489
+ 'table.metat td{border:1px solid #d6dde8}table.logot td{border:0}.kicker{color:' + acc + ';font-weight:bold;font-size:10pt;text-transform:uppercase;letter-spacing:1pt}.ctitle{font-size:26pt;color:' + ink + ';page-break-before:auto}.csub{font-size:13pt;color:#5a6878}.conf{font-size:8.5pt;color:#5a6878}.b{font-weight:bold}';
490
+ var cover = src.querySelector('.cover'), doc = src.querySelector('article.doc');
491
+ var body = (cover ? cover.innerHTML : '') + (doc ? doc.innerHTML : '');
492
+ var html = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>' + document.title + '</title><style>' + css + '</style></head><body>' + body + '</body></html>';
493
+ try {
494
+ var blob = htmlDocx.asBlob(html, { orientation: 'portrait', margins: { top: 1000, right: 900, bottom: 1000, left: 900 } });
495
+ download(blob, fileName().replace(/\.html?$/i, '') + '.docx');
496
+ toast(T('docxDone'), 4000);
497
+ } catch (e) { toast(T('docxFailed', { error: e.message }), 5000); }
498
+ }
499
+
500
+ comments.forEach(function (c) { if (c.status === 'resolved') marksOf(c.id).forEach(function (m) { m.classList.add('resolved'); }); });
501
+ render();
502
+ })();