@bongos/core 1.21.43 → 1.21.45
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/.bongos-core.json +80 -70
- package/docs/copy-inventory.md +34 -22
- package/docs/copy-registry.json +144 -26
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +5 -2
- package/docs/page-readings.json +573 -583
- package/modules/hall-ui/public/atlas.html +3 -3
- package/modules/hall-ui/public/blockers.html +3 -3
- package/modules/hall-ui/public/board-room.html +3 -3
- package/modules/hall-ui/public/city-draw.js +51 -13
- package/modules/hall-ui/public/city.css +9 -2
- package/modules/hall-ui/public/city.html +6 -6
- package/modules/hall-ui/public/city.js +23 -12
- package/modules/hall-ui/public/city.states.json +146 -12
- package/modules/hall-ui/public/collab.html +3 -3
- package/modules/hall-ui/public/commands.html +3 -3
- package/modules/hall-ui/public/copy-desk.html +3 -3
- package/modules/hall-ui/public/craft-mode.js +8 -1
- package/modules/hall-ui/public/deploy.html +3 -3
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/drachmae.html +3 -3
- package/modules/hall-ui/public/fleet.html +3 -3
- package/modules/hall-ui/public/gate.html +3 -3
- package/modules/hall-ui/public/goal-map.html +3 -3
- package/modules/hall-ui/public/goals.html +3 -3
- package/modules/hall-ui/public/government.html +3 -3
- package/modules/hall-ui/public/idea.html +3 -3
- package/modules/hall-ui/public/ideas.html +3 -3
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/modules.html +3 -3
- package/modules/hall-ui/public/paintbrush.js +398 -0
- package/modules/hall-ui/public/primer.html +3 -3
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/project-settings.html +3 -3
- package/modules/hall-ui/public/ranks.html +3 -3
- package/modules/hall-ui/public/roadmap.html +3 -3
- package/modules/hall-ui/public/roster.html +3 -3
- package/modules/hall-ui/public/sessions.html +3 -3
- package/modules/hall-ui/public/settings.html +3 -3
- package/modules/hall-ui/public/street-card.css +6 -0
- package/modules/hall-ui/public/street-card.js +31 -3
- package/modules/hall-ui/public/street-draw.js +96 -19
- package/modules/hall-ui/public/street.css +23 -7
- package/modules/hall-ui/public/street.html +11 -10
- package/modules/hall-ui/public/street.js +45 -13
- package/modules/hall-ui/public/street.states.json +68 -0
- package/modules/hall-ui/public/studio.html +3 -3
- package/modules/hall-ui/public/style.css +78 -0
- package/modules/hall-ui/public/task.html +3 -3
- package/modules/hall-ui/public/thinking.html +3 -3
- package/modules/hall-ui/public/tweak-editor.html +3 -3
- package/modules/hall-ui/public/watch.html +3 -3
- package/modules/hall-ui/public/work.html +3 -3
- package/modules/hall-ui/records/craft-mode.md +9 -0
- package/modules/hall-ui/records/governor-street.md +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/hall-preview/server.js +5 -2
- package/src/module-api.js +1 -1
- package/tests/hall_city.mjs +42 -11
- package/tests/hall_matter_card.mjs +17 -0
- package/tests/hall_street.mjs +185 -12
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/page_notes_panel.mjs +4 -3
- package/tests/paintbrush.mjs +351 -0
|
@@ -134,9 +134,9 @@
|
|
|
134
134
|
|
|
135
135
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
136
136
|
|
|
137
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
138
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
139
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
137
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
138
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
139
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
140
140
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
141
141
|
<script src="/builders/palette.js" defer></script>
|
|
142
142
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
|
|
51
51
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
52
52
|
|
|
53
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
54
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
55
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
53
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
54
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
55
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
56
56
|
<script src="/builders/palette.js" defer></script>
|
|
57
57
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib" defer></script>
|
|
58
58
|
<!-- task-detail.js still owns the #/idea/N and #/blocker/N overlay, so a ref
|
|
@@ -225,9 +225,9 @@
|
|
|
225
225
|
|
|
226
226
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
227
227
|
|
|
228
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
229
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
230
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
228
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
229
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
230
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
231
231
|
<script src="/builders/palette.js" defer></script>
|
|
232
232
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
233
233
|
<!-- task 1003069 (R08) — the quick-filing form's routed result links a task
|
|
@@ -106,9 +106,9 @@
|
|
|
106
106
|
<script src="/builders/brand-apply.js?v=2026-06-27-r75" defer></script>
|
|
107
107
|
<!-- The app shell (sidebar + top bar + view router). Dependency-free; loaded
|
|
108
108
|
BEFORE builders.js so the chrome ids exist when events are wired. -->
|
|
109
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
110
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
111
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
109
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush" defer></script>
|
|
110
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush" defer></script>
|
|
111
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush" defer></script>
|
|
112
112
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
113
113
|
<script src="/builders/palette.js" defer></script>
|
|
114
114
|
<!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
|
|
@@ -101,9 +101,9 @@
|
|
|
101
101
|
|
|
102
102
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
103
103
|
|
|
104
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
105
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
106
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
104
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
105
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
106
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
107
107
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
108
108
|
<script src="/builders/palette.js" defer></script>
|
|
109
109
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// paintbrush.js — the Artistry paintbrush (task 1004497, goal 1000095 WA6).
|
|
2
|
+
// Loaded on demand by craft-mode.js when the paintbrush is pressed. Never on
|
|
3
|
+
// page load.
|
|
4
|
+
//
|
|
5
|
+
// The words of the page you are on become editable where they stand. What you
|
|
6
|
+
// type is a proposed wording change for THIS page, never a live edit: it is
|
|
7
|
+
// the page's tweak draft (the same round the tweak editor and the studio's own
|
|
8
|
+
// words write, ADR 0341), saved as you type. "Review" lists every change; "Send
|
|
9
|
+
// for approval" submits the round, an engineer applies the words, and they
|
|
10
|
+
// come back to the artist to approve. Nobody else's view of the page changes,
|
|
11
|
+
// and leaving puts the page's own words back.
|
|
12
|
+
//
|
|
13
|
+
// Owner, 2026-10-04: the edits are REAL wording changes (the page-tweak path),
|
|
14
|
+
// kept apart from the palette's feedback docs.
|
|
15
|
+
//
|
|
16
|
+
// Taking the page is the copy desk's claim: artists only, one at a time, and
|
|
17
|
+
// the page shows as being tweaked while it is held. The walls are the server's
|
|
18
|
+
// (modules/copy-desk); this file only draws. A word the page fills in for
|
|
19
|
+
// itself (a {…} hole) and a word several pages share are left to the editor.
|
|
20
|
+
(function () {
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
var API = '/api/bongos/copy-desk/pages/';
|
|
24
|
+
var SAVE_MS = 700;
|
|
25
|
+
var HOLE = '{…}';
|
|
26
|
+
// Never made editable: form controls, code, drawings, and the hall's own chrome.
|
|
27
|
+
var SKIP = 'script, style, svg, textarea, input, select, option, button, code, pre, [contenteditable="false"], #paintbrush';
|
|
28
|
+
var SCRIPT_V = (function () {
|
|
29
|
+
var src = document.currentScript && document.currentScript.src;
|
|
30
|
+
var m = src && /[?&]v=([^&#]+)/.exec(src);
|
|
31
|
+
return m ? m[1] : '';
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
var bar = null, st = null;
|
|
35
|
+
|
|
36
|
+
function esc(s) {
|
|
37
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
38
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function tidy(s) { return String(s == null ? '' : s).replace(/\s+/g, ' ').trim(); }
|
|
42
|
+
function plural(n, one, many) { return n + ' ' + (n === 1 ? one : many); }
|
|
43
|
+
// The page's id in the readings (docs/page-readings.json): builders:<data-page>.
|
|
44
|
+
function pageId() {
|
|
45
|
+
var b = document.body || { dataset: {} };
|
|
46
|
+
var slug = String((b.dataset && b.dataset.page) || 'home').toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 64) || 'home';
|
|
47
|
+
return 'builders:' + slug;
|
|
48
|
+
}
|
|
49
|
+
function editorHref(id) { return '/builders/tweak-editor?page=' + encodeURIComponent(id); }
|
|
50
|
+
|
|
51
|
+
// The two pure helpers this tool shares with the studio and the editor.
|
|
52
|
+
function loadScript(name, global) {
|
|
53
|
+
if (window[global]) return Promise.resolve(window[global]);
|
|
54
|
+
return new Promise(function (resolve, reject) {
|
|
55
|
+
var tag = document.createElement('script');
|
|
56
|
+
tag.src = '/builders/' + name + (SCRIPT_V ? '?v=' + SCRIPT_V : '');
|
|
57
|
+
tag.onload = function () { if (window[global]) resolve(window[global]); else reject(new Error('missing ' + global)); };
|
|
58
|
+
tag.onerror = function () { reject(new Error('missing ' + global)); };
|
|
59
|
+
document.head.appendChild(tag);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function libs() {
|
|
63
|
+
return loadScript('tweak-editor-lib.js', 'OTBTweakEditor').then(function (editor) {
|
|
64
|
+
return loadScript('studio-lib.js', 'OTBStudioLib').then(function (studio) { return { editor: editor, studio: studio }; });
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function request(method, path, body, keepalive) {
|
|
68
|
+
var opts = { method: method, credentials: 'same-origin', headers: {} };
|
|
69
|
+
// On the way out of the page the save must outlive it (tweak-editor.js's rule).
|
|
70
|
+
if (keepalive) opts.keepalive = true;
|
|
71
|
+
if (body !== undefined) { opts.headers['content-type'] = 'application/json'; opts.body = JSON.stringify(body); }
|
|
72
|
+
return fetch(API + path, opts).then(function (r) {
|
|
73
|
+
return r.json().catch(function () { return {}; }).then(function (j) { return { ok: r.ok, status: r.status, data: j }; });
|
|
74
|
+
}, function () { return { ok: false, status: 0, data: {} }; });
|
|
75
|
+
}
|
|
76
|
+
function refusal(res, fallback) {
|
|
77
|
+
var O = window.OTB || {};
|
|
78
|
+
var d = O.errDetails ? O.errDetails(res.data) : ((res.data && res.data.error && res.data.error.details) || {});
|
|
79
|
+
var code = O.errCode ? O.errCode(res.data) : ((res.data && res.data.error && res.data.error.code) || '');
|
|
80
|
+
if (res.status === 401) return 'Sign in to edit this page\'s words.';
|
|
81
|
+
if (st && st.lib) return st.lib.studio.claimWords(code, d);
|
|
82
|
+
return d.what_this_means || fallback;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------- the bar ----------
|
|
86
|
+
function shell(inner) {
|
|
87
|
+
return '<div class="pb__inner">' +
|
|
88
|
+
'<p class="pb__eyebrow"><span class="pb__dot" aria-hidden="true"></span>Paintbrush · ' + esc(st.title) + '</p>' +
|
|
89
|
+
inner +
|
|
90
|
+
'</div>';
|
|
91
|
+
}
|
|
92
|
+
function paint(inner) {
|
|
93
|
+
if (!bar) return;
|
|
94
|
+
bar.innerHTML = shell(inner);
|
|
95
|
+
var first = bar.querySelector('[data-pb-focus]') || bar.querySelector('button');
|
|
96
|
+
if (first && st.focusBar) first.focus();
|
|
97
|
+
st.focusBar = false;
|
|
98
|
+
}
|
|
99
|
+
function note(text, withClose) {
|
|
100
|
+
paint('<p class="pb__line" role="status">' + text + '</p>' +
|
|
101
|
+
'<div class="pb__actions">' + (withClose === false ? '' : '<button class="pb__btn" type="button" data-pb-close data-pb-focus>Close</button>') + '</div>');
|
|
102
|
+
}
|
|
103
|
+
function offerHtml(err) {
|
|
104
|
+
return '<p class="pb__line"><strong>Edit this page\'s words where they stand?</strong> ' +
|
|
105
|
+
'Taking the page marks it as yours until you send the new words for approval. Nobody else sees them until then.</p>' +
|
|
106
|
+
'<p class="pb__error" role="alert">' + esc(err || '') + '</p>' +
|
|
107
|
+
'<div class="pb__actions">' +
|
|
108
|
+
'<button class="pb__btn" type="button" data-pb-close>Not now</button>' +
|
|
109
|
+
'<button class="pb__btn pb__btn--go" type="button" data-pb-take data-pb-focus>Take this page</button>' +
|
|
110
|
+
'</div>';
|
|
111
|
+
}
|
|
112
|
+
function editingHtml() {
|
|
113
|
+
var changed = changes().length;
|
|
114
|
+
var save = st.save === 'saving' ? 'saving…' : st.save === 'failed' ? 'not saved' : 'saved';
|
|
115
|
+
var rest = st.unreached;
|
|
116
|
+
return '<p class="pb__line" aria-live="polite"><strong>' + esc(plural(changed, 'change', 'changes')) + '</strong> · ' + esc(save) +
|
|
117
|
+
(st.error ? ' · <span class="pb__error-inline">' + esc(st.error) + '</span>' : '') + '</p>' +
|
|
118
|
+
'<p class="pb__hint">Click any underlined words and type over them.' +
|
|
119
|
+
(rest ? ' ' + esc(plural(rest, 'other line', 'other lines')) + ' of this page ' + (rest === 1 ? 'is' : 'are') + ' in the <a href="' + esc(editorHref(st.id)) + '">tweak editor</a>.' : '') + '</p>' +
|
|
120
|
+
'<div class="pb__actions">' +
|
|
121
|
+
'<button class="pb__btn" type="button" data-pb-stop>Stop editing</button>' +
|
|
122
|
+
'<button class="pb__btn pb__btn--go" type="button" data-pb-review' + (changed ? '' : ' disabled') + '>Review ' + esc(plural(changed, 'change', 'changes')) + '</button>' +
|
|
123
|
+
'</div>';
|
|
124
|
+
}
|
|
125
|
+
function reviewHtml(err) {
|
|
126
|
+
var rows = changes();
|
|
127
|
+
return '<p class="pb__line"><strong>Your changes to this page</strong></p>' +
|
|
128
|
+
'<ol class="pb__changes">' + rows.map(function (c) {
|
|
129
|
+
return '<li class="pb__change">' +
|
|
130
|
+
'<span class="pb__where">' + esc(c.section || 'this page') + '</span>' +
|
|
131
|
+
'<span class="pb__was"><span class="pb__tag">Was</span> ' + esc(c.was) + '</span>' +
|
|
132
|
+
'<span class="pb__now"><span class="pb__tag">Now</span> ' + esc(c.now) + '</span>' +
|
|
133
|
+
'</li>';
|
|
134
|
+
}).join('') + '</ol>' +
|
|
135
|
+
'<p class="pb__hint">Sending submits the page: an engineer applies the new words, and they come back to you to approve before anyone sees them.</p>' +
|
|
136
|
+
'<p class="pb__error" role="alert">' + esc(err || '') + '</p>' +
|
|
137
|
+
'<div class="pb__actions">' +
|
|
138
|
+
'<button class="pb__btn" type="button" data-pb-back>Keep editing</button>' +
|
|
139
|
+
'<button class="pb__btn pb__btn--go" type="button" data-pb-send data-pb-focus>Send for approval</button>' +
|
|
140
|
+
'</div>';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------- the words ----------
|
|
144
|
+
// Every line the artist has changed, in the page's reading order.
|
|
145
|
+
function changes() {
|
|
146
|
+
if (!st || !st.lines) return [];
|
|
147
|
+
return st.lines.filter(function (l) { return st.lib.editor.isChanged(l, st.values[l.key]) && String(st.values[l.key]).trim(); })
|
|
148
|
+
.map(function (l) { return { key: l.key, section: l.section, was: l.text, now: String(st.values[l.key]).replace(/\s+/g, ' ').trim() }; });
|
|
149
|
+
}
|
|
150
|
+
// The elements on the page whose words are one reading line: a leaf holding
|
|
151
|
+
// text, inside the page's main content, not a control and not shared chrome.
|
|
152
|
+
function candidates() {
|
|
153
|
+
var root = document.querySelector('main') || document.getElementById('main') || document.body;
|
|
154
|
+
var all = root ? root.querySelectorAll('*') : [];
|
|
155
|
+
var out = [];
|
|
156
|
+
for (var i = 0; i < all.length; i++) {
|
|
157
|
+
var el = all[i];
|
|
158
|
+
if (el.childElementCount || !String(el.textContent || '').trim()) continue;
|
|
159
|
+
if (el.closest && el.closest(SKIP)) continue;
|
|
160
|
+
if (el.closest && el.closest('[hidden]')) continue;
|
|
161
|
+
out.push(el);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
function start(ed) {
|
|
166
|
+
var reading = ed.reading || {};
|
|
167
|
+
st.lines = (reading.lines || []).filter(function (l) { return l.placement !== 'shared' && String(l.text).indexOf(HOLE) === -1; });
|
|
168
|
+
st.readingHash = reading.reading_hash || null;
|
|
169
|
+
st.values = st.lib.editor.initialValues(st.lines, ed.draft);
|
|
170
|
+
st.lastSent = JSON.stringify(st.lib.editor.draftBody(st.readingHash, st.lines, st.values));
|
|
171
|
+
st.els = [];
|
|
172
|
+
// The lines left are hole-free, so a match is the same words, spaces
|
|
173
|
+
// aside: one table built once, not a pattern per line per element.
|
|
174
|
+
var byText = {};
|
|
175
|
+
st.lines.forEach(function (l) { var k = tidy(l.text); if (k && !(k in byText)) byText[k] = l; });
|
|
176
|
+
var used = {};
|
|
177
|
+
candidates().forEach(function (el) {
|
|
178
|
+
var line = byText[tidy(el.textContent)] || null;
|
|
179
|
+
if (!line || used[line.key]) return;
|
|
180
|
+
used[line.key] = true;
|
|
181
|
+
st.els.push({ el: el, key: line.key, original: el.textContent });
|
|
182
|
+
el.textContent = st.values[line.key];
|
|
183
|
+
el.setAttribute('contenteditable', 'plaintext-only');
|
|
184
|
+
if (el.contentEditable !== 'plaintext-only') el.setAttribute('contenteditable', 'true');
|
|
185
|
+
el.setAttribute('spellcheck', 'false');
|
|
186
|
+
el.setAttribute('data-pb-key', line.key);
|
|
187
|
+
el.classList.add('pb-word');
|
|
188
|
+
el.classList.toggle('pb-word--changed', st.lib.editor.isChanged(line, st.values[line.key]));
|
|
189
|
+
});
|
|
190
|
+
// A hint, not a ledger: a line the page shows twice is edited once (the
|
|
191
|
+
// first), and its second showing is counted in neither number.
|
|
192
|
+
st.unreached = Math.max(0, (reading.lines || []).length - st.els.length - ((reading.lines || []).filter(function (l) { return l.placement === 'shared'; }).length));
|
|
193
|
+
st.editing = true;
|
|
194
|
+
st.save = 'saved';
|
|
195
|
+
document.documentElement.classList.add('pb-editing');
|
|
196
|
+
document.addEventListener('input', onInput, true);
|
|
197
|
+
document.addEventListener('click', onPageClick, true);
|
|
198
|
+
document.addEventListener('keydown', onPageKey, true);
|
|
199
|
+
document.addEventListener('paste', onPaste, true);
|
|
200
|
+
document.addEventListener('focusout', onBlur, true);
|
|
201
|
+
window.addEventListener('pagehide', flush);
|
|
202
|
+
if (!st.els.length) {
|
|
203
|
+
note('None of this page\'s words can be edited where they stand. <a href="' + esc(editorHref(st.id)) + '">Rewrite them in the tweak editor</a>.');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
paint(editingHtml());
|
|
207
|
+
st.els[0].el.focus();
|
|
208
|
+
}
|
|
209
|
+
function entryOf(el) {
|
|
210
|
+
var host = el && el.closest ? el.closest('[data-pb-key]') : null;
|
|
211
|
+
if (!host) return null;
|
|
212
|
+
for (var i = 0; i < st.els.length; i++) if (st.els[i].el === host) return st.els[i];
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
function onInput(e) {
|
|
216
|
+
var en = entryOf(e.target);
|
|
217
|
+
if (!en) return;
|
|
218
|
+
st.values[en.key] = en.el.textContent;
|
|
219
|
+
var line = lineOf(en.key);
|
|
220
|
+
en.el.classList.toggle('pb-word--changed', !!line && st.lib.editor.isChanged(line, st.values[en.key]));
|
|
221
|
+
if (!st.reviewing) paint(editingHtml());
|
|
222
|
+
clearTimeout(st.timer);
|
|
223
|
+
st.timer = setTimeout(save, SAVE_MS);
|
|
224
|
+
}
|
|
225
|
+
function lineOf(key) {
|
|
226
|
+
for (var i = 0; i < st.lines.length; i++) if (st.lines[i].key === key) return st.lines[i];
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
// While editing, a click on the words places the caret: the link or card
|
|
230
|
+
// around them does not navigate.
|
|
231
|
+
function onPageClick(e) {
|
|
232
|
+
if (entryOf(e.target)) { e.preventDefault(); }
|
|
233
|
+
}
|
|
234
|
+
function onPageKey(e) {
|
|
235
|
+
if (e.key === 'Escape' && bar && st.reviewing) { e.preventDefault(); back(); return; }
|
|
236
|
+
if (e.key === 'Enter' && entryOf(e.target)) { e.preventDefault(); e.target.blur(); return; }
|
|
237
|
+
// Words only: where plaintext-only is missing, bold/italic/underline keys do nothing.
|
|
238
|
+
if ((e.ctrlKey || e.metaKey) && /^[biu]$/i.test(e.key) && entryOf(e.target)) e.preventDefault();
|
|
239
|
+
}
|
|
240
|
+
function onPaste(e) {
|
|
241
|
+
if (!entryOf(e.target)) return;
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
var text = ((e.clipboardData && e.clipboardData.getData('text/plain')) || '').replace(/\s+/g, ' ');
|
|
244
|
+
document.execCommand('insertText', false, text);
|
|
245
|
+
}
|
|
246
|
+
function onBlur(e) { if (entryOf(e.target)) save(); }
|
|
247
|
+
|
|
248
|
+
// The WHOLE draft each time (every changed line, the ones typed here and any
|
|
249
|
+
// the editor saved), one save in flight at a time.
|
|
250
|
+
function save(leaving) {
|
|
251
|
+
clearTimeout(st.timer);
|
|
252
|
+
if (!st.editing) return Promise.resolve(true);
|
|
253
|
+
if (st.inflight) { st.again = true; return st.inflight; }
|
|
254
|
+
var body = st.lib.editor.draftBody(st.readingHash, st.lines, st.values);
|
|
255
|
+
var sent = JSON.stringify(body);
|
|
256
|
+
if (sent === st.lastSent) return Promise.resolve(true);
|
|
257
|
+
st.save = 'saving';
|
|
258
|
+
st.error = '';
|
|
259
|
+
if (!st.reviewing) paint(editingHtml());
|
|
260
|
+
st.inflight = request('PUT', encodeURIComponent(st.id) + '/draft', body, leaving === true).then(function (res) {
|
|
261
|
+
st.inflight = null;
|
|
262
|
+
if (res.ok) { st.lastSent = sent; st.save = 'saved'; }
|
|
263
|
+
else { st.save = 'failed'; st.error = refusal(res, 'the words could not be saved'); }
|
|
264
|
+
if (!st.reviewing && st.editing) paint(editingHtml());
|
|
265
|
+
if (st.again) { st.again = false; return save(); }
|
|
266
|
+
return res.ok;
|
|
267
|
+
});
|
|
268
|
+
return st.inflight;
|
|
269
|
+
}
|
|
270
|
+
function flush() { if (st && st.editing) save(true); }
|
|
271
|
+
|
|
272
|
+
// Put the page's own words back and leave nothing editable behind.
|
|
273
|
+
function restore() {
|
|
274
|
+
if (!st || !st.els) return;
|
|
275
|
+
st.els.forEach(function (en) {
|
|
276
|
+
en.el.textContent = en.original;
|
|
277
|
+
en.el.removeAttribute('contenteditable');
|
|
278
|
+
en.el.removeAttribute('spellcheck');
|
|
279
|
+
en.el.removeAttribute('data-pb-key');
|
|
280
|
+
en.el.classList.remove('pb-word', 'pb-word--changed');
|
|
281
|
+
});
|
|
282
|
+
st.els = [];
|
|
283
|
+
st.editing = false;
|
|
284
|
+
document.documentElement.classList.remove('pb-editing');
|
|
285
|
+
document.removeEventListener('input', onInput, true);
|
|
286
|
+
document.removeEventListener('click', onPageClick, true);
|
|
287
|
+
document.removeEventListener('keydown', onPageKey, true);
|
|
288
|
+
document.removeEventListener('paste', onPaste, true);
|
|
289
|
+
document.removeEventListener('focusout', onBlur, true);
|
|
290
|
+
window.removeEventListener('pagehide', flush);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---------- the steps ----------
|
|
294
|
+
function read() {
|
|
295
|
+
return request('GET', encodeURIComponent(st.id) + '/draft');
|
|
296
|
+
}
|
|
297
|
+
function begin() {
|
|
298
|
+
note('Reading this page\'s words…', false);
|
|
299
|
+
libs().then(function (lib) {
|
|
300
|
+
st.lib = lib;
|
|
301
|
+
return read();
|
|
302
|
+
}).then(function (res) {
|
|
303
|
+
if (!bar) return;
|
|
304
|
+
if (!res.ok) { note(esc(refusal(res, 'This page\'s words could not be read just now.'))); return; }
|
|
305
|
+
var ed = res.data || {};
|
|
306
|
+
st.title = (ed.page && ed.page.title) || st.title;
|
|
307
|
+
if (!ed.reading || !ed.reading.read) { note('This page has not been read yet, so its words cannot be edited here. Use Feedback to say what its words should be.'); return; }
|
|
308
|
+
var round = ed.round;
|
|
309
|
+
if (round && round.held_by_you && round.state === 'writing') { start(ed); return; }
|
|
310
|
+
if (round && round.state === 'writing') { note(esc(st.lib.studio.claimWords('page_held', { holder: round.holder }))); return; }
|
|
311
|
+
if (round) { note(esc(st.lib.studio.claimWords('page_in_flight', {}))); return; }
|
|
312
|
+
st.focusBar = true;
|
|
313
|
+
paint(offerHtml());
|
|
314
|
+
}, function () {
|
|
315
|
+
note('The paintbrush could not load. Nothing changed. Reload the page and press it again.');
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function take() {
|
|
319
|
+
var btn = bar && bar.querySelector('[data-pb-take]');
|
|
320
|
+
if (btn) btn.disabled = true;
|
|
321
|
+
request('POST', encodeURIComponent(st.id) + '/claim', {}).then(function (res) {
|
|
322
|
+
if (!res.ok) { paint(offerHtml(refusal(res, 'That page could not be taken just now. Nothing changed.'))); return null; }
|
|
323
|
+
return read();
|
|
324
|
+
}).then(function (res) {
|
|
325
|
+
if (!res) return;
|
|
326
|
+
if (!res.ok || !res.data || !res.data.reading) { note('The page is yours, but its words could not be read just now. <a href="' + esc(editorHref(st.id)) + '">Open it in the tweak editor</a>.'); return; }
|
|
327
|
+
start(res.data);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function review() {
|
|
331
|
+
save();
|
|
332
|
+
st.reviewing = true;
|
|
333
|
+
st.focusBar = true;
|
|
334
|
+
paint(reviewHtml());
|
|
335
|
+
}
|
|
336
|
+
function back() {
|
|
337
|
+
st.reviewing = false;
|
|
338
|
+
paint(editingHtml());
|
|
339
|
+
var b = bar.querySelector('[data-pb-review]');
|
|
340
|
+
if (b) b.focus();
|
|
341
|
+
}
|
|
342
|
+
function send() {
|
|
343
|
+
var btn = bar.querySelector('[data-pb-send]');
|
|
344
|
+
if (btn) btn.disabled = true;
|
|
345
|
+
save().then(function (ok) {
|
|
346
|
+
if (!ok) { paint(reviewHtml(st.error || 'The words could not be saved, so nothing was sent.')); return null; }
|
|
347
|
+
return request('POST', encodeURIComponent(st.id) + '/submit', {});
|
|
348
|
+
}).then(function (res) {
|
|
349
|
+
if (!res) return;
|
|
350
|
+
if (!res.ok) { paint(reviewHtml(refusal(res, 'The page could not be sent just now. Your words are saved.'))); return; }
|
|
351
|
+
var n = changes().length;
|
|
352
|
+
restore();
|
|
353
|
+
st.reviewing = false;
|
|
354
|
+
st.focusBar = true;
|
|
355
|
+
note('Sent for approval: ' + esc(plural(n, 'change', 'changes')) + '. An engineer applies the new words, and they come back to you in the studio to approve.');
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
function stop() {
|
|
359
|
+
var keep = changes().length;
|
|
360
|
+
save().then(function () {
|
|
361
|
+
restore();
|
|
362
|
+
close();
|
|
363
|
+
if (keep && window.OTB && typeof window.OTB.toast === 'function') {
|
|
364
|
+
window.OTB.toast('Your words are saved, and the page stays yours. Press the paintbrush to carry on.');
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
function onBarClick(e) {
|
|
369
|
+
var btn = e.target.closest && e.target.closest('button');
|
|
370
|
+
if (!btn || btn.disabled) return;
|
|
371
|
+
if (btn.hasAttribute('data-pb-close')) close();
|
|
372
|
+
else if (btn.hasAttribute('data-pb-take')) take();
|
|
373
|
+
else if (btn.hasAttribute('data-pb-review')) review();
|
|
374
|
+
else if (btn.hasAttribute('data-pb-back')) back();
|
|
375
|
+
else if (btn.hasAttribute('data-pb-send')) send();
|
|
376
|
+
else if (btn.hasAttribute('data-pb-stop')) stop();
|
|
377
|
+
}
|
|
378
|
+
function close() {
|
|
379
|
+
if (st && st.editing) { stop(); return; }
|
|
380
|
+
if (bar && bar.parentNode) bar.parentNode.removeChild(bar);
|
|
381
|
+
bar = null;
|
|
382
|
+
st = null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function open() {
|
|
386
|
+
if (bar) { var f = bar.querySelector('button'); if (f) f.focus(); return; }
|
|
387
|
+
st = { id: pageId(), title: (document.body && document.body.dataset && document.body.dataset.pageTitle) || String(document.title || '').split(/\s[—-]\s/)[0] || 'this page', focusBar: false };
|
|
388
|
+
bar = document.createElement('section');
|
|
389
|
+
bar.className = 'pb';
|
|
390
|
+
bar.id = 'paintbrush';
|
|
391
|
+
bar.setAttribute('aria-label', 'Paintbrush: edit this page\'s words');
|
|
392
|
+
bar.addEventListener('click', onBarClick);
|
|
393
|
+
document.body.appendChild(bar);
|
|
394
|
+
begin();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
window.OTBPaintbrush = { open: open, close: close, pageId: pageId };
|
|
398
|
+
})();
|
|
@@ -115,9 +115,9 @@
|
|
|
115
115
|
|
|
116
116
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
117
117
|
|
|
118
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
119
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
120
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
118
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
119
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
120
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
121
121
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
122
122
|
<script src="/builders/palette.js" defer></script>
|
|
123
123
|
<!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
|
|
@@ -206,9 +206,9 @@
|
|
|
206
206
|
|
|
207
207
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
208
208
|
chrome exists before the page script runs. -->
|
|
209
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
210
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
211
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
209
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
210
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
211
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
212
212
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
213
213
|
<script src="/builders/palette.js" defer></script>
|
|
214
214
|
<!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
|
|
59
59
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
60
60
|
|
|
61
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
62
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
63
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
61
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
62
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
63
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
64
64
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
65
65
|
<script src="/builders/palette.js" defer></script>
|
|
66
66
|
<script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
|
|
@@ -77,9 +77,9 @@
|
|
|
77
77
|
|
|
78
78
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
79
79
|
chrome exists before the page script runs. -->
|
|
80
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
81
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
82
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
80
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
81
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
82
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
83
83
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
84
84
|
<script src="/builders/palette.js" defer></script>
|
|
85
85
|
<!-- shared frontend toolkit (window.OTB) — loaded before ranks.js, which pulls from it. -->
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
|
|
42
42
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
43
43
|
|
|
44
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
45
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
46
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
44
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
45
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
46
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
47
47
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
48
48
|
<script src="/builders/palette.js" defer></script>
|
|
49
49
|
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
|
|
54
54
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
55
55
|
chrome exists before the page script runs. -->
|
|
56
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
57
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
58
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
56
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
57
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
58
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
59
59
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
60
60
|
<script src="/builders/palette.js" defer></script>
|
|
61
61
|
<!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
|
|
@@ -111,9 +111,9 @@
|
|
|
111
111
|
|
|
112
112
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
113
113
|
chrome exists before the page script runs. -->
|
|
114
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
115
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
116
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
114
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
115
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
116
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
117
117
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
118
118
|
<script src="/builders/palette.js" defer></script>
|
|
119
119
|
<!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
|
|
@@ -435,9 +435,9 @@
|
|
|
435
435
|
|
|
436
436
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
437
437
|
chrome exists before the page scripts run. -->
|
|
438
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
439
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
440
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
438
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
439
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
440
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
441
441
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
442
442
|
<script src="/builders/palette.js" defer></script>
|
|
443
443
|
<!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
|
|
@@ -84,6 +84,12 @@ body[data-page="street"] {
|
|
|
84
84
|
|
|
85
85
|
/* more: the detail, in place */
|
|
86
86
|
.mcard-more__toggle { margin-top: -2px; }
|
|
87
|
+
/* the pager: "1 of N" with previous and next, when a pin holds more than one matter (task 1004569) */
|
|
88
|
+
.mcard-pager { display: flex; align-items: center; gap: 4px; margin: -4px 0 2px; }
|
|
89
|
+
.mcard-pager__btn { width: 32px; height: 32px; border-radius: 999px; border: 1px solid var(--street-rule-strong); background: none; color: var(--ink); font-family: var(--font-body); font-size: 17px; line-height: 1; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; padding: 0 0 2px; }
|
|
90
|
+
.mcard-pager__btn:hover { border-color: var(--ink-faint); }
|
|
91
|
+
.mcard-pager__btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
92
|
+
.mcard-pager__at { min-width: 52px; text-align: center; font-size: 12px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
|
|
87
93
|
.mcard-more { display: flex; flex-direction: column; gap: 6px; padding-top: 10px; border-top: 1px solid var(--street-rule); }
|
|
88
94
|
.mcard-more__text { margin: 0 0 4px; font-size: 14px; line-height: 1.5; white-space: pre-line; overflow-wrap: anywhere; }
|
|
89
95
|
.mcard-more__meta { margin: 0; font-size: 13px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
|