@bongos/core 1.19.459 → 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 +30 -25
- package/docs/copy-inventory.md +52 -61
- package/docs/copy-registry.json +92 -203
- package/docs/module-api-changelog.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/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/hall_collab_world.mjs +214 -0
|
@@ -1355,5 +1355,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1355
1355
|
landed since 1.19.457 with no explicit bump. run 33566876510. (task 1002620)
|
|
1356
1356
|
1.19.459 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1357
1357
|
landed since 1.19.458 with no explicit bump. run 33568609735. (task 1002620)
|
|
1358
|
+
1.19.460 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1359
|
+
landed since 1.19.459 with no explicit bump. run 33571137958. (task 1002620)
|
|
1358
1360
|
---------------------------------------------------------------------------
|
|
1359
1361
|
```
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// modules/hall-ui/public/collab-lib.js — the Collab page's PURE helpers, exposed
|
|
2
|
-
// as window.OTBCollabLib (task 1003172 / C02, goal 1000075
|
|
2
|
+
// as window.OTBCollabLib (task 1003172 / C02, goal 1000075; the row's first
|
|
3
|
+
// line, the asker and the groups added by the redesign, task 1003471).
|
|
3
4
|
//
|
|
4
5
|
// Same split as goals-lib.js: everything here is a pure function of its
|
|
5
6
|
// arguments, so the rules can be executed by a test instead of grepped out of a
|
|
@@ -52,15 +53,29 @@
|
|
|
52
53
|
|
|
53
54
|
// Who a request is addressed to, in words. A craft request and a by-name
|
|
54
55
|
// request read differently on purpose: "asked of builders" is a call anyone of
|
|
55
|
-
// that craft may take, "asked of you" is personal and should feel it.
|
|
56
|
+
// that craft may take, "asked of you" is personal and should feel it. A
|
|
57
|
+
// by-name ask that is not yours names the person when the server joined a
|
|
58
|
+
// name, and says "one person" when it did not.
|
|
56
59
|
function addressedTo(req, viewerId) {
|
|
57
60
|
if (req && req.needs_builder_id != null) {
|
|
58
|
-
|
|
61
|
+
if (String(req.needs_builder_id) === String(viewerId)) return 'asked of you by name';
|
|
62
|
+
var name = req.needs_builder_name || req.needs_builder_login;
|
|
63
|
+
return name ? 'asked of ' + name + ' by name' : 'asked of one person';
|
|
59
64
|
}
|
|
60
65
|
if (req && req.needs_craft) return 'asked of ' + req.needs_craft + 's';
|
|
61
66
|
return 'asked';
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
// Who is ASKING, in words — the fact a helper needs to answer. The name the
|
|
70
|
+
// server joined, else the login, else the bare id an older server sends;
|
|
71
|
+
// never nothing.
|
|
72
|
+
function askerOf(req) {
|
|
73
|
+
if (!req) return '';
|
|
74
|
+
if (req.requested_by_name) return String(req.requested_by_name);
|
|
75
|
+
if (req.requested_by_login) return String(req.requested_by_login);
|
|
76
|
+
return req.requested_by != null ? 'builder ' + req.requested_by : '';
|
|
77
|
+
}
|
|
78
|
+
|
|
64
79
|
// What the request is about, if anything. Returns null rather than an empty
|
|
65
80
|
// string so a caller can skip the element entirely.
|
|
66
81
|
function contextOf(req) {
|
|
@@ -70,8 +85,29 @@
|
|
|
70
85
|
return null;
|
|
71
86
|
}
|
|
72
87
|
|
|
88
|
+
// The row's first line. A help request has no title field, so the ask's
|
|
89
|
+
// first sentence is the title and the rest is the sub line — both the asker's
|
|
90
|
+
// own words, nothing clipped. A first sentence too long to be a line, or an
|
|
91
|
+
// ask with no sentence break, is the whole ask.
|
|
92
|
+
var TITLE_MAX = 140;
|
|
93
|
+
function titleOf(text) {
|
|
94
|
+
var s = String(text == null ? '' : text).trim();
|
|
95
|
+
var m = /^([\s\S]*?[.?!])\s+([\s\S]+)$/.exec(s);
|
|
96
|
+
if (!m || m[1].length > TITLE_MAX) return { title: s, rest: '' };
|
|
97
|
+
return { title: m[1], rest: m[2].trim() };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The "asked of you" list reads in runs: what is asked of you BY NAME first
|
|
101
|
+
// (a personal ask is the one you owe an answer), then one run per craft.
|
|
102
|
+
function groupOf(req) {
|
|
103
|
+
if (req && req.needs_builder_id != null) return { id: 'you', label: 'asked of you by name' };
|
|
104
|
+
if (req && req.needs_craft) return { id: req.needs_craft, label: 'asked of ' + req.needs_craft + 's' };
|
|
105
|
+
return { id: 'other', label: 'asked' };
|
|
106
|
+
}
|
|
107
|
+
|
|
73
108
|
window.OTBCollabLib = {
|
|
74
109
|
BANDS: BANDS, ageBand: ageBand, byAgeOldestFirst: byAgeOldestFirst,
|
|
75
|
-
addressedTo: addressedTo, contextOf: contextOf,
|
|
110
|
+
addressedTo: addressedTo, askerOf: askerOf, contextOf: contextOf,
|
|
111
|
+
titleOf: titleOf, groupOf: groupOf,
|
|
76
112
|
};
|
|
77
113
|
})();
|
|
@@ -1,36 +1,20 @@
|
|
|
1
|
-
/* modules/hall-ui/public/collab.css — the Collab page (task 1003172 / C02)
|
|
2
|
-
|
|
3
|
-
is
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
.
|
|
8
|
-
|
|
9
|
-
border-left: 3px solid var(--rule);
|
|
10
|
-
border-radius: var(--radius-sm);
|
|
11
|
-
background: var(--bg-card);
|
|
12
|
-
padding: 10px 12px;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.cb-item__stuck { margin: 0; color: var(--ink); font-size: 14px; line-height: 1.5; white-space: pre-wrap; }
|
|
16
|
-
.cb-item__meta { margin: 6px 0 0; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 12px; }
|
|
17
|
-
.cb-item__actions { margin: 8px 0 0; display: flex; gap: 8px; }
|
|
18
|
-
.cb-who { color: var(--ink-soft); }
|
|
19
|
-
.cb-ctx { color: var(--accent); text-decoration: none; }
|
|
20
|
-
.cb-ctx:hover { text-decoration: underline; }
|
|
21
|
-
.cb-empty { color: var(--ink-faint); font-size: 13px; margin: 10px 0 0; }
|
|
1
|
+
/* modules/hall-ui/public/collab.css — the Collab page (task 1003172 / C02),
|
|
2
|
+
rebuilt on the hall's row archetype by task 1003471: the panel is the shell's
|
|
3
|
+
.scroll, the row is oversight.css's .ov-row, the age is a .fact-pill, the
|
|
4
|
+
context a .fact-chip. This sheet holds only what Collab has and no other
|
|
5
|
+
page does — the colour of each age band.
|
|
6
|
+
Tokens only from style.css's real set: inventing one renders as nothing,
|
|
7
|
+
which is how copy-desk.css once ended up with invisible fallbacks (idea
|
|
8
|
+
1000821). */
|
|
22
9
|
|
|
23
10
|
/* THE AGEING SIGNAL. The criterion asks that a request nobody takes be visibly
|
|
24
|
-
aging rather than silently dead — so age
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.cb-
|
|
31
|
-
.cb-
|
|
32
|
-
.cb-
|
|
33
|
-
.cb-
|
|
34
|
-
.cb-item--stale .cb-age { color: var(--danger); font-weight: 600; }
|
|
35
|
-
|
|
36
|
-
.btn--quiet { background: transparent; color: var(--ink-soft); border-color: var(--rule); }
|
|
11
|
+
aging rather than silently dead — so the age is a state pill in the row's
|
|
12
|
+
right column: read down the list and the column says it in words AND in the
|
|
13
|
+
verification colours, because colour alone is not a message and not everyone
|
|
14
|
+
sees it. Four bands, four treatments: faint, soft, warn, danger. The 3px
|
|
15
|
+
coloured band the first cut painted down the row's left edge is gone — this
|
|
16
|
+
world marks state with a pill, never with a bar. */
|
|
17
|
+
.cb-age--fresh { color: var(--ink-faint); }
|
|
18
|
+
.cb-age--waiting { color: var(--ink-soft); }
|
|
19
|
+
.cb-age--aging { color: var(--warn); }
|
|
20
|
+
.cb-age--stale { color: var(--danger); font-weight: 600; }
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
11
|
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
12
|
+
<link rel="stylesheet" href="/builders/oversight.css">
|
|
12
13
|
<link rel="stylesheet" href="/builders/collab.css">
|
|
13
14
|
</head>
|
|
14
15
|
<body data-page="collab" data-page-title="Collab">
|
|
@@ -19,11 +20,9 @@
|
|
|
19
20
|
<div class="app-body">
|
|
20
21
|
<header class="app-topbar" id="app-topbar"></header>
|
|
21
22
|
<div class="app-content">
|
|
22
|
-
<!--
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
is the LABEL voice, not a display line: on a working surface the
|
|
26
|
-
page's own name is the least interesting thing on it. -->
|
|
23
|
+
<!-- The board page head: the page's own name in the LABEL voice, not a
|
|
24
|
+
display line — on a working surface it is the least interesting
|
|
25
|
+
thing on it. -->
|
|
27
26
|
<div class="page-head page-head--board">
|
|
28
27
|
<h1 class="page-head__h">Collab</h1>
|
|
29
28
|
<p class="page-head__sub" id="cb-sub">Loading…</p>
|
|
@@ -32,28 +31,29 @@
|
|
|
32
31
|
<main id="hall-main">
|
|
33
32
|
<!-- ASKED OF YOU comes first: it is the half where someone is waiting on a
|
|
34
33
|
human. Your own asks are below, because they are a status check, not a
|
|
35
|
-
call to act.
|
|
34
|
+
call to act. The rows are the hall's row archetype (oversight.css);
|
|
35
|
+
collab.js fills the mount with one run per addressee. -->
|
|
36
36
|
<section class="scroll" aria-labelledby="cb-for-me-h">
|
|
37
37
|
<h2 class="scroll__h" id="cb-for-me-h">Asked of you</h2>
|
|
38
|
-
<p class="
|
|
38
|
+
<p class="scroll__lede" id="cb-for-me-note">
|
|
39
39
|
Requests addressed to your craft, or to you by name. Answering is optional
|
|
40
40
|
and declining costs nothing — nobody is assigned by being asked.
|
|
41
41
|
</p>
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
Nothing is waiting on you. Requests reach you by your craft — set yours in
|
|
45
|
-
<a href="/builders/settings">Settings</a> if this stays empty
|
|
46
|
-
</
|
|
42
|
+
<div id="cb-for-me"></div>
|
|
43
|
+
<div class="empty-state" id="cb-for-me-empty" hidden>
|
|
44
|
+
<p>Nothing is waiting on you. Requests reach you by your craft — set yours in
|
|
45
|
+
<a href="/builders/settings">Settings</a> if this stays empty.</p>
|
|
46
|
+
</div>
|
|
47
47
|
</section>
|
|
48
48
|
|
|
49
49
|
<section class="scroll" aria-labelledby="cb-mine-h">
|
|
50
50
|
<h2 class="scroll__h" id="cb-mine-h">Your asks</h2>
|
|
51
|
-
<p class="
|
|
51
|
+
<p class="scroll__lede">
|
|
52
52
|
Only you can settle your own request — mark it answered when someone
|
|
53
53
|
helped, or withdraw it when you no longer need it.
|
|
54
54
|
</p>
|
|
55
|
-
<ul class="
|
|
56
|
-
<
|
|
55
|
+
<ul class="ov-rows" id="cb-mine"></ul>
|
|
56
|
+
<div class="empty-state" id="cb-mine-empty" hidden><p>You have no open asks.</p></div>
|
|
57
57
|
</section>
|
|
58
58
|
</main>
|
|
59
59
|
</div>
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
<script src="/builders/shell.js?v=2026-07-02-shell1"></script>
|
|
66
66
|
<script src="/builders/palette.js" defer></script>
|
|
67
67
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
68
|
-
<script src="/builders/collab-lib.js?v=2026-
|
|
69
|
-
<script src="/builders/collab.js?v=2026-
|
|
68
|
+
<script src="/builders/collab-lib.js?v=2026-09-01-redesign"></script>
|
|
69
|
+
<script src="/builders/collab.js?v=2026-09-01-redesign"></script>
|
|
70
70
|
</body>
|
|
71
71
|
</html>
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// modules/hall-ui/public/collab.js — the Collab page (/builders/collab).
|
|
2
|
-
// Task 1003172 / C02 of goal 1000075, criterion collab-page-matches-my-craft
|
|
2
|
+
// Task 1003172 / C02 of goal 1000075, criterion collab-page-matches-my-craft;
|
|
3
|
+
// rebuilt on the hall's row archetype by task 1003471 (goal 1000081).
|
|
3
4
|
//
|
|
4
|
-
// The hall surface for C01's help requests. Two
|
|
5
|
+
// The hall surface for C01's help requests. Two panels, in this order on
|
|
5
6
|
// purpose: what is ASKED OF YOU first, because someone is waiting on a human
|
|
6
7
|
// answer, then YOUR OWN ASKS, which are a status check rather than a call to act.
|
|
8
|
+
// "Asked of you" reads in runs — by name first, then each craft — because a
|
|
9
|
+
// personal ask is the one you owe an answer.
|
|
7
10
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// A row is the family's .ov-row: the ask's first sentence as the title and the
|
|
12
|
+
// rest as the sub (collab-lib titleOf), WHO IS ASKING as a fact with a door to
|
|
13
|
+
// their profile, the task or goal as a chip, and the AGE as a state pill in the
|
|
14
|
+
// right column, so a glance down the list reads the ages in words and in the
|
|
15
|
+
// verification colours. The band rules live in collab-lib.js where a test can
|
|
16
|
+
// execute them.
|
|
13
17
|
//
|
|
14
18
|
// MATCHING IS THE SERVER'S JOB, not this file's. GET /help-requests/for-me derives
|
|
15
19
|
// the viewer's crafts from their own profile; sending a craft from here would be
|
|
@@ -31,15 +35,38 @@
|
|
|
31
35
|
|
|
32
36
|
let settling = false;
|
|
33
37
|
|
|
38
|
+
// The age pill: the band's words, in the band's colour (collab.css .cb-age--*).
|
|
34
39
|
function ageChip(req, nowMs) {
|
|
35
40
|
const band = L.ageBand(req.created_at, nowMs);
|
|
36
|
-
return `<span class="cb-age cb-age--${band.id}" title="Asked ${escapeHtml(String(req.created_at || ''))}">${escapeHtml(band.label)}</span>`;
|
|
41
|
+
return `<span class="fact-pill cb-age cb-age--${band.id}" title="Asked ${escapeHtml(String(req.created_at || ''))}">${escapeHtml(band.label)}</span>`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function whenLine(iso) {
|
|
45
|
+
const d = iso ? new Date(iso) : null;
|
|
46
|
+
if (!d || Number.isNaN(d.getTime())) return '';
|
|
47
|
+
return `<span class="ov-row__when">${escapeHtml(d.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }))}</span>`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function titleHtml(req) {
|
|
51
|
+
const t = L.titleOf(req.what_is_stuck);
|
|
52
|
+
return `<span class="ov-row__title">${escapeHtml(t.title)}</span>`
|
|
53
|
+
+ (t.rest ? `<p class="ov-row__sub">${escapeHtml(t.rest)}</p>` : '');
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
function contextChip(req) {
|
|
40
57
|
const ctx = L.contextOf(req);
|
|
41
58
|
if (!ctx) return '';
|
|
42
|
-
return `<a class="
|
|
59
|
+
return `<a class="fact-chip" href="${escapeHtml(ctx.href)}">${escapeHtml(ctx.kind)} #${escapeHtml(String(ctx.id))}</a>`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Who to answer: the asker's name, as a door to their profile.
|
|
63
|
+
function askerFact(req) {
|
|
64
|
+
const name = L.askerOf(req);
|
|
65
|
+
if (!name) return '';
|
|
66
|
+
const who = req.requested_by != null
|
|
67
|
+
? `<a href="/builders/profile?id=${escapeHtml(String(req.requested_by))}">${escapeHtml(name)}</a>`
|
|
68
|
+
: escapeHtml(name);
|
|
69
|
+
return `<span class="ov-fact">asked by ${who}</span>`;
|
|
43
70
|
}
|
|
44
71
|
|
|
45
72
|
// A row for the "asked of you" list. There is deliberately NO "claim this"
|
|
@@ -47,58 +74,67 @@
|
|
|
47
74
|
// be reimplementing the Agora, which this goal explicitly forbids (C06 owns the
|
|
48
75
|
// chat mirror). What the page owes a helper is the request, its age, its
|
|
49
76
|
// context, and who to answer.
|
|
50
|
-
function forMeRow(req,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
${contextChip(req)}
|
|
57
|
-
${ageChip(req, nowMs)}
|
|
58
|
-
</p>
|
|
77
|
+
function forMeRow(req, nowMs) {
|
|
78
|
+
return `<li class="ov-row" data-id="${escapeHtml(String(req.id))}">
|
|
79
|
+
<div class="ov-row__main">${titleHtml(req)}
|
|
80
|
+
<div class="ov-row__facts">${askerFact(req)}${contextChip(req)}</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="ov-row__right">${ageChip(req, nowMs)}${whenLine(req.created_at)}</div>
|
|
59
83
|
</li>`;
|
|
60
84
|
}
|
|
61
85
|
|
|
62
86
|
// Your own ask, with the two settle actions. Only the asker can settle — the
|
|
63
87
|
// server enforces it in the UPDATE's WHERE, this is just the affordance.
|
|
64
88
|
function mineRow(req, nowMs) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<
|
|
68
|
-
<span class="
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
</
|
|
89
|
+
const id = escapeHtml(String(req.id));
|
|
90
|
+
return `<li class="ov-row" data-id="${id}">
|
|
91
|
+
<div class="ov-row__main">${titleHtml(req)}
|
|
92
|
+
<div class="ov-row__facts"><span class="ov-fact">${escapeHtml(L.addressedTo(req, null))}</span>${contextChip(req)}</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="ov-row__right">${ageChip(req, nowMs)}
|
|
95
|
+
<div class="ov-row__actions">
|
|
96
|
+
<button class="btn-accent" data-settle="answered" data-id="${id}">Someone helped</button>
|
|
97
|
+
<button class="btn-ghost" data-settle="withdrawn" data-id="${id}">Withdraw</button>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
76
100
|
</li>`;
|
|
77
101
|
}
|
|
78
102
|
|
|
79
|
-
|
|
103
|
+
// "Asked of you" as runs under a group head: by name first, then each craft.
|
|
104
|
+
function groupedHtml(rows, nowMs) {
|
|
105
|
+
const groups = [];
|
|
106
|
+
for (const r of rows) {
|
|
107
|
+
const g = L.groupOf(r);
|
|
108
|
+
let hit = groups.find((x) => x.id === g.id);
|
|
109
|
+
if (!hit) { hit = { id: g.id, label: g.label, rows: [] }; groups.push(hit); }
|
|
110
|
+
hit.rows.push(r);
|
|
111
|
+
}
|
|
112
|
+
groups.sort((a, b) => Number(b.id === 'you') - Number(a.id === 'you'));
|
|
113
|
+
return groups.map((g) => `<section class="ov-group">
|
|
114
|
+
<div class="ov-group__head"><span class="ov-group__name">${escapeHtml(g.label)}</span><span class="ov-group__n">${g.rows.length}</span></div>
|
|
115
|
+
<ul class="ov-rows">${g.rows.map((r) => forMeRow(r, nowMs)).join('')}</ul>
|
|
116
|
+
</section>`).join('');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function paint(hostSel, emptySel, html, n) {
|
|
80
120
|
const host = $(hostSel);
|
|
81
121
|
const empty = $(emptySel);
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
if (empty) empty.hidden = rows.length > 0;
|
|
122
|
+
if (host) host.innerHTML = html;
|
|
123
|
+
if (empty) empty.hidden = n > 0;
|
|
85
124
|
}
|
|
86
125
|
|
|
87
126
|
async function load() {
|
|
88
127
|
const nowMs = Date.now();
|
|
89
|
-
const [forMe, mine
|
|
128
|
+
const [forMe, mine] = await Promise.all([
|
|
90
129
|
api.helpRequests.getHelpRequestsForMe({}),
|
|
91
130
|
api.helpRequests.getHelpRequestsMine({ query: { status: 'open' } }),
|
|
92
|
-
api.me.getMe ? api.me.getMe({}) : Promise.resolve({ ok: false }),
|
|
93
131
|
]);
|
|
94
132
|
|
|
95
|
-
const viewerId = me && me.ok && me.data && me.data.builder ? me.data.builder.id : null;
|
|
96
|
-
|
|
97
133
|
const forMeRows = ((forMe.ok && forMe.data && forMe.data.help_requests) || []).slice().sort(L.byAgeOldestFirst);
|
|
98
134
|
const mineRows = ((mine.ok && mine.data && mine.data.help_requests) || []).slice().sort(L.byAgeOldestFirst);
|
|
99
135
|
|
|
100
|
-
|
|
101
|
-
|
|
136
|
+
paint('#cb-for-me', '#cb-for-me-empty', groupedHtml(forMeRows, nowMs), forMeRows.length);
|
|
137
|
+
paint('#cb-mine', '#cb-mine-empty', mineRows.map((r) => mineRow(r, nowMs)).join(''), mineRows.length);
|
|
102
138
|
|
|
103
139
|
const sub = $('#cb-sub');
|
|
104
140
|
if (sub) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_": "Collab — the two-sided ask board. ASKED OF YOU comes first because it is the half where someone is waiting, and the empty state of that half is the page's most common render. Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once.",
|
|
2
|
+
"_": "Collab — the two-sided ask board. ASKED OF YOU comes first because it is the half where someone is waiting, and the empty state of that half is the page's most common render. 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 four asks and two of the viewer's own in the harness so the rows can be looked at.",
|
|
3
3
|
"page": "/builders/collab",
|
|
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 state renders populated: help-requests__for-me.json (four asks across the four age bands, one by name) and help-requests__mine.json (two of the viewer's own). Every declared 404 below is a feed the kit's stub does not carry, named rather than hidden.",
|
|
10
10
|
"ignoreRequests": [
|
|
11
11
|
"/api/(gds|bongos)/me$",
|
|
12
12
|
"/api/(gds|bongos)/me/",
|
|
@@ -1,124 +1,79 @@
|
|
|
1
|
-
/* modules/hall-ui/public/copy-desk.css — the Copy desk
|
|
2
|
-
of goal 1000074)
|
|
1
|
+
/* modules/hall-ui/public/copy-desk.css — the Copy desk page (task 1003113 /
|
|
2
|
+
R02 of goal 1000074), rebuilt on the hall's row archetype by task 1003471:
|
|
3
|
+
the counts are the shell's glass .stat-row, every list is a run of
|
|
4
|
+
oversight.css's .ov-row, the filters and the search are the kit's .filters,
|
|
5
|
+
the two dialogs are the family's .ov-modal on a native <dialog>. What is
|
|
6
|
+
left here is what only this page has: the quoted string as evidence, the
|
|
7
|
+
reason as prose, the places a duplicate lives, the dialog's scrim.
|
|
3
8
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
defined here. */
|
|
9
|
+
px and tokens throughout, and state is COLOUR — the pill's verification
|
|
10
|
+
vocabulary — never opacity. Opacity dims the panel behind the text as well
|
|
11
|
+
as the text, which is why the first cut read muddy rather than quiet. */
|
|
8
12
|
|
|
9
|
-
.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
.cd-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
gap: 0.15rem;
|
|
19
|
-
padding: 0.75rem 0.9rem;
|
|
20
|
-
border: var(--line) solid var(--rule-soft);
|
|
21
|
-
border-radius: var(--radius-sm);
|
|
22
|
-
background: var(--glass);
|
|
23
|
-
}
|
|
24
|
-
.cd-stat__n { font-size: 1.6rem; font-weight: 700; line-height: 1.1; }
|
|
25
|
-
.cd-stat__l { font-size: 0.8rem; opacity: 0.7; }
|
|
26
|
-
|
|
27
|
-
.cd-note { font-size: 0.85rem; opacity: 0.75; line-height: 1.5; }
|
|
28
|
-
/* 4px, not --radius-sm: a 10px radius on a 16px-tall inline span reads as a
|
|
29
|
-
pill, and the tier has no smaller token. Deliberate, not missed. */
|
|
30
|
-
.cd-note code {
|
|
31
|
-
padding: 0.05rem 0.3rem;
|
|
32
|
-
border-radius: 0.25rem;
|
|
33
|
-
background: var(--glass);
|
|
34
|
-
font-size: 0.95em;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.cd-filters { display: flex; flex-wrap: wrap; gap: 0.5rem 0.75rem; align-items: center; margin: 0.5rem 0 1rem; }
|
|
38
|
-
.cd-filters__label { font-size: 0.8rem; opacity: 0.7; }
|
|
39
|
-
.cd-filters__select,
|
|
40
|
-
.cd-browse__input,
|
|
41
|
-
.cd-dialog__select,
|
|
42
|
-
.cd-dialog__input,
|
|
43
|
-
.cd-dialog__text {
|
|
44
|
-
padding: 0.4rem 0.6rem;
|
|
45
|
-
border: var(--line) solid var(--rule-soft);
|
|
46
|
-
border-radius: var(--radius-sm);
|
|
47
|
-
background: var(--bg-deep);
|
|
48
|
-
color: inherit;
|
|
49
|
-
font: inherit;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.cd-browse { margin: 0.5rem 0 1rem; }
|
|
53
|
-
.cd-browse__label { display: block; font-size: 0.8rem; opacity: 0.7; margin-bottom: 0.3rem; }
|
|
54
|
-
.cd-browse__input { width: 100%; max-width: 34rem; }
|
|
55
|
-
|
|
56
|
-
.cd-list { display: flex; flex-direction: column; gap: 0.6rem; }
|
|
57
|
-
.cd-empty { padding: 1rem; opacity: 0.65; font-size: 0.9rem; }
|
|
58
|
-
|
|
59
|
-
.cd-card {
|
|
60
|
-
padding: 0.85rem 1rem;
|
|
61
|
-
border: var(--line) solid var(--rule-soft);
|
|
62
|
-
border-radius: var(--radius-sm);
|
|
13
|
+
/* The summary strip sits closer to its provenance line than to the next panel.
|
|
14
|
+
The inline code takes 4px, not --radius-sm: a 10px radius on a 16px-tall
|
|
15
|
+
inline span reads as a pill, and the tier has no smaller token. */
|
|
16
|
+
.cd-summary { margin-bottom: 12px; }
|
|
17
|
+
.cd-registry-note { margin: 0 0 var(--sp-md); max-width: 72ch; }
|
|
18
|
+
.cd-registry-note code,
|
|
19
|
+
.cd-search-count code {
|
|
20
|
+
padding: 1px 5px;
|
|
21
|
+
border-radius: 4px;
|
|
63
22
|
background: var(--glass);
|
|
23
|
+
font-family: var(--font-mono);
|
|
24
|
+
font-size: 11.5px;
|
|
25
|
+
color: var(--ink-soft);
|
|
64
26
|
}
|
|
65
|
-
.cd-card--slim { padding: 0.6rem 0.8rem; }
|
|
66
|
-
.cd-card__head { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; margin-bottom: 0.4rem; }
|
|
67
|
-
.cd-card__target { margin: 0.2rem 0 0.45rem; line-height: 1.5; }
|
|
68
|
-
.cd-card__actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; justify-content: flex-end; margin-top: 0.5rem; }
|
|
69
27
|
|
|
70
28
|
/* The quoted string is the product's own words, shown as evidence — so it is
|
|
71
|
-
|
|
72
|
-
leading and trailing space in a string is often exactly what is
|
|
29
|
+
set apart from this page's own voice: mono, in the ink, wrapping preserved
|
|
30
|
+
because leading and trailing space in a string is often exactly what is
|
|
31
|
+
wrong. It is the row's title, so it sits at the title's line, in the mono's
|
|
32
|
+
own size and weight. */
|
|
73
33
|
.cd-quote {
|
|
74
34
|
quotes: '"' '"';
|
|
75
35
|
font-family: var(--font-mono);
|
|
76
|
-
font-size:
|
|
36
|
+
font-size: 13.5px;
|
|
37
|
+
font-weight: 400;
|
|
38
|
+
letter-spacing: 0;
|
|
39
|
+
color: var(--ink);
|
|
77
40
|
white-space: pre-wrap;
|
|
78
|
-
|
|
41
|
+
overflow-wrap: anywhere;
|
|
79
42
|
}
|
|
80
|
-
.cd-
|
|
81
|
-
.cd-byline { font-size: 0.8rem; opacity: 0.65; margin-left: auto; }
|
|
82
|
-
.cd-reason { margin: 0.3rem 0 0; line-height: 1.55; }
|
|
83
|
-
.cd-snapshot-label { font-size: 0.8rem; opacity: 0.6; }
|
|
84
|
-
.cd-closed { margin: 0.5rem 0 0; font-size: 0.85rem; opacity: 0.8; line-height: 1.5; }
|
|
85
|
-
.cd-places { margin: 0.3rem 0 0; padding-left: 1.1rem; font-size: 0.8rem; opacity: 0.65; font-family: var(--font-mono); }
|
|
43
|
+
.cd-snapshot-label { font-size: 12.5px; font-weight: 400; color: var(--ink-faint); }
|
|
86
44
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
letter-spacing: 0.02em;
|
|
94
|
-
opacity: 0.85;
|
|
95
|
-
}
|
|
96
|
-
.cd-tag--kind { font-weight: 600; opacity: 1; }
|
|
97
|
-
/* 'untouched' is the only state that is WORK, so it is the only one that carries
|
|
98
|
-
colour — a queue where everything is highlighted highlights nothing. */
|
|
99
|
-
.cd-tag--untouched { border-color: currentColor; opacity: 1; }
|
|
100
|
-
.cd-tag--flagged,
|
|
101
|
-
.cd-tag--settled,
|
|
102
|
-
.cd-tag--moved { opacity: 0.6; }
|
|
103
|
-
.cd-tag--gone { opacity: 0.75; font-style: italic; }
|
|
45
|
+
/* The reason is the judgement the artist reads, so it takes the body ink at
|
|
46
|
+
body size rather than the sub line's soft ink, and the detail-lines' 72ch
|
|
47
|
+
rather than the clause measure: a reason is a paragraph. */
|
|
48
|
+
.cd-reason { font-size: 14px; color: var(--ink); max-width: 72ch; }
|
|
49
|
+
.cd-closed { margin: 8px 0 0; font-size: 13px; line-height: 1.5; color: var(--ink-soft); max-width: 72ch; }
|
|
50
|
+
.cd-closed b { color: var(--ink); font-weight: 500; }
|
|
104
51
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
52
|
+
/* Where a duplicate lives: one mono line per place and no hairlines — seven
|
|
53
|
+
near-identical file:line lines ruled apart read as a table nobody laid out. */
|
|
54
|
+
.cd-places {
|
|
55
|
+
list-style: none;
|
|
56
|
+
margin: 8px 0 0;
|
|
57
|
+
padding: 0;
|
|
58
|
+
font-family: var(--font-mono);
|
|
59
|
+
font-size: 12px;
|
|
60
|
+
line-height: 1.6;
|
|
61
|
+
color: var(--ink-faint);
|
|
112
62
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
63
|
+
.cd-places li { overflow-wrap: anywhere; }
|
|
64
|
+
|
|
65
|
+
/* The search field takes the room a phrase needs. */
|
|
66
|
+
.cd-find .filter { flex: 1 1 280px; max-width: 520px; }
|
|
67
|
+
.cd-find .filter input[type="search"] { width: 100%; }
|
|
68
|
+
.cd-search-count { margin: 0 0 14px; }
|
|
69
|
+
|
|
70
|
+
/* The two dialogs are the family modal on a native <dialog>: bg-card, one rule
|
|
71
|
+
hairline, no shadow. The backdrop is the one literal this sheet keeps — pure
|
|
72
|
+
black with alpha as a SCRIM is the exception the Fifteen Rule names, and a
|
|
73
|
+
backdrop cannot take a ground token: it has to darken whatever is behind it
|
|
74
|
+
in either mode. */
|
|
75
|
+
.cd-dialog { width: min(480px, calc(100vw - 32px)); color: var(--ink); }
|
|
116
76
|
.cd-dialog::backdrop { background: rgba(0, 0, 0, 0.55); }
|
|
117
|
-
.cd-
|
|
118
|
-
.cd-
|
|
119
|
-
.cd-
|
|
120
|
-
.cd-dialog__hint { display: block; font-weight: 400; font-size: 0.78rem; opacity: 0.65; line-height: 1.45; margin-top: 0.1rem; }
|
|
121
|
-
.cd-dialog__select, .cd-dialog__input, .cd-dialog__text { width: 100%; }
|
|
122
|
-
.cd-dialog__text { resize: vertical; line-height: 1.5; }
|
|
123
|
-
.cd-dialog__err { min-height: 1.2rem; margin: 0.5rem 0 0; font-size: 0.85rem; color: var(--danger); line-height: 1.45; }
|
|
124
|
-
.cd-dialog__actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 0.9rem; }
|
|
77
|
+
.cd-dialog .ov-form { margin: 14px 0 0; }
|
|
78
|
+
.cd-dialog textarea { resize: vertical; line-height: 1.5; }
|
|
79
|
+
.ov-modal .cd-dialog__err { min-height: 18px; margin: 8px 0 0; font-size: 13px; line-height: 1.45; color: var(--danger); }
|