@alfacapital-tech/c4builder 0.9.0-rc.1 → 0.9.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.MD +4 -2
- package/dist/cli/agent.js +21 -0
- package/dist/cli/commands/check.js +9 -6
- package/dist/cli/commands/feedback.js +45 -0
- package/dist/cli/commands/site.comments.js +318 -77
- package/dist/cli/commands/site.i18n.js +88 -0
- package/dist/cli/commands/site.js +152 -8
- package/dist/cli/dispatch.js +15 -4
- package/dist/config/defaults.js +2 -1
- package/dist/config/schema.js +2 -0
- package/dist/core/compose/markdown.js +3 -2
- package/dist/core/feedback.js +59 -0
- package/dist/core/render/bpmnrenderer.js +3 -28
- package/dist/core/render/diagrams.js +22 -10
- package/dist/core/render/mermaidrenderer.js +495 -0
- package/dist/core/render/textmetrics.js +36 -0
- package/dist/core/scan/fences.js +2 -2
- package/dist/core/scan/tree.js +14 -9
- package/dist/plugins/openspec/render.js +1 -1
- package/package.json +2 -1
- package/template/AGENTS.md +16 -4
- package/template/src/6 Mermaid Example/flow.mmd +6 -0
- package/template/src/6 Mermaid Example/mermaid.md +16 -0
|
@@ -1,17 +1,39 @@
|
|
|
1
|
+
import { UI_STRINGS } from "./site.i18n.js";
|
|
1
2
|
export const COMMENTS_PATH = '/__c4builder/comments';
|
|
3
|
+
export const FEEDBACK_PATH = '/__c4builder/feedback';
|
|
4
|
+
export const FEEDBACK_HEADER = 'X-C4builder-Feedback';
|
|
2
5
|
const CSS = [
|
|
3
|
-
'.c4b-
|
|
6
|
+
'.c4b-cbtns{position:absolute;z-index:1001;white-space:nowrap}',
|
|
7
|
+
'.c4b-cbtns button{margin-right:4px;padding:4px 10px;border:1px solid #42b983;border-radius:4px;background:#fff;color:#2c3e50;font:13px/1.4 sans-serif;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,.15)}',
|
|
8
|
+
'.c4b-cbtns button.c4b-fb{border-color:#3b82f6}',
|
|
4
9
|
'.c4b-cpop{position:absolute;z-index:1002;width:380px;max-width:calc(100vw - 32px);padding:10px;border:1px solid #ddd;border-radius:6px;background:#fff;color:#2c3e50;font:13px/1.4 sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.2)}',
|
|
5
10
|
'.c4b-cpop blockquote{margin:0 0 8px;padding:4px 8px;border-left:3px solid #42b983;background:#f8f8f8;max-height:80px;overflow:auto;font-size:12px;white-space:pre-wrap}',
|
|
6
|
-
'.c4b-cpop textarea{width:100%;box-sizing:border-box;min-height:70px;padding:6px;border:1px solid #ccc;border-radius:4px;font:13px/1.4 sans-serif;resize:vertical}',
|
|
11
|
+
'.c4b-cpop textarea,.c4b-review textarea{width:100%;box-sizing:border-box;min-height:70px;padding:6px;border:1px solid #ccc;border-radius:4px;font:13px/1.4 sans-serif;resize:vertical}',
|
|
7
12
|
'.c4b-cpop .c4b-row{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:8px}',
|
|
8
13
|
'.c4b-cpop small{color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'.c4b-cpop
|
|
14
|
+
// Кнопки поповера не сжимаются: длинную подсказку обрезает многоточие, а не кнопки уходят
|
|
15
|
+
// на вторую строку (русские подписи и длинные названия разделов).
|
|
16
|
+
'.c4b-cpop .c4b-row>span{flex:none}',
|
|
17
|
+
'.c4b-cpop button,.c4b-review button{padding:4px 12px;border:1px solid #ccc;border-radius:4px;background:#fff;color:#2c3e50;cursor:pointer;font:13px sans-serif}',
|
|
18
|
+
'.c4b-cpop button.c4b-primary,.c4b-review button.c4b-primary{border-color:#42b983;background:#42b983;color:#fff}',
|
|
19
|
+
'.c4b-cpop button:disabled,.c4b-review button:disabled{opacity:.6;cursor:default}',
|
|
20
|
+
// Панель review — ниже кнопок у выделения и поповера: она прибита к низу экрана и иначе
|
|
21
|
+
// перекрывает их у выделения в нижних строках.
|
|
22
|
+
'.c4b-review{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);z-index:1000;width:520px;max-width:calc(100vw - 32px);box-sizing:border-box;padding:8px 12px;border:1px solid #3b82f6;border-radius:6px;background:#fff;color:#2c3e50;font:13px/1.4 sans-serif;box-shadow:0 2px 8px rgba(0,0,0,.2)}',
|
|
23
|
+
'.c4b-review .c4b-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px}',
|
|
24
|
+
'.c4b-review .c4b-row small{flex:1;color:#888;white-space:nowrap}',
|
|
25
|
+
// Кнопки панели переносятся одной группой: иначе при нехватке места часть уезжает на
|
|
26
|
+
// вторую строку, а Discard остаётся один в первой.
|
|
27
|
+
'.c4b-review .c4b-acts{display:flex;gap:6px;margin-left:auto}',
|
|
28
|
+
'.c4b-review button.c4b-link{padding:0;border:0;background:none;color:#2c3e50;font-weight:700;text-decoration:underline dotted}',
|
|
29
|
+
'.c4b-review ul{margin:6px 0;padding:0;list-style:none;max-height:40vh;overflow:auto}',
|
|
30
|
+
'.c4b-review li{padding:3px 0;border-top:1px solid #eee;overflow-wrap:anywhere}',
|
|
31
|
+
'.c4b-review textarea{margin-top:6px}',
|
|
32
|
+
'.c4b-review .c4b-del{opacity:1}',
|
|
12
33
|
'.c4b-ctoast{position:fixed;left:16px;bottom:16px;z-index:1003;max-width:420px;padding:8px 14px;border-radius:6px;background:#2c3e50;color:#fff;font:13px/1.4 sans-serif;box-shadow:0 2px 8px rgba(0,0,0,.3)}',
|
|
13
34
|
'.c4b-ctoast.c4b-err{background:#c0392b}',
|
|
14
35
|
'mark.c4b-hl{background:#fff3a0;color:inherit;padding:0;border-radius:2px}',
|
|
36
|
+
'mark.c4b-dhl{background:#cfe3ff;color:inherit;padding:0;border-radius:2px}',
|
|
15
37
|
'sup.c4b-ref{font-size:.7em;line-height:0;margin-left:1px;user-select:none}',
|
|
16
38
|
'sup.c4b-ref a{color:#b58900;text-decoration:none;font-weight:700;cursor:pointer}',
|
|
17
39
|
'.c4b-qrow{transition:background .3s}',
|
|
@@ -21,22 +43,47 @@ const CSS = [
|
|
|
21
43
|
'.c4b-del:hover{color:#fff;background:#c0392b;border-color:#c0392b}',
|
|
22
44
|
'.c4b-del:disabled{opacity:.5;cursor:default}'
|
|
23
45
|
].join('');
|
|
24
|
-
const
|
|
46
|
+
const jsValue = (v) => JSON.stringify(v)
|
|
25
47
|
.replace(/</g, '\\u003c')
|
|
26
48
|
.replace(/[\u0080-]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
27
|
-
|
|
49
|
+
// Начальное состояние агентского режима и ключ проекта вставляет сервер, дальше его обновляют
|
|
50
|
+
// события `c4builder:agent` от сниппета livereload (change site-agent-feedback, design D4, D8).
|
|
51
|
+
// Язык выбирает сам скрипт: при `auto` — по navigator.languages (change site-ui-language, D1).
|
|
52
|
+
export const commentsSnippet = (heading, { agent = false, listening = false, project = '', language = 'auto' } = {}) => `
|
|
28
53
|
<script>
|
|
29
54
|
(function () {
|
|
30
55
|
if (window.__c4builderComments) return;
|
|
31
56
|
window.__c4builderComments = true;
|
|
32
57
|
var ENDPOINT = '${COMMENTS_PATH}';
|
|
33
|
-
var
|
|
58
|
+
var FEEDBACK = '${FEEDBACK_PATH}';
|
|
59
|
+
var HEADING = ${jsValue(heading)};
|
|
34
60
|
var TOAST_KEY = '__c4builderCommentToast';
|
|
61
|
+
var DRAFT_KEY = '__c4builderReview:' + ${jsValue(project)};
|
|
62
|
+
var AGENT = { on: ${agent}, listening: ${listening} };
|
|
35
63
|
var BLOCKS = 'p,li,td,th,h1,h2,h3,h4,h5,h6,pre,blockquote,dd,dt,div,section,details,summary';
|
|
36
64
|
var style = document.createElement('style');
|
|
37
|
-
style.textContent = ${
|
|
65
|
+
style.textContent = ${jsValue(CSS)};
|
|
38
66
|
document.head.appendChild(style);
|
|
39
67
|
|
|
68
|
+
var STRINGS = ${jsValue(UI_STRINGS)};
|
|
69
|
+
var has = function (lang) { return Object.prototype.hasOwnProperty.call(STRINGS, lang); };
|
|
70
|
+
var LANG = (function (pref) {
|
|
71
|
+
if (has(pref)) return pref;
|
|
72
|
+
var prefs = [].concat(navigator.languages || [], navigator.language || []);
|
|
73
|
+
for (var i = 0; i < prefs.length; i++) {
|
|
74
|
+
var tag = String(prefs[i]).toLowerCase().split('-')[0];
|
|
75
|
+
if (has(tag)) return tag;
|
|
76
|
+
}
|
|
77
|
+
return 'en';
|
|
78
|
+
})(${jsValue(language)});
|
|
79
|
+
var plural = new Intl.PluralRules(LANG);
|
|
80
|
+
var t = function (key, vars) {
|
|
81
|
+
vars = vars || {};
|
|
82
|
+
var s = STRINGS[LANG][key];
|
|
83
|
+
if (typeof s === 'object') s = s[plural.select(vars.n)];
|
|
84
|
+
return s.replace(/\\{(\\w+)\\}/g, function (m, k) { return k in vars ? String(vars[k]) : m; });
|
|
85
|
+
};
|
|
86
|
+
|
|
40
87
|
var collapse = function (s) { return String(s).replace(/\\s+/g, ' ').trim(); };
|
|
41
88
|
var same = function (a, b) { return collapse(a).toLowerCase() === collapse(b).toLowerCase(); };
|
|
42
89
|
|
|
@@ -109,7 +156,7 @@ export const commentsSnippet = (heading) => `
|
|
|
109
156
|
for (var r = 0; r < roots.length; r++) walk(roots[r]);
|
|
110
157
|
return { text: chars.join(''), map: map };
|
|
111
158
|
};
|
|
112
|
-
var wrapRange = function (idx, from, to, title) {
|
|
159
|
+
var wrapRange = function (idx, from, to, title, cls) {
|
|
113
160
|
var first = null, last = null, i;
|
|
114
161
|
for (i = from; i < to && !first; i++) first = idx.map[i];
|
|
115
162
|
for (i = to - 1; i >= from && !last; i--) last = idx.map[i];
|
|
@@ -125,9 +172,10 @@ export const commentsSnippet = (heading) => `
|
|
|
125
172
|
var end = n === last.node ? last.offset + 1 : n.nodeValue.length;
|
|
126
173
|
if (end < n.nodeValue.length) n.splitText(end);
|
|
127
174
|
var mid = start > 0 ? n.splitText(start) : n;
|
|
128
|
-
|
|
175
|
+
// Overlapping quotes: the number link of a question is not part of the text.
|
|
176
|
+
if (!mid.nodeValue.trim() || mid.parentNode.closest('sup.c4b-ref')) continue;
|
|
129
177
|
var mark = document.createElement('mark');
|
|
130
|
-
mark.className =
|
|
178
|
+
mark.className = cls;
|
|
131
179
|
mark.title = title;
|
|
132
180
|
mid.parentNode.insertBefore(mark, mid);
|
|
133
181
|
mark.appendChild(mid);
|
|
@@ -135,6 +183,20 @@ export const commentsSnippet = (heading) => `
|
|
|
135
183
|
}
|
|
136
184
|
return marks;
|
|
137
185
|
};
|
|
186
|
+
var tryIn = function (idx, quote, cls, title) {
|
|
187
|
+
var at = idx.text.indexOf(quote);
|
|
188
|
+
var marks = at < 0 ? null : wrapRange(idx, at, at + quote.length, title, cls);
|
|
189
|
+
return marks && marks.length ? marks : null;
|
|
190
|
+
};
|
|
191
|
+
// The quote is searched in its own section first: the same words may repeat on the page.
|
|
192
|
+
var locate = function (root, q, skip, cls, title) {
|
|
193
|
+
var marks = null;
|
|
194
|
+
if (q.section) {
|
|
195
|
+
var sh = findHeading(root, q.section);
|
|
196
|
+
if (sh && skip.indexOf(sh) < 0) marks = tryIn(textIndex(sectionOf(sh), skip), q.quote, cls, title);
|
|
197
|
+
}
|
|
198
|
+
return marks || tryIn(textIndex([root], skip), q.quote, cls, title);
|
|
199
|
+
};
|
|
138
200
|
var flash = function (row) {
|
|
139
201
|
row.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
140
202
|
row.classList.add('c4b-qflash');
|
|
@@ -168,28 +230,23 @@ export const commentsSnippet = (heading) => `
|
|
|
168
230
|
del.type = 'button';
|
|
169
231
|
del.className = 'c4b-del';
|
|
170
232
|
del.textContent = '\\u00d7';
|
|
171
|
-
del.title = '
|
|
233
|
+
del.title = t('removeQuestion');
|
|
172
234
|
del.addEventListener('click', function (e) {
|
|
173
235
|
e.preventDefault();
|
|
174
236
|
e.stopPropagation();
|
|
175
237
|
del.disabled = true;
|
|
176
238
|
send({ page: pageFile(), number: Number(item.number) || null, text: collapse(item.text) }, 'DELETE').then(function (r) {
|
|
177
|
-
var msg = '
|
|
239
|
+
var msg = t('removed', { file: r.file });
|
|
178
240
|
try { sessionStorage.setItem(TOAST_KEY, msg); } catch (e) {}
|
|
179
241
|
toast(msg);
|
|
180
242
|
}, function (err) {
|
|
181
243
|
del.disabled = false;
|
|
182
|
-
|
|
244
|
+
failure('removeFailed')(err);
|
|
183
245
|
});
|
|
184
246
|
});
|
|
185
247
|
item.cell.appendChild(del);
|
|
186
248
|
};
|
|
187
249
|
var skip = [h].concat(section);
|
|
188
|
-
var tryIn = function (idx, q) {
|
|
189
|
-
var at = idx.text.indexOf(q.quote);
|
|
190
|
-
var marks = at < 0 ? null : wrapRange(idx, at, at + q.quote.length, q.comment);
|
|
191
|
-
return marks && marks.length ? marks : null;
|
|
192
|
-
};
|
|
193
250
|
items.forEach(function (item) {
|
|
194
251
|
item.row.classList.add('c4b-qrow');
|
|
195
252
|
item.row.id = item.row.id || 'c4b-q-' + item.number;
|
|
@@ -197,11 +254,7 @@ export const commentsSnippet = (heading) => `
|
|
|
197
254
|
// Index is rebuilt per try: wrapping splits text nodes and stales the previous map.
|
|
198
255
|
var q = null, marks = null;
|
|
199
256
|
parseQuestions(item.text).some(function (c) {
|
|
200
|
-
|
|
201
|
-
var sh = findHeading(root, c.section);
|
|
202
|
-
if (sh && skip.indexOf(sh) < 0) marks = tryIn(textIndex(sectionOf(sh), skip), c);
|
|
203
|
-
}
|
|
204
|
-
marks = marks || tryIn(textIndex([root], skip), c);
|
|
257
|
+
marks = locate(root, c, skip, 'c4b-hl', c.comment);
|
|
205
258
|
q = marks ? c : null;
|
|
206
259
|
return !!marks;
|
|
207
260
|
});
|
|
@@ -219,7 +272,12 @@ export const commentsSnippet = (heading) => `
|
|
|
219
272
|
lastMark.parentNode.insertBefore(sup, lastMark.nextSibling);
|
|
220
273
|
});
|
|
221
274
|
};
|
|
222
|
-
var schedule = function () {
|
|
275
|
+
var schedule = function () {
|
|
276
|
+
setTimeout(function () {
|
|
277
|
+
try { highlight(); } catch (e) {}
|
|
278
|
+
try { highlightDraft(); } catch (e) {}
|
|
279
|
+
}, 50);
|
|
280
|
+
};
|
|
223
281
|
var docsifyVm = null;
|
|
224
282
|
if (window.$docsify) {
|
|
225
283
|
window.$docsify.plugins = [].concat(window.$docsify.plugins || [], function (hook, vm) {
|
|
@@ -230,12 +288,24 @@ export const commentsSnippet = (heading) => `
|
|
|
230
288
|
window.addEventListener('hashchange', function () { setTimeout(schedule, 300); });
|
|
231
289
|
window.addEventListener('load', function () { setTimeout(schedule, 500); });
|
|
232
290
|
|
|
233
|
-
var
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
291
|
+
var button = function (label, onClick, cls) {
|
|
292
|
+
var b = document.createElement('button');
|
|
293
|
+
b.type = 'button';
|
|
294
|
+
b.textContent = label;
|
|
295
|
+
if (cls) b.className = cls;
|
|
296
|
+
if (onClick) b.addEventListener('click', onClick);
|
|
297
|
+
return b;
|
|
298
|
+
};
|
|
299
|
+
var btns = document.createElement('div');
|
|
300
|
+
btns.className = 'c4b-cbtns';
|
|
301
|
+
btns.hidden = true;
|
|
302
|
+
var fbBtn = button(t('feedback'), null, 'c4b-fb');
|
|
303
|
+
fbBtn.title = t('feedbackTitle');
|
|
304
|
+
var qBtn = button(t('question'));
|
|
305
|
+
qBtn.title = t('questionTitle', { heading: HEADING });
|
|
306
|
+
btns.appendChild(fbBtn);
|
|
307
|
+
btns.appendChild(qBtn);
|
|
308
|
+
document.body.appendChild(btns);
|
|
239
309
|
var pop = null;
|
|
240
310
|
|
|
241
311
|
var selection = function () {
|
|
@@ -273,9 +343,9 @@ export const commentsSnippet = (heading) => `
|
|
|
273
343
|
var update = function () {
|
|
274
344
|
if (pop) return;
|
|
275
345
|
var s = selection();
|
|
276
|
-
if (!s) {
|
|
277
|
-
|
|
278
|
-
place(
|
|
346
|
+
if (!s) { btns.hidden = true; return; }
|
|
347
|
+
btns.hidden = false;
|
|
348
|
+
place(btns, anchorRect(s.range));
|
|
279
349
|
};
|
|
280
350
|
document.addEventListener('selectionchange', function () { setTimeout(update, 0); });
|
|
281
351
|
|
|
@@ -292,8 +362,8 @@ export const commentsSnippet = (heading) => `
|
|
|
292
362
|
if (!p) return (window.$docsify && window.$docsify.homepage) || 'README.md';
|
|
293
363
|
return /\\.md$/i.test(p) ? p : p + '.md';
|
|
294
364
|
};
|
|
295
|
-
var send = function (body, method) {
|
|
296
|
-
return fetch(ENDPOINT, {
|
|
365
|
+
var send = function (body, method, url) {
|
|
366
|
+
return fetch(url || ENDPOINT, {
|
|
297
367
|
method: method || 'POST',
|
|
298
368
|
headers: { 'Content-Type': 'application/json' },
|
|
299
369
|
body: JSON.stringify(body)
|
|
@@ -304,75 +374,246 @@ export const commentsSnippet = (heading) => `
|
|
|
304
374
|
});
|
|
305
375
|
});
|
|
306
376
|
};
|
|
377
|
+
var failure = function (key) {
|
|
378
|
+
return function (err) { toast(t(key, { error: err && err.message ? err.message : err }), true); };
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Feedback to the agent: "Delivered" only after the server handed the batch to
|
|
382
|
+
// "c4builder feedback"; the event may outrun the POST response, hence both maps.
|
|
383
|
+
var delivered = {}, pending = {};
|
|
384
|
+
var sendBatch = function (decision, comments, general) {
|
|
385
|
+
return send({ decision: decision, comments: comments, general: general }, 'POST', FEEDBACK).then(function (r) {
|
|
386
|
+
if (delivered[r.id]) return toast(t('delivered'));
|
|
387
|
+
pending[r.id] = true;
|
|
388
|
+
toast(t(r.queued ? 'queued' : 'sent'));
|
|
389
|
+
});
|
|
390
|
+
};
|
|
391
|
+
window.addEventListener('c4builder:delivered', function (e) {
|
|
392
|
+
var id = e.detail && e.detail.id;
|
|
393
|
+
delivered[id] = true;
|
|
394
|
+
if (pending[id]) { delete pending[id]; toast(t('delivered')); }
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Review draft lives in localStorage per project: it survives livereload and a closed tab;
|
|
398
|
+
// without storage it lives until the page reloads.
|
|
399
|
+
var memDraft = [];
|
|
400
|
+
var draft = function () {
|
|
401
|
+
try {
|
|
402
|
+
var v = JSON.parse(localStorage.getItem(DRAFT_KEY) || '[]');
|
|
403
|
+
return Array.isArray(v) ? v : [];
|
|
404
|
+
} catch (e) { return memDraft; }
|
|
405
|
+
};
|
|
406
|
+
var saveDraft = function (items) {
|
|
407
|
+
memDraft = items;
|
|
408
|
+
try {
|
|
409
|
+
if (items.length) localStorage.setItem(DRAFT_KEY, JSON.stringify(items));
|
|
410
|
+
else localStorage.removeItem(DRAFT_KEY);
|
|
411
|
+
} catch (e) {}
|
|
412
|
+
renderPanel();
|
|
413
|
+
highlightDraft();
|
|
414
|
+
};
|
|
415
|
+
var highlightDraft = function () {
|
|
416
|
+
var root = content();
|
|
417
|
+
if (!root) return;
|
|
418
|
+
var marks = root.querySelectorAll('mark.c4b-dhl');
|
|
419
|
+
for (var i = 0; i < marks.length; i++) {
|
|
420
|
+
var m = marks[i], parent = m.parentNode;
|
|
421
|
+
while (m.firstChild) parent.insertBefore(m.firstChild, m);
|
|
422
|
+
parent.removeChild(m);
|
|
423
|
+
parent.normalize();
|
|
424
|
+
}
|
|
425
|
+
var page = pageFile();
|
|
426
|
+
var h = findHeading(root, HEADING);
|
|
427
|
+
var skip = h ? [h].concat(sectionOf(h)) : [];
|
|
428
|
+
draft().forEach(function (it) {
|
|
429
|
+
if (it.page === page) locate(root, { quote: collapse(it.quote), section: it.section }, skip, 'c4b-dhl', it.comment);
|
|
430
|
+
});
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
var panel = document.createElement('div');
|
|
434
|
+
panel.className = 'c4b-review';
|
|
435
|
+
var expanded = false, busy = false;
|
|
436
|
+
var count = button('', function () { expanded = !expanded; renderPanel(); }, 'c4b-link');
|
|
437
|
+
var listen = document.createElement('small');
|
|
438
|
+
listen.className = 'c4b-listen';
|
|
439
|
+
var list = document.createElement('ul');
|
|
440
|
+
var general = document.createElement('textarea');
|
|
441
|
+
general.placeholder = t('generalPlaceholder');
|
|
442
|
+
general.hidden = true;
|
|
443
|
+
var finish = function (decision) {
|
|
444
|
+
var items = draft(), ids = items.map(function (it) { return it.id; });
|
|
445
|
+
busy = true;
|
|
446
|
+
renderPanel();
|
|
447
|
+
sendBatch(decision, items, general.hidden ? '' : general.value.trim()).then(function () {
|
|
448
|
+
general.value = '';
|
|
449
|
+
general.hidden = true;
|
|
450
|
+
expanded = false;
|
|
451
|
+
saveDraft(draft().filter(function (it) { return ids.indexOf(it.id) < 0; }));
|
|
452
|
+
}, failure('reviewFailed')).then(function () { busy = false; renderPanel(); });
|
|
453
|
+
};
|
|
454
|
+
var discard = button(t('discard'), function () {
|
|
455
|
+
var n = draft().length;
|
|
456
|
+
if (n && !window.confirm(t('discardConfirm', { n: n }))) return;
|
|
457
|
+
general.value = '';
|
|
458
|
+
general.hidden = true;
|
|
459
|
+
saveDraft([]);
|
|
460
|
+
});
|
|
461
|
+
// One finishing action, two labels: on an empty draft "Approve" does not read as "finish".
|
|
462
|
+
var approve = button('', function () { finish('approved'); });
|
|
463
|
+
var submit = button(t('submitReview'), function () {
|
|
464
|
+
if (!general.hidden) return finish('submitted');
|
|
465
|
+
general.hidden = false;
|
|
466
|
+
general.focus();
|
|
467
|
+
}, 'c4b-primary');
|
|
468
|
+
general.addEventListener('keydown', function (e) {
|
|
469
|
+
if (e.key === 'Escape') { e.preventDefault(); general.hidden = true; }
|
|
470
|
+
else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); submit.click(); }
|
|
471
|
+
});
|
|
472
|
+
var head = document.createElement('div');
|
|
473
|
+
head.className = 'c4b-row';
|
|
474
|
+
var acts = document.createElement('span');
|
|
475
|
+
acts.className = 'c4b-acts';
|
|
476
|
+
[discard, approve, submit].forEach(function (el) { acts.appendChild(el); });
|
|
477
|
+
[count, listen, acts].forEach(function (el) { head.appendChild(el); });
|
|
478
|
+
panel.appendChild(head);
|
|
479
|
+
panel.appendChild(list);
|
|
480
|
+
panel.appendChild(general);
|
|
481
|
+
document.body.appendChild(panel);
|
|
482
|
+
var renderPanel = function () {
|
|
483
|
+
panel.hidden = !AGENT.on;
|
|
484
|
+
var items = draft(), pages = {}, empty = !items.length;
|
|
485
|
+
items.forEach(function (it) { pages[it.page] = true; });
|
|
486
|
+
var n = Object.keys(pages).length;
|
|
487
|
+
count.textContent = empty
|
|
488
|
+
? t('reviewEmpty')
|
|
489
|
+
: t('reviewCount', { comments: t('comments', { n: items.length }), pages: t('pages', { n: n }) });
|
|
490
|
+
count.disabled = empty;
|
|
491
|
+
list.textContent = '';
|
|
492
|
+
items.forEach(function (it) {
|
|
493
|
+
var li = document.createElement('li');
|
|
494
|
+
var q = collapse(it.quote);
|
|
495
|
+
li.textContent = it.page + ' \\u2014 \\u00ab' + (q.length > 60 ? q.slice(0, 59) + '\\u2026' : q) + '\\u00bb: ' + it.comment;
|
|
496
|
+
var del = button('\\u00d7', function () {
|
|
497
|
+
saveDraft(draft().filter(function (x) { return x.id !== it.id; }));
|
|
498
|
+
}, 'c4b-del');
|
|
499
|
+
del.title = t('removeFromReview');
|
|
500
|
+
li.appendChild(del);
|
|
501
|
+
list.appendChild(li);
|
|
502
|
+
});
|
|
503
|
+
list.hidden = !expanded || empty;
|
|
504
|
+
approve.textContent = t(empty ? 'done' : 'approve');
|
|
505
|
+
approve.title = t(empty ? 'doneTitle' : 'approveTitle');
|
|
506
|
+
approve.className = empty ? 'c4b-primary' : '';
|
|
507
|
+
discard.hidden = submit.hidden = empty;
|
|
508
|
+
discard.disabled = busy;
|
|
509
|
+
approve.disabled = busy;
|
|
510
|
+
submit.disabled = busy || empty;
|
|
511
|
+
};
|
|
512
|
+
var renderAgent = function () {
|
|
513
|
+
fbBtn.hidden = !AGENT.on;
|
|
514
|
+
renderPanel();
|
|
515
|
+
var text = t(AGENT.listening ? 'listening' : 'notListening');
|
|
516
|
+
var els = document.querySelectorAll('.c4b-listen');
|
|
517
|
+
for (var i = 0; i < els.length; i++) { els[i].textContent = text; els[i].title = text; }
|
|
518
|
+
};
|
|
519
|
+
window.addEventListener('c4builder:agent', function (e) {
|
|
520
|
+
var d = e.detail || {};
|
|
521
|
+
AGENT.on = !!d.on;
|
|
522
|
+
AGENT.listening = !!d.listening;
|
|
523
|
+
renderAgent();
|
|
524
|
+
});
|
|
525
|
+
window.addEventListener('storage', function (e) {
|
|
526
|
+
if (e.key === DRAFT_KEY) { renderPanel(); highlightDraft(); }
|
|
527
|
+
});
|
|
307
528
|
|
|
308
529
|
var close = function () {
|
|
309
530
|
if (pop) { pop.remove(); pop = null; }
|
|
310
|
-
|
|
531
|
+
btns.hidden = true;
|
|
311
532
|
};
|
|
312
|
-
var open = function (data, rect) {
|
|
533
|
+
var open = function (data, rect, feedback) {
|
|
313
534
|
close();
|
|
314
535
|
pop = document.createElement('div');
|
|
315
536
|
pop.className = 'c4b-cpop';
|
|
316
537
|
var quote = document.createElement('blockquote');
|
|
317
538
|
quote.textContent = data.quote;
|
|
318
539
|
var ta = document.createElement('textarea');
|
|
319
|
-
ta.placeholder =
|
|
540
|
+
ta.placeholder = t(feedback ? 'feedbackPlaceholder' : 'questionPlaceholder');
|
|
320
541
|
var row = document.createElement('div');
|
|
321
542
|
row.className = 'c4b-row';
|
|
322
543
|
var hint = document.createElement('small');
|
|
323
|
-
|
|
324
|
-
hint.
|
|
544
|
+
if (feedback) hint.className = 'c4b-listen';
|
|
545
|
+
else hint.textContent = hint.title = t('questionTo', { target: HEADING + (data.section ? ' / ' + data.section : '') });
|
|
325
546
|
var actions = document.createElement('span');
|
|
326
|
-
var
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
547
|
+
var buttons = [button(t('cancel'), close)];
|
|
548
|
+
var run = function (action) {
|
|
549
|
+
return function () {
|
|
550
|
+
var comment = ta.value.trim();
|
|
551
|
+
if (!comment) { ta.focus(); return; }
|
|
552
|
+
buttons.forEach(function (b) { b.disabled = true; });
|
|
553
|
+
action(comment).then(close, function (err) {
|
|
554
|
+
buttons.forEach(function (b) { b.disabled = false; });
|
|
555
|
+
failure(feedback ? 'feedbackFailed' : 'questionFailed')(err);
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
};
|
|
559
|
+
var item = function (comment) {
|
|
560
|
+
return { page: pageFile(), quote: data.quote, section: data.section, comment: comment };
|
|
561
|
+
};
|
|
562
|
+
var primary;
|
|
563
|
+
if (feedback) {
|
|
564
|
+
var add = button(t('addToReview'), run(function (c) {
|
|
565
|
+
var it = item(c);
|
|
566
|
+
it.id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
567
|
+
saveDraft(draft().concat([it]));
|
|
568
|
+
return Promise.resolve();
|
|
569
|
+
}));
|
|
570
|
+
var now = button(t('sendNow'), run(function (c) { return sendBatch('submitted', [item(c)], ''); }));
|
|
571
|
+
buttons.push(add, now);
|
|
572
|
+
// As in a GitLab merge request: once a review is started, comments go into it.
|
|
573
|
+
primary = draft().length ? add : now;
|
|
574
|
+
} else {
|
|
575
|
+
primary = button(t('addQuestion'), run(function (c) {
|
|
576
|
+
return send(item(c)).then(function (r) {
|
|
577
|
+
var msg = t(r.number ? 'addedNumber' : 'added', { file: r.file, number: r.number });
|
|
578
|
+
try { sessionStorage.setItem(TOAST_KEY, msg); } catch (e) {}
|
|
579
|
+
toast(msg);
|
|
580
|
+
});
|
|
581
|
+
}));
|
|
582
|
+
buttons.push(primary);
|
|
583
|
+
}
|
|
584
|
+
primary.className = 'c4b-primary';
|
|
585
|
+
buttons.forEach(function (b) {
|
|
586
|
+
actions.appendChild(b);
|
|
587
|
+
actions.appendChild(document.createTextNode(' '));
|
|
588
|
+
});
|
|
336
589
|
row.appendChild(hint);
|
|
337
590
|
row.appendChild(actions);
|
|
338
591
|
pop.appendChild(quote);
|
|
339
592
|
pop.appendChild(ta);
|
|
340
593
|
pop.appendChild(row);
|
|
341
594
|
document.body.appendChild(pop);
|
|
595
|
+
renderAgent();
|
|
342
596
|
place(pop, rect);
|
|
343
597
|
ta.focus();
|
|
344
|
-
var submit = function () {
|
|
345
|
-
var comment = ta.value.trim();
|
|
346
|
-
if (!comment) { ta.focus(); return; }
|
|
347
|
-
ok.disabled = true;
|
|
348
|
-
cancel.disabled = true;
|
|
349
|
-
send({ page: pageFile(), quote: data.quote, section: data.section, comment: comment }).then(function (r) {
|
|
350
|
-
var msg = 'Added to ' + r.file + (r.number ? ' as #' + r.number : '');
|
|
351
|
-
try { sessionStorage.setItem(TOAST_KEY, msg); } catch (e) {}
|
|
352
|
-
close();
|
|
353
|
-
toast(msg);
|
|
354
|
-
}, function (err) {
|
|
355
|
-
ok.disabled = false;
|
|
356
|
-
cancel.disabled = false;
|
|
357
|
-
toast('Comment failed: ' + (err && err.message ? err.message : err), true);
|
|
358
|
-
});
|
|
359
|
-
};
|
|
360
|
-
ok.addEventListener('click', submit);
|
|
361
|
-
cancel.addEventListener('click', close);
|
|
362
598
|
ta.addEventListener('keydown', function (e) {
|
|
363
599
|
if (e.key === 'Escape') { e.preventDefault(); close(); }
|
|
364
|
-
else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault();
|
|
600
|
+
else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); primary.click(); }
|
|
365
601
|
});
|
|
366
602
|
};
|
|
367
603
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
604
|
+
var openFor = function (feedback) {
|
|
605
|
+
return function () {
|
|
606
|
+
var s = selection();
|
|
607
|
+
if (s) open({ quote: s.text, section: headingBefore(s.range) }, anchorRect(s.range), feedback);
|
|
608
|
+
};
|
|
609
|
+
};
|
|
610
|
+
btns.addEventListener('mousedown', function (e) { e.preventDefault(); });
|
|
611
|
+
fbBtn.addEventListener('click', openFor(true));
|
|
612
|
+
qBtn.addEventListener('click', openFor(false));
|
|
373
613
|
document.addEventListener('mousedown', function (e) {
|
|
374
|
-
if (pop && !pop.contains(e.target) && e.target
|
|
614
|
+
if (pop && !pop.contains(e.target) && !btns.contains(e.target)) close();
|
|
375
615
|
});
|
|
376
616
|
window.addEventListener('hashchange', close);
|
|
617
|
+
renderAgent();
|
|
377
618
|
})();
|
|
378
619
|
</script>`;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Словарь интерфейса страницы `site -w` (change site-ui-language). Единственный файл `src/`, где
|
|
2
|
+
// тест cli-language разрешает кириллицу: это текст страницы в браузере, а не вывод CLI. Наборы
|
|
3
|
+
// ключей en и ru совпадают (проверяет тот же тест). `{name}` подставляет скрипт страницы; строка
|
|
4
|
+
// с числом — объект форм для Intl.PluralRules, число в ней — `{n}`.
|
|
5
|
+
export const UI_STRINGS = {
|
|
6
|
+
en: {
|
|
7
|
+
feedback: 'Feedback',
|
|
8
|
+
feedbackTitle: 'Tell the agent what to change',
|
|
9
|
+
question: 'Question',
|
|
10
|
+
questionTitle: 'Add a question to the {heading} section of the source file',
|
|
11
|
+
removeQuestion: 'Remove this question',
|
|
12
|
+
removed: 'Removed from {file}',
|
|
13
|
+
added: 'Added to {file}',
|
|
14
|
+
addedNumber: 'Added to {file} as #{number}',
|
|
15
|
+
sent: 'Sent to the agent',
|
|
16
|
+
delivered: 'Delivered to the agent',
|
|
17
|
+
queued: 'Queued — agent is not listening',
|
|
18
|
+
reviewFailed: 'Review failed: {error}',
|
|
19
|
+
feedbackFailed: 'Feedback failed: {error}',
|
|
20
|
+
questionFailed: 'Question failed: {error}',
|
|
21
|
+
removeFailed: 'Remove failed: {error}',
|
|
22
|
+
generalPlaceholder: 'Overall comment (optional, Ctrl+Enter to submit, Esc to hide)',
|
|
23
|
+
discard: 'Discard',
|
|
24
|
+
discardConfirm: { one: 'Discard {n} pending comment?', other: 'Discard {n} pending comments?' },
|
|
25
|
+
approve: 'Approve',
|
|
26
|
+
approveTitle: 'Tell the agent there is nothing more to change (pending comments go along)',
|
|
27
|
+
done: 'Done — no changes',
|
|
28
|
+
doneTitle: 'Tell the agent there is nothing to change',
|
|
29
|
+
submitReview: 'Submit review',
|
|
30
|
+
reviewEmpty: 'Review: nothing pending',
|
|
31
|
+
reviewCount: 'Review: {comments} ({pages})',
|
|
32
|
+
comments: { one: '{n} pending', other: '{n} pending' },
|
|
33
|
+
pages: { one: '{n} page', other: '{n} pages' },
|
|
34
|
+
removeFromReview: 'Remove from the review',
|
|
35
|
+
listening: 'agent: listening ●',
|
|
36
|
+
notListening: 'agent: not listening ○',
|
|
37
|
+
feedbackPlaceholder: 'What should the agent change? (Ctrl+Enter to send, Esc to cancel)',
|
|
38
|
+
questionPlaceholder: 'Your question (Ctrl+Enter to send, Esc to cancel)',
|
|
39
|
+
questionTo: 'To: {target}',
|
|
40
|
+
cancel: 'Cancel',
|
|
41
|
+
addToReview: 'Add to review',
|
|
42
|
+
sendNow: 'Send now',
|
|
43
|
+
addQuestion: 'Add question'
|
|
44
|
+
},
|
|
45
|
+
ru: {
|
|
46
|
+
feedback: 'Замечание',
|
|
47
|
+
feedbackTitle: 'Сказать агенту, что изменить',
|
|
48
|
+
question: 'Вопрос',
|
|
49
|
+
questionTitle: 'Добавить вопрос в раздел «{heading}» исходного файла',
|
|
50
|
+
removeQuestion: 'Удалить вопрос',
|
|
51
|
+
removed: 'Удалено из {file}',
|
|
52
|
+
added: 'Добавлено в {file}',
|
|
53
|
+
addedNumber: 'Добавлено в {file} под номером {number}',
|
|
54
|
+
sent: 'Отправлено агенту',
|
|
55
|
+
delivered: 'Доставлено агенту',
|
|
56
|
+
queued: 'В очереди: агент не слушает',
|
|
57
|
+
reviewFailed: 'Не удалось отправить ревью: {error}',
|
|
58
|
+
feedbackFailed: 'Не удалось отправить замечание: {error}',
|
|
59
|
+
questionFailed: 'Не удалось добавить вопрос: {error}',
|
|
60
|
+
removeFailed: 'Не удалось удалить вопрос: {error}',
|
|
61
|
+
generalPlaceholder: 'Общий комментарий (необязательно; Ctrl+Enter — отправить, Esc — скрыть)',
|
|
62
|
+
discard: 'Очистить',
|
|
63
|
+
discardConfirm: {
|
|
64
|
+
one: 'Удалить из ревью {n} замечание?',
|
|
65
|
+
few: 'Удалить из ревью {n} замечания?',
|
|
66
|
+
many: 'Удалить из ревью {n} замечаний?'
|
|
67
|
+
},
|
|
68
|
+
approve: 'Одобрить',
|
|
69
|
+
approveTitle: 'Сообщить агенту, что правок больше нет (замечания из ревью уйдут вместе с этим)',
|
|
70
|
+
done: 'Готово, правок нет',
|
|
71
|
+
doneTitle: 'Сообщить агенту, что правок нет',
|
|
72
|
+
submitReview: 'Отправить ревью',
|
|
73
|
+
reviewEmpty: 'Ревью: замечаний нет',
|
|
74
|
+
reviewCount: 'Ревью: {comments} {pages}',
|
|
75
|
+
comments: { one: '{n} замечание', few: '{n} замечания', many: '{n} замечаний' },
|
|
76
|
+
pages: { one: 'на {n} странице', few: 'на {n} страницах', many: 'на {n} страницах' },
|
|
77
|
+
removeFromReview: 'Убрать из ревью',
|
|
78
|
+
listening: 'агент: слушает ●',
|
|
79
|
+
notListening: 'агент: не слушает ○',
|
|
80
|
+
feedbackPlaceholder: 'Что должен изменить агент? (Ctrl+Enter — отправить, Esc — отмена)',
|
|
81
|
+
questionPlaceholder: 'Ваш вопрос (Ctrl+Enter — отправить, Esc — отмена)',
|
|
82
|
+
questionTo: 'Куда: {target}',
|
|
83
|
+
cancel: 'Отмена',
|
|
84
|
+
addToReview: 'В ревью',
|
|
85
|
+
sendNow: 'Отправить',
|
|
86
|
+
addQuestion: 'Добавить вопрос'
|
|
87
|
+
}
|
|
88
|
+
};
|