@bongos/core 1.19.458 → 1.19.460
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 +117 -42
- package/.claude/skills/design/SKILL.md +10 -0
- package/docs/adr/0219-a-look-is-a-branding-pack-the-style-library.md +30 -0
- package/docs/adr/README.md +1 -0
- package/docs/branding-contract.md +1 -1
- package/docs/copy-inventory.md +52 -61
- package/docs/copy-registry.json +92 -203
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/ui-look-before-you-ship.md +2 -0
- package/modules/hall-ui/public/collab-lib.js +40 -4
- package/modules/hall-ui/public/collab.css +18 -34
- package/modules/hall-ui/public/collab.html +17 -17
- package/modules/hall-ui/public/collab.js +75 -39
- package/modules/hall-ui/public/collab.states.json +2 -2
- package/modules/hall-ui/public/copy-desk.css +64 -109
- package/modules/hall-ui/public/copy-desk.html +76 -69
- package/modules/hall-ui/public/copy-desk.js +175 -80
- package/modules/hall-ui/public/copy-desk.states.json +59 -2
- package/modules/hall-ui/public/oversight.css +11 -2
- package/modules/lifecycle/help-requests.js +18 -8
- package/modules/ui-design/kit/README.md +7 -5
- package/modules/ui-design/kit/check-mock.js +12 -5
- package/modules/ui-design/kit/contrast.js +13 -6
- package/modules/ui-design/kit/lib.js +39 -2
- package/modules/ui-design/kit/probe.js +4 -1
- package/modules/ui-design/kit/render.js +7 -4
- package/modules/ui-design/kit/serve.js +9 -2
- package/modules/ui-design/styles/README.md +54 -2
- package/modules/ui-design/styles/chrome-world/DESIGN.md +56 -0
- package/modules/ui-design/styles/chrome-world/mock.html +275 -0
- package/modules/ui-design/styles/chrome-world/mock.states.json +11 -0
- package/modules/ui-design/styles/chrome-world/pack.json +33 -0
- package/modules/ui-design/styles/expedition/DESIGN.md +141 -0
- package/modules/ui-design/styles/expedition/mock.html +285 -0
- package/modules/ui-design/styles/expedition/mock.states.json +11 -0
- package/modules/ui-design/styles/expedition/pack.json +33 -0
- package/modules/ui-design/styles/grove/DESIGN.md +148 -0
- package/modules/ui-design/styles/grove/mock.html +294 -0
- package/modules/ui-design/styles/grove/mock.states.json +11 -0
- package/modules/ui-design/styles/grove/pack.json +33 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/collab_page.mjs +13 -7
- package/tests/cosmos_token_contrast.mjs +9 -2
- package/tests/hall_collab_world.mjs +214 -0
- package/tests/hall_tokens.mjs +15 -6
- package/tests/ui_design_styles.mjs +218 -0
- package/tests/world_tokens.mjs +9 -2
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
// modules/hall-ui/public/copy-desk.js — the hall "Copy desk" tab
|
|
2
|
-
// (/builders/copy-desk). Task 1003113 / R02 of goal 1000074, the artist's loop
|
|
2
|
+
// (/builders/copy-desk). Task 1003113 / R02 of goal 1000074, the artist's loop;
|
|
3
|
+
// rebuilt on the hall's row archetype by task 1003471 (goal 1000081).
|
|
3
4
|
//
|
|
4
5
|
// The surface half of the flag: R01 built the inventory of every string the
|
|
5
6
|
// product shows a person; the copy-desk module turned "this one is wrong" into a
|
|
6
7
|
// row; this page is where an artist reads those rows and where everyone else
|
|
7
8
|
// files them.
|
|
8
9
|
//
|
|
9
|
-
// FOUR
|
|
10
|
-
// 1. the counts — what is outstanding, and the fast pile
|
|
10
|
+
// FOUR PARTS, IN THE ORDER AN ARTIST WOULD WORK:
|
|
11
|
+
// 1. the counts — the glass figure strip: what is outstanding, and the fast pile
|
|
11
12
|
// 2. Flagged — the queue itself, newest first, open by default
|
|
12
13
|
// 3. Said more than once — the deterministic duplicate signal, minus what has
|
|
13
|
-
// already been flagged or dismissed
|
|
14
|
+
// already been flagged or dismissed, paged through the kit
|
|
14
15
|
// 4. Find something to flag — search the whole inventory
|
|
15
16
|
//
|
|
17
|
+
// Every list is a run of the family's .ov-row (oversight.css): the string as
|
|
18
|
+
// the title, the judgement as prose, the kind and the target's state as pills.
|
|
19
|
+
// State is COLOUR from the verification vocabulary, never opacity.
|
|
20
|
+
//
|
|
16
21
|
// NO LIVE CMS. Nothing on this page edits a string. The composer collects a
|
|
17
22
|
// JUDGEMENT (kind + reason); the fix ships as a claimed, graded task. There is
|
|
18
23
|
// deliberately no "new text" field — see modules/copy-desk/tests/copy_no_cms.mjs.
|
|
@@ -23,17 +28,21 @@
|
|
|
23
28
|
const API = '/api/bongos';
|
|
24
29
|
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
25
30
|
const { escapeHtml, $, toast, fmtInt } = window.OTB;
|
|
31
|
+
const kit = window.OTBKit;
|
|
26
32
|
|
|
27
33
|
let viewer = null;
|
|
28
34
|
let queue = null;
|
|
29
35
|
// The composer's current target, set when a "Flag" button is pressed.
|
|
30
36
|
let target = null;
|
|
31
37
|
let closing = null;
|
|
38
|
+
let dupePager = null;
|
|
32
39
|
|
|
33
40
|
// ---------------------------------------------------------------------------
|
|
34
41
|
// rendering helpers
|
|
35
42
|
// ---------------------------------------------------------------------------
|
|
36
43
|
|
|
44
|
+
const int = (n) => String(fmtInt ? fmtInt(n) : n);
|
|
45
|
+
|
|
37
46
|
// A string shown as evidence is UNTRUSTED text from the product's own source
|
|
38
47
|
// tree, rendered into this page — so it is escaped like any other user data,
|
|
39
48
|
// and clipped so one runaway template literal cannot push the queue off screen.
|
|
@@ -45,7 +54,17 @@
|
|
|
45
54
|
|
|
46
55
|
function where(file, line) {
|
|
47
56
|
if (!file) return '';
|
|
48
|
-
return `<span class="
|
|
57
|
+
return `<span class="ov-key ov-key--faint">${escapeHtml(file)}${Number.isInteger(line) ? `:${line}` : ''}</span>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function pill(mod, text, title) {
|
|
61
|
+
return `<span class="fact-pill${mod ? ` ${mod}` : ''}"${title ? ` title="${escapeHtml(title)}"` : ''}>${escapeHtml(text)}</span>`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function when(iso) {
|
|
65
|
+
const d = iso ? new Date(iso) : null;
|
|
66
|
+
if (!d || Number.isNaN(d.getTime())) return '';
|
|
67
|
+
return `<span class="ov-row__when">${escapeHtml(d.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }))}</span>`;
|
|
49
68
|
}
|
|
50
69
|
|
|
51
70
|
const KIND_LABEL = {
|
|
@@ -60,10 +79,10 @@
|
|
|
60
79
|
// never something the server closes on its own.
|
|
61
80
|
function targetState(f) {
|
|
62
81
|
if (f.target_state === 'gone') {
|
|
63
|
-
return '
|
|
82
|
+
return pill('fact-pill--warn', 'target gone', 'The string this flag names is no longer in the registry — it was edited or removed. Probably already fixed; confirm and close it.');
|
|
64
83
|
}
|
|
65
84
|
if (f.moved) {
|
|
66
|
-
return '
|
|
85
|
+
return pill('', 'moved', 'Same text, different place — it moved file or line since the flag was filed.');
|
|
67
86
|
}
|
|
68
87
|
return '';
|
|
69
88
|
}
|
|
@@ -74,68 +93,97 @@
|
|
|
74
93
|
return viewer.rank === 'metic' || viewer.rank === 'archon';
|
|
75
94
|
}
|
|
76
95
|
|
|
96
|
+
// The outcome of a closed flag: the status as a pill, the task that carried
|
|
97
|
+
// the fix as a chip, the note as a line — every fact the closer wrote.
|
|
98
|
+
function closedFacts(f) {
|
|
99
|
+
if (f.status === 'open') return '';
|
|
100
|
+
const task = f.resolved_task_id
|
|
101
|
+
? `<a class="fact-chip" href="/builders/task/${escapeHtml(String(f.resolved_task_id))}">task #${escapeHtml(String(f.resolved_task_id))}</a>`
|
|
102
|
+
: '';
|
|
103
|
+
return pill(f.status === 'resolved' ? 'fact-pill--ok' : 'fact-pill--quiet', f.status) + task;
|
|
104
|
+
}
|
|
105
|
+
|
|
77
106
|
function flagRow(f) {
|
|
78
|
-
const who =
|
|
79
|
-
const when = f.flagged_at ? new Date(f.flagged_at).toLocaleDateString() : '';
|
|
107
|
+
const who = f.flagged_by_name || f.flagged_by_login || 'someone';
|
|
80
108
|
// Show the live text when the string is still there, and the snapshot when it
|
|
81
109
|
// is not — labelled, so "what it says now" is never confused with "what it
|
|
82
|
-
// said when someone objected".
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
110
|
+
// said when someone objected". A whole-surface flag names the surface.
|
|
111
|
+
let title;
|
|
112
|
+
let place;
|
|
113
|
+
if (f.scope === 'surface') {
|
|
114
|
+
title = `<span class="ov-key">${escapeHtml(f.surface)}</span>`;
|
|
115
|
+
place = pill('', 'whole surface');
|
|
116
|
+
} else if (f.current) {
|
|
117
|
+
title = quote(f.current.text);
|
|
118
|
+
place = where(f.current.file, f.current.line);
|
|
119
|
+
} else {
|
|
120
|
+
title = `<span class="cd-snapshot-label">said, when flagged:</span> ${quote(f.text_at_flag)}`;
|
|
121
|
+
place = where(f.file_at_flag, f.line_at_flag);
|
|
122
|
+
}
|
|
123
|
+
const note = f.status !== 'open' && f.resolution_note
|
|
124
|
+
? `<p class="cd-closed"><b>${escapeHtml(f.status)}</b> — ${escapeHtml(f.resolution_note)}</p>`
|
|
125
|
+
: '';
|
|
126
|
+
const actions = f.status === 'open' && canClose(f)
|
|
127
|
+
? `<div class="ov-row__actions"><button class="btn-ghost cd-close-btn" data-flag="${escapeHtml(String(f.id))}">Close…</button></div>`
|
|
93
128
|
: '';
|
|
94
129
|
|
|
95
|
-
return `<
|
|
96
|
-
<div class="
|
|
97
|
-
<span class="
|
|
98
|
-
|
|
99
|
-
<
|
|
130
|
+
return `<li class="ov-row" data-flag="${escapeHtml(String(f.id))}">
|
|
131
|
+
<div class="ov-row__main">
|
|
132
|
+
<span class="ov-row__title">${title}</span>
|
|
133
|
+
<p class="ov-row__sub cd-reason">${escapeHtml(f.reason)}</p>
|
|
134
|
+
<div class="ov-row__facts">
|
|
135
|
+
${pill('', KIND_LABEL[f.kind] || f.kind)}${targetState(f)}${place}
|
|
136
|
+
<span class="ov-fact">flagged by <b>${escapeHtml(who)}</b></span>
|
|
137
|
+
${closedFacts(f)}
|
|
138
|
+
</div>
|
|
139
|
+
${note}
|
|
100
140
|
</div>
|
|
101
|
-
<div class="
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
141
|
+
<div class="ov-row__right">${when(f.flagged_at)}${actions}</div>
|
|
142
|
+
</li>`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function flagButton(d) {
|
|
146
|
+
return `<div class="ov-row__actions"><button class="btn-ghost cd-flag-btn"
|
|
147
|
+
data-id="${escapeHtml(d.id)}" data-surface="${escapeHtml(d.surface)}" data-text="${escapeHtml(d.text)}">Flag this…</button></div>`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The duplicate's state. Colour marks the EXCEPTION here, the inverse of the
|
|
151
|
+
// queue: most of this list is untouched work, so the untouched row is the
|
|
152
|
+
// plain pill and colour goes to the few already in the queue or looked at.
|
|
153
|
+
function dupeState(d) {
|
|
154
|
+
if (d.flagged) return pill('fact-pill--open', 'already flagged');
|
|
155
|
+
if (d.settled) return pill('fact-pill--ok', 'looked at');
|
|
156
|
+
return pill('', 'untouched');
|
|
108
157
|
}
|
|
109
158
|
|
|
110
159
|
function dupeRow(d) {
|
|
111
|
-
const state = d.flagged
|
|
112
|
-
? '<span class="cd-tag cd-tag--flagged">already flagged</span>'
|
|
113
|
-
: d.settled
|
|
114
|
-
? '<span class="cd-tag cd-tag--settled">looked at</span>'
|
|
115
|
-
: '<span class="cd-tag cd-tag--untouched">untouched</span>';
|
|
116
160
|
const places = (d.at || []).map((a) => `<li>${escapeHtml(a)}</li>`).join('');
|
|
117
|
-
return `<
|
|
118
|
-
<div class="
|
|
119
|
-
|
|
120
|
-
<
|
|
161
|
+
return `<li class="ov-row">
|
|
162
|
+
<div class="ov-row__main">
|
|
163
|
+
<span class="ov-row__title">${quote(d.text)}</span>
|
|
164
|
+
<div class="ov-row__facts">
|
|
165
|
+
${dupeState(d)}
|
|
166
|
+
<span class="ov-fact"><b>${escapeHtml(String(d.occurrences))}</b> places</span>
|
|
167
|
+
<span class="fact-chip">${escapeHtml(d.surface)}</span>
|
|
168
|
+
</div>
|
|
169
|
+
<ul class="cd-places">${places}</ul>
|
|
121
170
|
</div>
|
|
122
|
-
<div class="
|
|
123
|
-
|
|
124
|
-
${d.flagged ? '' : `<div class="cd-card__actions"><button class="btn btn--ghost cd-flag-btn"
|
|
125
|
-
data-id="${escapeHtml(d.id)}" data-surface="${escapeHtml(d.surface)}" data-text="${escapeHtml(d.text)}">Flag this…</button></div>`}
|
|
126
|
-
</article>`;
|
|
171
|
+
<div class="ov-row__right">${d.flagged ? '' : flagButton(d)}</div>
|
|
172
|
+
</li>`;
|
|
127
173
|
}
|
|
128
174
|
|
|
129
175
|
function resultRow(e) {
|
|
130
|
-
return `<
|
|
131
|
-
<div class="
|
|
132
|
-
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
176
|
+
return `<li class="ov-row ov-row--dense">
|
|
177
|
+
<div class="ov-row__main">
|
|
178
|
+
<span class="ov-row__title">${quote(e.text)}</span>
|
|
179
|
+
<div class="ov-row__facts">
|
|
180
|
+
${where(e.file, e.line)}
|
|
181
|
+
<span class="fact-chip">${escapeHtml(e.surface)}</span>
|
|
182
|
+
${e.origin ? `<span class="ov-fact">${escapeHtml(e.origin)}</span>` : ''}
|
|
183
|
+
</div>
|
|
137
184
|
</div>
|
|
138
|
-
|
|
185
|
+
<div class="ov-row__right">${e.flagged ? pill('fact-pill--open', 'already flagged') : flagButton(e)}</div>
|
|
186
|
+
</li>`;
|
|
139
187
|
}
|
|
140
188
|
|
|
141
189
|
// ---------------------------------------------------------------------------
|
|
@@ -146,14 +194,18 @@
|
|
|
146
194
|
const c = queue.counts;
|
|
147
195
|
const r = queue.registry || {};
|
|
148
196
|
const total = (r.counts && r.counts.strings) || 0;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
197
|
+
const surfaces = Object.keys(r.surfaces || {}).length;
|
|
198
|
+
const tile = (label, n, sub) => `<div class="stat-tile">
|
|
199
|
+
<span class="stat-tile__label">${escapeHtml(label)}</span>
|
|
200
|
+
<span class="stat-tile__num">${escapeHtml(int(n))}</span>
|
|
201
|
+
<span class="stat-tile__sub">${escapeHtml(sub)}</span>
|
|
202
|
+
</div>`;
|
|
203
|
+
$('#cd-summary-scroll').innerHTML = [
|
|
204
|
+
tile('Open flags', c.flags_open, c.flags_shown === c.flags_open ? 'in the queue below' : `${int(c.flags_shown)} shown below`),
|
|
205
|
+
tile('Target since edited', c.flags_target_gone, 'probably already fixed; confirm and close'),
|
|
206
|
+
tile('Duplicates untouched', c.duplicates_untouched, `of ${int(c.duplicates_total)} said more than once`),
|
|
207
|
+
tile('Strings inventoried', total, `across ${int(surfaces)} surface${surfaces === 1 ? '' : 's'}`),
|
|
208
|
+
].join('');
|
|
157
209
|
|
|
158
210
|
// Say which artifact this page is describing, and how to check it is current.
|
|
159
211
|
// The registry has no timestamp by design (its generator is deterministic and
|
|
@@ -166,23 +218,43 @@
|
|
|
166
218
|
+ (blind ? ` This inventory has ${blind} documented blind spot${blind === 1 ? '' : 's'} — server-side messages, skill prose and runtime-composed strings are not in it, so it is not every word the project says.` : '');
|
|
167
219
|
}
|
|
168
220
|
|
|
221
|
+
// The queue, as one run when every flag is in the same state (the default,
|
|
222
|
+
// open) and as a group per state when the filter shows everything.
|
|
169
223
|
function paintFlags() {
|
|
170
224
|
const el = $('#cd-flags');
|
|
171
|
-
|
|
172
|
-
|
|
225
|
+
const rows = queue.flags;
|
|
226
|
+
if (!rows.length) {
|
|
227
|
+
el.innerHTML = kit.emptyStateHtml('Nothing flagged here. That is either good news or an unread queue — the duplicates below are the cheapest place to find out.');
|
|
173
228
|
return;
|
|
174
229
|
}
|
|
175
|
-
|
|
230
|
+
const states = ['open', 'resolved', 'dismissed'].filter((s) => rows.some((f) => f.status === s));
|
|
231
|
+
if (states.length < 2) { el.innerHTML = `<ul class="ov-rows">${rows.map(flagRow).join('')}</ul>`; return; }
|
|
232
|
+
el.innerHTML = states.map((s) => {
|
|
233
|
+
const g = rows.filter((f) => f.status === s);
|
|
234
|
+
return `<section class="ov-group">
|
|
235
|
+
<div class="ov-group__head"><span class="ov-group__name">${escapeHtml(s)}</span><span class="ov-group__n">${g.length}</span></div>
|
|
236
|
+
<ul class="ov-rows">${g.map(flagRow).join('')}</ul>
|
|
237
|
+
</section>`;
|
|
238
|
+
}).join('');
|
|
176
239
|
}
|
|
177
240
|
|
|
241
|
+
// Untouched first — it is the only part of this list that is work — and paged
|
|
242
|
+
// through the kit, because the registry carries over a hundred of these.
|
|
178
243
|
function paintDupes() {
|
|
179
244
|
const el = $('#cd-dupes');
|
|
180
245
|
const rows = queue.duplicates || [];
|
|
181
|
-
if (!rows.length) { el.innerHTML = '
|
|
182
|
-
// Untouched first — it is the only part of this list that is work.
|
|
246
|
+
if (!rows.length) { el.innerHTML = kit.emptyStateHtml('No duplicated strings in the registry.'); return; }
|
|
183
247
|
const ordered = [...rows].sort((a, b) =>
|
|
184
248
|
(Number(a.flagged || a.settled) - Number(b.flagged || b.settled)) || (b.occurrences - a.occurrences));
|
|
185
|
-
|
|
249
|
+
if (!dupePager) dupePager = kit.makePager('cd-dupes', 'copy-desk.dupes.perPage', { defaultPerPage: 25 });
|
|
250
|
+
let list = el.querySelector('.ov-rows');
|
|
251
|
+
const rerender = () => { list.innerHTML = kit.paginate(ordered, dupePager).map(dupeRow).join(''); };
|
|
252
|
+
if (!list) {
|
|
253
|
+
el.innerHTML = `<ul class="ov-rows"></ul>${kit.pagerBarHtml('cd-dupes', { label: 'rows' })}`;
|
|
254
|
+
list = el.querySelector('.ov-rows');
|
|
255
|
+
kit.wirePager(dupePager, rerender);
|
|
256
|
+
}
|
|
257
|
+
rerender();
|
|
186
258
|
}
|
|
187
259
|
|
|
188
260
|
function paintSurfaces() {
|
|
@@ -209,8 +281,8 @@
|
|
|
209
281
|
$('#cd-sub').textContent = code === 'registry_missing'
|
|
210
282
|
? `No copy registry on this instance yet. Run ${detail.run || 'node scripts/gds/copy-inventory.js'} and commit it.`
|
|
211
283
|
: 'Could not load the copy desk.';
|
|
212
|
-
$('#cd-flags').innerHTML = '
|
|
213
|
-
$('#cd-dupes').innerHTML = '
|
|
284
|
+
$('#cd-flags').innerHTML = kit.emptyStateHtml('—');
|
|
285
|
+
$('#cd-dupes').innerHTML = kit.emptyStateHtml('—');
|
|
214
286
|
return;
|
|
215
287
|
}
|
|
216
288
|
queue = res.data;
|
|
@@ -237,7 +309,7 @@
|
|
|
237
309
|
$('#cd-search-count').textContent = truncated
|
|
238
310
|
? `${matched} matches — showing the first ${limit}. Narrow the search to see the rest.`
|
|
239
311
|
: `${matched} match${matched === 1 ? '' : 'es'}.`;
|
|
240
|
-
$('#cd-results').innerHTML = entries.map(resultRow).join('')
|
|
312
|
+
$('#cd-results').innerHTML = `<ul class="ov-rows">${entries.map(resultRow).join('')}</ul>`;
|
|
241
313
|
}
|
|
242
314
|
|
|
243
315
|
// ---------------------------------------------------------------------------
|
|
@@ -247,8 +319,8 @@
|
|
|
247
319
|
function openComposer(t) {
|
|
248
320
|
target = t;
|
|
249
321
|
$('#cd-dialog-target').innerHTML = t.string_id
|
|
250
|
-
? `${quote(t.text)}<br><span class="
|
|
251
|
-
:
|
|
322
|
+
? `${quote(t.text)}<br><span class="ov-key ov-key--faint">${escapeHtml(t.surface)}</span>`
|
|
323
|
+
: `${pill('', 'whole surface')} <span class="ov-key ov-key--faint">${escapeHtml(t.surface)}</span>`;
|
|
252
324
|
$('#cd-reason').value = '';
|
|
253
325
|
$('#cd-dialog-err').textContent = '';
|
|
254
326
|
$('#cd-dialog').showModal();
|
|
@@ -286,7 +358,14 @@
|
|
|
286
358
|
function openCloser(id) {
|
|
287
359
|
closing = queue.flags.find((f) => String(f.id) === String(id));
|
|
288
360
|
if (!closing) return;
|
|
289
|
-
|
|
361
|
+
// What is being closed: the string (the product's words, as evidence) or
|
|
362
|
+
// the surface, then the reason as the prose it is — not the reason in the
|
|
363
|
+
// evidence voice, which is what the first cut quoted.
|
|
364
|
+
const about = closing.scope === 'surface'
|
|
365
|
+
? `${pill('', 'whole surface')} <span class="ov-key ov-key--faint">${escapeHtml(closing.surface)}</span>`
|
|
366
|
+
: quote(closing.current ? closing.current.text : closing.text_at_flag, 120);
|
|
367
|
+
const reason = String(closing.reason || '');
|
|
368
|
+
$('#cd-close-target').innerHTML = `${about}<br>${escapeHtml(reason.length > 160 ? `${reason.slice(0, 160)}…` : reason)}`;
|
|
290
369
|
$('#cd-close-note').value = '';
|
|
291
370
|
$('#cd-close-task').value = '';
|
|
292
371
|
$('#cd-close-err').textContent = '';
|
|
@@ -319,6 +398,25 @@
|
|
|
319
398
|
// wire
|
|
320
399
|
// ---------------------------------------------------------------------------
|
|
321
400
|
|
|
401
|
+
// The queue's filters and the search field are the kit's filter bar, so the
|
|
402
|
+
// controls are the shell's own; the ids the loaders read are kept.
|
|
403
|
+
kit.makeFilterBar({
|
|
404
|
+
mount: $('#cd-filters'),
|
|
405
|
+
primary: [
|
|
406
|
+
{ id: 'cd-status', label: 'Show', options: [
|
|
407
|
+
{ value: 'open', label: 'Open' }, { value: 'resolved', label: 'Resolved' },
|
|
408
|
+
{ value: 'dismissed', label: 'Dismissed' }, { value: 'all', label: 'Everything' },
|
|
409
|
+
] },
|
|
410
|
+
{ id: 'cd-surface', label: 'Surface', options: [{ value: '', label: 'All surfaces' }] },
|
|
411
|
+
],
|
|
412
|
+
onChange: () => { loadQueue(); if ($('#cd-search').value.trim()) runSearch(); },
|
|
413
|
+
});
|
|
414
|
+
kit.makeFilterBar({
|
|
415
|
+
mount: $('#cd-find'),
|
|
416
|
+
search: { id: 'cd-search', label: 'Search the copy', placeholder: 'a phrase you saw, or a file name…' },
|
|
417
|
+
onChange: () => { clearTimeout(searchTimer); searchTimer = setTimeout(runSearch, 200); },
|
|
418
|
+
});
|
|
419
|
+
|
|
322
420
|
document.addEventListener('click', (ev) => {
|
|
323
421
|
const flagBtn = ev.target.closest('.cd-flag-btn');
|
|
324
422
|
if (flagBtn) {
|
|
@@ -337,14 +435,11 @@
|
|
|
337
435
|
$('#cd-submit').addEventListener('click', submitFlag);
|
|
338
436
|
$('#cd-close-cancel').addEventListener('click', () => $('#cd-close-dialog').close());
|
|
339
437
|
$('#cd-close-submit').addEventListener('click', submitClose);
|
|
340
|
-
$('#cd-status').addEventListener('change', loadQueue);
|
|
341
|
-
$('#cd-surface').addEventListener('change', () => { loadQueue(); if ($('#cd-search').value.trim()) runSearch(); });
|
|
342
|
-
$('#cd-search').addEventListener('input', () => {
|
|
343
|
-
clearTimeout(searchTimer);
|
|
344
|
-
searchTimer = setTimeout(runSearch, 200);
|
|
345
|
-
});
|
|
346
438
|
|
|
347
439
|
(async () => {
|
|
440
|
+
// The closing floor is written from the pack's rank label, never a hardcoded
|
|
441
|
+
// Greek name (ADR 0157 moved the floor; the pack owns the word).
|
|
442
|
+
if (window.OTB.rankLabel && $('#cd-close-rank')) $('#cd-close-rank').textContent = window.OTB.rankLabel('metic');
|
|
348
443
|
const me = await api.request('GET', `${API}/me`);
|
|
349
444
|
if (me.ok) viewer = me.data.builder || me.data;
|
|
350
445
|
await loadQueue();
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_": "Copy desk — the one-voice review queue. Its
|
|
2
|
+
"_": "Copy desk — the one-voice review queue. Its two dialogs are real <dialog>s, so their ::backdrop is the sheet's single sanctioned literal (black with alpha as a scrim). Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once; the redesign (task 1003471) put a queue, ten duplicates and a search's hits in the harness so the rows and both dialogs can be looked at.",
|
|
3
3
|
"page": "/builders/copy-desk",
|
|
4
4
|
"surface": "hall-ui",
|
|
5
5
|
"stub": {
|
|
6
6
|
"prefix": "/builders/"
|
|
7
7
|
},
|
|
8
8
|
"modeQuery": false,
|
|
9
|
-
"_feeds": "The kit's own stub carries no feed for this page, so against it the page renders its EMPTY
|
|
9
|
+
"_feeds": "The kit's own stub carries no feed for this page, so against it the page renders its EMPTY state — which is the point: that is a state the floors must hold in. Against the hall-preview harness the same states render populated: copy-desk__queue.json (six flags across open, resolved and dismissed, with a gone target, a moved one and a whole-surface one; ten real duplicates) and copy-desk__strings.json (a search's hits). Every declared 404 below is a feed the kit's stub does not carry, named rather than hidden. The composer and closer states use clickIfVisible because against the stub the buttons do not exist.",
|
|
10
10
|
"ignoreRequests": [
|
|
11
11
|
"/api/(gds|bongos)/me$",
|
|
12
12
|
"/api/(gds|bongos)/me/",
|
|
@@ -45,6 +45,63 @@
|
|
|
45
45
|
},
|
|
46
46
|
"out": {
|
|
47
47
|
"auth": false
|
|
48
|
+
},
|
|
49
|
+
"search": {
|
|
50
|
+
"auth": true,
|
|
51
|
+
"actions": [
|
|
52
|
+
[
|
|
53
|
+
"wait",
|
|
54
|
+
700
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
"fill",
|
|
58
|
+
"#cd-search",
|
|
59
|
+
"save"
|
|
60
|
+
],
|
|
61
|
+
[
|
|
62
|
+
"wait",
|
|
63
|
+
600
|
|
64
|
+
]
|
|
65
|
+
],
|
|
66
|
+
"expect": {
|
|
67
|
+
"visible": [
|
|
68
|
+
"#cd-results"
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"composer": {
|
|
73
|
+
"auth": true,
|
|
74
|
+
"actions": [
|
|
75
|
+
[
|
|
76
|
+
"wait",
|
|
77
|
+
700
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
"clickIfVisible",
|
|
81
|
+
".cd-flag-btn"
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
"wait",
|
|
85
|
+
250
|
|
86
|
+
]
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"closer": {
|
|
90
|
+
"auth": true,
|
|
91
|
+
"actions": [
|
|
92
|
+
[
|
|
93
|
+
"wait",
|
|
94
|
+
700
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
"clickIfVisible",
|
|
98
|
+
".cd-close-btn"
|
|
99
|
+
],
|
|
100
|
+
[
|
|
101
|
+
"wait",
|
|
102
|
+
250
|
|
103
|
+
]
|
|
104
|
+
]
|
|
48
105
|
}
|
|
49
106
|
},
|
|
50
107
|
"reducedMotion": {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/* oversight.css — the oversight family's archetype (task 1003469, goal
|
|
2
|
-
1000081): what
|
|
2
|
+
1000081): what an oversight ROW is in the chrome world, and the pieces the
|
|
3
3
|
four pages (government, watch, harbor, gate) share around it — the figure
|
|
4
4
|
band inside a panel, the group head, the inline form, the grant toggle, the
|
|
5
5
|
confirm modal. The third family sheet after record.css and room.css: a page
|
|
6
|
-
sheet holds only what one page has and the others do not.
|
|
6
|
+
sheet holds only what one page has and the others do not. The collaboration
|
|
7
|
+
pages (collab, copy desk — task 1003471) ride the same row rather than a
|
|
8
|
+
fourth: a page whose repeated thing is a titled row with facts and actions
|
|
9
|
+
links this sheet before its own.
|
|
7
10
|
|
|
8
11
|
Every panel is the shell's .scroll; every heading its .scroll__h; every state
|
|
9
12
|
pill its .fact-pill; every table its .ledger. This sheet adds nothing the
|
|
@@ -150,6 +153,12 @@ button.ov-row[aria-pressed="true"] .ov-row__title { color: var(--accent-ink); }
|
|
|
150
153
|
The shell's glass .stat-row, given the air a panel would have under it. */
|
|
151
154
|
.ov-summary { margin: 0 0 var(--sp-md); }
|
|
152
155
|
|
|
156
|
+
/* A chip that is a door (the task or goal a row is about) keeps the chip's
|
|
157
|
+
faint ink at rest — a coloured chip would read as a second status — and
|
|
158
|
+
takes the link ink on hover, the way the row's title link does. */
|
|
159
|
+
a.fact-chip { color: var(--ink-faint); }
|
|
160
|
+
a.fact-chip:hover { color: var(--accent-ink); border-color: var(--accent-ink); text-decoration: none; }
|
|
161
|
+
|
|
153
162
|
/* ---- plain facts and the machine's words ------------------------------------ */
|
|
154
163
|
.ov-fact { font-size: 12.5px; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
|
|
155
164
|
.ov-fact b { color: var(--ink-soft); font-weight: 500; }
|
|
@@ -49,6 +49,16 @@ async function craftsForBuilder(builderId, deps = {}) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const COLS = 'id, requested_by, needs_craft, needs_builder_id, what_is_stuck, task_id, goal_id, status, created_at, updated_at';
|
|
52
|
+
// The two list reads also NAME the people on a row — who is asking (the fact
|
|
53
|
+
// the collab page owes a helper: "who to answer", task 1003471) and who a
|
|
54
|
+
// by-name ask is addressed to — through two LEFT JOINs on builders. COLS stays
|
|
55
|
+
// bare for the writes, whose RETURNING cannot reach a joined alias.
|
|
56
|
+
const LIST_COLS = `${COLS.split(', ').map((c) => `h.${c}`).join(', ')}, `
|
|
57
|
+
+ 'a.github_login AS requested_by_login, a.display_name AS requested_by_name, '
|
|
58
|
+
+ 'n.github_login AS needs_builder_login, n.display_name AS needs_builder_name';
|
|
59
|
+
const LIST_FROM = 'FROM lifecycle_help_requests h '
|
|
60
|
+
+ 'LEFT JOIN builders a ON a.id = h.requested_by '
|
|
61
|
+
+ 'LEFT JOIN builders n ON n.id = h.needs_builder_id';
|
|
52
62
|
const MAX_STUCK = 4000;
|
|
53
63
|
// Defence in depth: both reads are bounded today (open-status churn, per-builder
|
|
54
64
|
// scope), but an unsettled pile should degrade the page, not the server.
|
|
@@ -130,11 +140,11 @@ async function listOpenForBuilder({ builderId, crafts = [], limit = MAX_ROWS },
|
|
|
130
140
|
const activePool = deps.pool || pool;
|
|
131
141
|
const safeCrafts = (Array.isArray(crafts) ? crafts : []).filter((c) => HELP_CRAFTS.includes(c));
|
|
132
142
|
const { rows } = await activePool.query(
|
|
133
|
-
`SELECT ${
|
|
134
|
-
WHERE status = 'open'
|
|
135
|
-
AND requested_by <> $1
|
|
136
|
-
AND (needs_builder_id = $1 OR needs_craft = ANY($2::text[]))
|
|
137
|
-
ORDER BY created_at DESC, id DESC
|
|
143
|
+
`SELECT ${LIST_COLS} ${LIST_FROM}
|
|
144
|
+
WHERE h.status = 'open'
|
|
145
|
+
AND h.requested_by <> $1
|
|
146
|
+
AND (h.needs_builder_id = $1 OR h.needs_craft = ANY($2::text[]))
|
|
147
|
+
ORDER BY h.created_at DESC, h.id DESC
|
|
138
148
|
LIMIT $3`,
|
|
139
149
|
[builderId, safeCrafts, capRows(limit)]
|
|
140
150
|
);
|
|
@@ -181,9 +191,9 @@ function craftMapSql(col) {
|
|
|
181
191
|
async function listMine({ builderId, status = null, limit = MAX_ROWS }, deps = {}) {
|
|
182
192
|
const activePool = deps.pool || pool;
|
|
183
193
|
const { rows } = await activePool.query(
|
|
184
|
-
`SELECT ${
|
|
185
|
-
WHERE requested_by = $1 AND ($2::text IS NULL OR status = $2)
|
|
186
|
-
ORDER BY created_at DESC, id DESC
|
|
194
|
+
`SELECT ${LIST_COLS} ${LIST_FROM}
|
|
195
|
+
WHERE h.requested_by = $1 AND ($2::text IS NULL OR h.status = $2)
|
|
196
|
+
ORDER BY h.created_at DESC, h.id DESC
|
|
187
197
|
LIMIT $3`,
|
|
188
198
|
[builderId, status, capRows(limit)]
|
|
189
199
|
);
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
The stub, the renderer/auditor, the interaction probes, the mock checker and the contrast probe any builder points at any surface of any instance ([task 1003320](https://cloudbongos.com/builders#/task/1003320), [ADR 0197](../../../docs/adr/<redacted>.md)). Plain Node; the browser is the system Edge or Chrome via `playwright-core` (a devDependency — nothing is downloaded).
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
node modules/ui-design/kit/serve.js # the stub (SURFACE, PREFIX, AUTH=1, CHILD=1, LIVE=…, FAIL=…, STUB_PORT)
|
|
7
|
-
node modules/ui-design/kit/render.js --page /projects # every state × 1440/390/320 × dark/light, audited against the floors
|
|
8
|
-
node modules/ui-design/kit/probe.js --page /projects # the interaction contract as PASS/FAIL lines
|
|
9
|
-
node modules/ui-design/kit/check-mock.js --file docs/design/mocks/hub/v2.html
|
|
10
|
-
node modules/ui-design/kit/contrast.js --pack | --url <url> [--glyphs sel,sel]
|
|
6
|
+
node modules/ui-design/kit/serve.js # the stub (SURFACE, PREFIX, AUTH=1, CHILD=1, LIVE=…, FAIL=…, STYLE=<look>, STUB_PORT)
|
|
7
|
+
node modules/ui-design/kit/render.js --page /projects # every state × 1440/390/320 × dark/light, audited against the floors (--style <look>)
|
|
8
|
+
node modules/ui-design/kit/probe.js --page /projects # the interaction contract as PASS/FAIL lines (--style <look>)
|
|
9
|
+
node modules/ui-design/kit/check-mock.js --file docs/design/mocks/hub/v2.html # (--pack <look> for a style's own mock)
|
|
10
|
+
node modules/ui-design/kit/contrast.js --pack [look] | --url <url> [--glyphs sel,sel]
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
A **look** is a branding pack from the module's style library (`modules/ui-design/styles/<name>/`, task 1003323): `STYLE=<name>` lays it over the instance's own pack through `GDS_BRANDING_FILE`, so any surface renders under any look with no page edit.
|
|
14
|
+
|
|
13
15
|
A surface describes itself in two JSON files beside it — `<page>.states.json` and `<page>.probes.json` — in the vocabularies `lib.js` defines and validates. The full loop, the file shapes and the failure table: [`docs/recipes/ui-look-before-you-ship.md`](../../../docs/recipes/ui-look-before-you-ship.md). Exit codes everywhere: 0 clean · 1 problems · 2 a broken file or usage · 3 no browser.
|
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
// shadow). The em dash ban is Cloud Bongos's, not the platform's: an instance
|
|
15
15
|
// whose DESIGN.md never mentions it is not held to it.
|
|
16
16
|
//
|
|
17
|
-
// node modules/ui-design/kit/check-mock.js --file <mock.html> [--states a,b] [--keys THESIS,BAR] [--design <DESIGN.md>] [--no-taste]
|
|
17
|
+
// node modules/ui-design/kit/check-mock.js --file <mock.html> [--states a,b] [--keys THESIS,BAR] [--design <DESIGN.md>] [--no-taste] [--pack <style>]
|
|
18
18
|
//
|
|
19
|
+
// `--pack <style>` checks a style's mock against that look's own colours (the pinned-literal
|
|
20
|
+
// rule) and, unless --design says otherwise, its own DESIGN.md bans (task 1003323).
|
|
19
21
|
// Prints PASS/FAIL/WARN per rule; exit 1 on any FAIL, 2 on usage.
|
|
20
22
|
const fs = require('node:fs');
|
|
21
23
|
const path = require('node:path');
|
|
@@ -89,7 +91,8 @@ function checkMock(html, { bans = new Set(), packColors = {}, states = [], keys
|
|
|
89
91
|
ok('no literal hex colour (the fifteen via var()/color-mix(); black or white with alpha only)', hexes.length === 0, hexes.length ? [...new Set(hexes)].slice(0, 12).join(',') : '');
|
|
90
92
|
const rgbLit = [...css.matchAll(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/g)].filter((x) => !((x[1] === '0' && x[2] === '0' && x[3] === '0') || (x[1] === '255' && x[2] === '255' && x[3] === '255')));
|
|
91
93
|
ok('rgb() literals only as black or white with alpha', rgbLit.length === 0, rgbLit.length ? `${rgbLit.length} found` : '');
|
|
92
|
-
|
|
94
|
+
// a pack whose card is pure white (the expedition's) must not turn the one legal literal into a pin
|
|
95
|
+
const pinned = Object.entries(packColors).filter(([, v]) => typeof v === 'string' && /^#[0-9a-f]{6}$/i.test(v) && !BLACK_OR_WHITE.test(v.slice(1).toLowerCase()) && new RegExp(v.replace('#', '#'), 'i').test(css)).map(([k]) => k);
|
|
93
96
|
ok('no pack colour pinned as a literal (a var() of the contract token instead)', pinned.length === 0, pinned.join(','));
|
|
94
97
|
|
|
95
98
|
// ── the taste bans the instance declared ──
|
|
@@ -126,16 +129,20 @@ function checkMock(html, { bans = new Set(), packColors = {}, states = [], keys
|
|
|
126
129
|
function main() {
|
|
127
130
|
const args = process.argv.slice(2);
|
|
128
131
|
const file = lib.argOf(args, '--file', null);
|
|
129
|
-
if (!file || lib.hasFlag(args, '--help')) { process.stderr.write('usage: node modules/ui-design/kit/check-mock.js --file <mock.html> [--states a,b] [--keys A,B] [--design <DESIGN.md>] [--no-taste]\n'); process.exit(lib.EXIT.USAGE); }
|
|
132
|
+
if (!file || lib.hasFlag(args, '--help')) { process.stderr.write('usage: node modules/ui-design/kit/check-mock.js --file <mock.html> [--states a,b] [--keys A,B] [--design <DESIGN.md>] [--no-taste] [--pack <style>]\n'); process.exit(lib.EXIT.USAGE); }
|
|
130
133
|
if (!fs.existsSync(file)) { process.stderr.write(`check-mock: not found: ${file}\n`); process.exit(lib.EXIT.USAGE); }
|
|
131
|
-
const
|
|
134
|
+
const styleArg = lib.argOf(args, '--pack', null);
|
|
135
|
+
const style = styleArg ? lib.resolveStyle(styleArg) : null;
|
|
136
|
+
if (styleArg && !style) { process.stderr.write(`check-mock: --pack "${styleArg}" is not a style under modules/ui-design/styles/ (${lib.listStyles().join(', ') || 'none'})\n`); process.exit(lib.EXIT.USAGE); }
|
|
137
|
+
const designPath = lib.argOf(args, '--design', style ? path.join(style.dir, 'DESIGN.md') : path.join(lib.REPO_ROOT, 'DESIGN.md'));
|
|
132
138
|
let bans = new Set();
|
|
133
139
|
if (!lib.hasFlag(args, '--no-taste')) {
|
|
134
140
|
if (fs.existsSync(designPath)) bans = readDesignBans(fs.readFileSync(designPath, 'utf8'));
|
|
135
141
|
else process.stdout.write(`WARN taste bans off: no DESIGN.md at ${designPath} (the platform rules still apply)\n`);
|
|
136
142
|
}
|
|
137
143
|
let packColors = {};
|
|
138
|
-
|
|
144
|
+
if (style) packColors = lib.packColors(style.pack);
|
|
145
|
+
else { try { packColors = lib.packColors(lib.instancePack()); } catch { packColors = {}; } }
|
|
139
146
|
const results = checkMock(fs.readFileSync(file, 'utf8'), { bans, packColors, states: lib.listArg(args, '--states', []), keys: lib.listArg(args, '--keys', []) });
|
|
140
147
|
for (const [s, rule, detail] of results) process.stdout.write(`${s.padEnd(4)} ${rule}${detail ? ' (' + detail + ')' : ''}\n`);
|
|
141
148
|
const fails = results.filter((r) => r[0] === 'FAIL').length;
|