@bongos/core 1.21.41 → 1.21.42
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 +19 -19
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +4 -2
- package/docs/page-readings.json +359 -332
- package/modules/autonomy/governor-docket.js +6 -1
- package/modules/hall-ui/public/street-card.css +13 -0
- package/modules/hall-ui/public/street-card.js +61 -9
- package/modules/hall-ui/public/street-card.states.json +72 -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 +6 -0
- package/src/module-api.js +1 -1
- package/tests/governor_docket.mjs +3 -1
- package/tests/hall_matter_card.mjs +71 -0
|
@@ -31,9 +31,13 @@ function fenceDocketMatters(project, mayManage) {
|
|
|
31
31
|
const goals = Array.isArray(project.goals) ? project.goals : [];
|
|
32
32
|
let title = null;
|
|
33
33
|
let where = null;
|
|
34
|
+
let detail = null;
|
|
34
35
|
if (project.enabled === false) {
|
|
35
36
|
title = 'Unattended building is switched off';
|
|
36
|
-
|
|
37
|
+
// the kicker is building · floor only (task 1004568); the reason is the
|
|
38
|
+
// card's fact (cut to one line) and its "more" (in full), through `detail`
|
|
39
|
+
where = 'automation · the switch';
|
|
40
|
+
detail = project.paused_reason ? String(project.paused_reason).trim() : null;
|
|
37
41
|
} else if (!goals.length) {
|
|
38
42
|
title = 'Unattended building is on, but no goal is allowed';
|
|
39
43
|
where = 'automation · allowed goals';
|
|
@@ -52,6 +56,7 @@ function fenceDocketMatters(project, mayManage) {
|
|
|
52
56
|
tally: null,
|
|
53
57
|
href: '/watch',
|
|
54
58
|
ref: { type: 'autonomy_fence', id: 'project' },
|
|
59
|
+
...(detail ? { detail } : {}),
|
|
55
60
|
// The floor desk's acts (task 1004559), only for the switch's own atom.
|
|
56
61
|
...(mayManage ? { acts: project.enabled === false ? [{ key: 'switch_on' }] : [{ key: 'allow', pick: 'open_goals', except: [] }, { key: 'switch_off', options: OFF_REASONS }] } : {}),
|
|
57
62
|
}];
|
|
@@ -22,6 +22,19 @@ body[data-page="street"] {
|
|
|
22
22
|
|
|
23
23
|
.street-card__body { display: flex; flex-direction: column; gap: 11px; }
|
|
24
24
|
|
|
25
|
+
/* sized to its words (task 1004568): no min-height anywhere, and every part
|
|
26
|
+
of the compact card is held to its lines. The model cuts at a word boundary
|
|
27
|
+
(street-card.js LINE); these are the net under that, so a wide run of glyphs
|
|
28
|
+
can never push a part onto another line and stretch the card. The kicker
|
|
29
|
+
gives way to "when" and the close. The title is cut to about two lines;
|
|
30
|
+
its clamp at three is only the net, for a run of wide glyphs. */
|
|
31
|
+
.street-card__head .street-label { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
32
|
+
.street-card__head #streetCardWhen { flex: none; }
|
|
33
|
+
.street-card__title { overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; overflow: hidden; flex: none; }
|
|
34
|
+
.mcard-fact { margin: 0; font-size: 14px; line-height: 1.4; color: var(--ink-soft); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
35
|
+
.street-card__lines li { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
36
|
+
.mcard-voice__words { overflow-wrap: anywhere; }
|
|
37
|
+
|
|
25
38
|
.mcard-quote { margin: 0; font-size: 14px; line-height: 1.45; color: var(--ink-soft); }
|
|
26
39
|
|
|
27
40
|
/* the wording change: struck, then inserted */
|
|
@@ -101,10 +101,25 @@
|
|
|
101
101
|
if (names.length <= 1) return names.join('');
|
|
102
102
|
return names.slice(0, -1).join(', ') + ' and ' + names[names.length - 1];
|
|
103
103
|
}
|
|
104
|
+
// a text as one run of words: every whitespace run is one space
|
|
105
|
+
function norm(s) { return str(s).replace(/\s+/g, ' ').trim(); }
|
|
104
106
|
function clip(s, n) {
|
|
105
|
-
var t =
|
|
107
|
+
var t = norm(s);
|
|
106
108
|
return t.length > n ? t.slice(0, n - 1).replace(/\s+\S*$/, '') + '…' : t;
|
|
107
109
|
}
|
|
110
|
+
// THE COMPACT CARD'S LINE RULE (task 1004568). The card is only as tall as
|
|
111
|
+
// its words need: every part of it has a cap, and anything longer is cut at
|
|
112
|
+
// a word boundary with an ellipsis (clip(): the desk's line rule, GovDesk.fit(),
|
|
113
|
+
// but never mid-word), with the whole text under "more". The caps are
|
|
114
|
+
// characters at the card's 390px width: the kicker, the fact and a listed
|
|
115
|
+
// line are one line; the title two; a voice's reason two; the proposer's
|
|
116
|
+
// words three.
|
|
117
|
+
var LINE = { where: 40, title: 64, fact: 44, line: 44, reason: 96, quote: 132 };
|
|
118
|
+
// the first line of a text, as words (a fact is one line of it)
|
|
119
|
+
function firstLine(s) {
|
|
120
|
+
var lines = str(s).split(/\r?\n/).map(function (l) { return l.replace(/^[#>*\-\s]+/, '').replace(/\s+/g, ' ').trim(); }).filter(Boolean);
|
|
121
|
+
return lines[0] || '';
|
|
122
|
+
}
|
|
108
123
|
// the first paragraph of a markdown text, as words
|
|
109
124
|
function firstPara(md) {
|
|
110
125
|
var paras = str(md).split(/\r?\n\s*\r?\n/).map(function (p) { return p.replace(/^[#>*\-\s]+/gm, '').replace(/\s+/g, ' ').trim(); }).filter(Boolean);
|
|
@@ -273,7 +288,7 @@
|
|
|
273
288
|
var votes = (item && item.votes) || [];
|
|
274
289
|
var out = [];
|
|
275
290
|
votes.filter(function (v) { return v.direction === 'no'; }).forEach(function (v) {
|
|
276
|
-
out.push({ side: 'against', by: firstName(personName(v)) + ' · objects to ' + sectionWords(v.section_key), words:
|
|
291
|
+
out.push({ side: 'against', by: firstName(personName(v)) + ' · objects to ' + sectionWords(v.section_key), words: norm(v.reason_md) });
|
|
277
292
|
});
|
|
278
293
|
var yes = votes.filter(function (v) { return v.direction === 'yes'; }).map(function (v) { return firstName(personName(v)); });
|
|
279
294
|
if (yes.length && out.length) out.unshift({ side: 'for', by: 'for', words: list(yes) });
|
|
@@ -319,7 +334,7 @@
|
|
|
319
334
|
var kind = subjectKind(item);
|
|
320
335
|
var proposer = m.proposer ? firstName(m.proposer.name || m.proposer.login) : '';
|
|
321
336
|
var words = proposerWords(item);
|
|
322
|
-
if (words) cv.quote = { by: proposer, words:
|
|
337
|
+
if (words) cv.quote = { by: proposer, words: words };
|
|
323
338
|
cv.change = changeOf(item, ctx.docket, ctx.constitution);
|
|
324
339
|
cv.revision = revisionOf(item, ctx.room);
|
|
325
340
|
var b = ballotOf(item, m, ctx.viewerId);
|
|
@@ -399,6 +414,9 @@
|
|
|
399
414
|
var who = m.proposer ? str(m.proposer.name || m.proposer.login) : '';
|
|
400
415
|
cv.subject = { kind: m.source, id: id, who: who, goalId: m.source === 'goal_join_requests' ? joinGoalId(m) : null };
|
|
401
416
|
var mine = m.state === 'needs';
|
|
417
|
+
// its one fact (task 1004568): the first line of the matter's detail (the
|
|
418
|
+
// switch's reason, a blocker's body), cut by compact(); "more" has it whole
|
|
419
|
+
if (m.detail) cv.fact = firstLine(m.detail);
|
|
402
420
|
if (m.source === 'access_requests') {
|
|
403
421
|
if (who) cv.asked = who + ' asked to join as a builder' + (/vouched/.test(str(m.where)) ? ', and someone vouched for them.' : '.');
|
|
404
422
|
if (!mine) { cv.readOnly = true; cv.result = 'Not yours to decide.'; return cv; }
|
|
@@ -470,7 +488,7 @@
|
|
|
470
488
|
if (cv.revision) history.push('an earlier sitting sent it back to be revised');
|
|
471
489
|
(item.votes || []).slice().sort(function (a, b) { return (Date.parse(a.cast_at) || 0) - (Date.parse(b.cast_at) || 0); }).forEach(function (v) {
|
|
472
490
|
var by = personName(v) || 'someone';
|
|
473
|
-
var said = v.direction === 'yes' ? by + ' voted for' : by + ' objected to ' + sectionWords(v.section_key) + (v.reason_md ? ': ' +
|
|
491
|
+
var said = v.direction === 'yes' ? by + ' voted for' : by + ' objected to ' + sectionWords(v.section_key) + (v.reason_md ? ': ' + bound(str(v.reason_md).replace(/\s+/g, ' ').trim()) : '');
|
|
474
492
|
history.push([dayOf(v.cast_at), said].filter(Boolean).join(' · '));
|
|
475
493
|
});
|
|
476
494
|
} else if (m) {
|
|
@@ -497,9 +515,9 @@
|
|
|
497
515
|
key: short.key, building: short.building, where: short.where, when: short.when, title: short.title, href: short.href,
|
|
498
516
|
source: null, quote: null, change: null, revision: null, seats: [], tally: '', voices: [], counsel: null,
|
|
499
517
|
result: short.result, actions: [], textActions: [], reviseForm: null, readOnly: false, loading: false, lines: short.lines || [], subject: null,
|
|
500
|
-
more: null, row: null,
|
|
518
|
+
more: null, row: null, fact: '',
|
|
501
519
|
};
|
|
502
|
-
if (short.key === 'queue') { cv.source = 'queue'; cv.readOnly = true; return cv; }
|
|
520
|
+
if (short.key === 'queue') { cv.source = 'queue'; cv.readOnly = true; return compact(cv); }
|
|
503
521
|
var m = findMatter(c.docket, c.key);
|
|
504
522
|
cv.source = m.source;
|
|
505
523
|
var viewerId = c.viewerId != null ? c.viewerId : (c.docket && c.docket.viewer ? c.docket.viewer.id : null);
|
|
@@ -511,14 +529,47 @@
|
|
|
511
529
|
cv.readOnly = true;
|
|
512
530
|
cv.tally = short.tally;
|
|
513
531
|
cv.result = c.room ? 'This sitting could not be read just now.' : 'Reading the sitting…';
|
|
514
|
-
return cv;
|
|
532
|
+
return compact(cv);
|
|
515
533
|
}
|
|
516
534
|
boardCard(cv, m, item, { docket: c.docket, room: c.room, constitution: c.constitution, viewerId: viewerId });
|
|
517
535
|
cv.more = moreOf(cv, m, item);
|
|
518
|
-
return cv;
|
|
536
|
+
return compact(cv);
|
|
519
537
|
}
|
|
520
538
|
sourceCard(cv, m);
|
|
521
539
|
cv.more = moreOf(cv, m, null);
|
|
540
|
+
return compact(cv);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Every part of the compact card under the line rule (task 1004568): the
|
|
544
|
+
// kicker, the title, the fact, the proposer's words, the listed lines and a
|
|
545
|
+
// voice's reason. What
|
|
546
|
+
// was cut and is not already in "more" is added there whole, so nothing a
|
|
547
|
+
// cut hides is lost.
|
|
548
|
+
function compact(cv) {
|
|
549
|
+
var whole = [];
|
|
550
|
+
function cut(s, n) {
|
|
551
|
+
var t = norm(s);
|
|
552
|
+
var c = clip(t, n);
|
|
553
|
+
if (c !== t) whole.push(t);
|
|
554
|
+
return c;
|
|
555
|
+
}
|
|
556
|
+
cv.where = cut(cv.where, LINE.where);
|
|
557
|
+
cv.title = cut(cv.title, LINE.title);
|
|
558
|
+
cv.fact = cut(cv.fact, LINE.fact);
|
|
559
|
+
if (cv.quote && cv.quote.words) cv.quote = { by: cv.quote.by, words: cut(cv.quote.words, LINE.quote) };
|
|
560
|
+
cv.lines = (cv.lines || []).map(function (l) { return cut(l, LINE.line); });
|
|
561
|
+
cv.voices = (cv.voices || []).map(function (v) {
|
|
562
|
+
var w = cut(v.words, LINE.reason);
|
|
563
|
+
return w === v.words ? v : { side: v.side, by: v.by, words: w };
|
|
564
|
+
});
|
|
565
|
+
if (whole.length) {
|
|
566
|
+
var mo = cv.more || { text: [], who: '', when: '', history: [] };
|
|
567
|
+
var have = mo.text.map(function (x) { return x.words; }).concat(mo.history).join('\n');
|
|
568
|
+
// compared as "more" holds it: bound() may have shortened a long text there
|
|
569
|
+
var add = whole.filter(function (t) { return have.indexOf(bound(t).replace(/…$/, '')) < 0; }).map(function (t) { return { label: '', words: bound(t) }; });
|
|
570
|
+
mo.text = add.concat(mo.text);
|
|
571
|
+
cv.more = mo;
|
|
572
|
+
}
|
|
522
573
|
return cv;
|
|
523
574
|
}
|
|
524
575
|
|
|
@@ -807,6 +858,7 @@
|
|
|
807
858
|
function render(doc, host, cv, on, ui) {
|
|
808
859
|
var u = ui || {};
|
|
809
860
|
while (host.firstChild) host.removeChild(host.firstChild);
|
|
861
|
+
if (cv.fact) host.appendChild(el(doc, 'p', 'mcard-fact', cv.fact));
|
|
810
862
|
if (cv.quote && cv.quote.words) host.appendChild(el(doc, 'p', 'mcard-quote', (cv.quote.by ? cv.quote.by + ': ' : '') + cv.quote.words));
|
|
811
863
|
if (cv.lines && cv.lines.length) {
|
|
812
864
|
var ul = el(doc, 'ul', 'street-card__lines');
|
|
@@ -997,7 +1049,7 @@
|
|
|
997
1049
|
var api = {
|
|
998
1050
|
API: API, SECTIONS: SECTIONS, needsRoom: needsRoom, itemIdOf: itemIdOf, subjectKind: subjectKind,
|
|
999
1051
|
model: model, actsCard: actsCard, moreOf: moreOf, request: request, orderRequest: orderRequest, check: check, answer: answer, refusal: refusal, render: render,
|
|
1000
|
-
memberWords: memberWords, windowWords: windowWords,
|
|
1052
|
+
memberWords: memberWords, windowWords: windowWords, LINE: LINE, compact: compact,
|
|
1001
1053
|
// the wording change, for a drafted amendment's struck and inserted text (task 1004530)
|
|
1002
1054
|
change: changeOf,
|
|
1003
1055
|
};
|
|
@@ -649,6 +649,78 @@
|
|
|
649
649
|
"#mcardMore"
|
|
650
650
|
]
|
|
651
651
|
}
|
|
652
|
+
},
|
|
653
|
+
"automation": {
|
|
654
|
+
"auth": true,
|
|
655
|
+
"_": "the switch is off with a long reason (task 1004568): the kicker is building · floor only, the reason is one cut line, and the card is only as tall as its words.",
|
|
656
|
+
"actions": [
|
|
657
|
+
[
|
|
658
|
+
"wait",
|
|
659
|
+
900
|
|
660
|
+
],
|
|
661
|
+
[
|
|
662
|
+
"clickIfVisible",
|
|
663
|
+
"[data-craft-close]"
|
|
664
|
+
],
|
|
665
|
+
[
|
|
666
|
+
"wait",
|
|
667
|
+
300
|
|
668
|
+
],
|
|
669
|
+
[
|
|
670
|
+
"dispatchClick",
|
|
671
|
+
".street-pin[data-matter=\"autobongos_fence:fence\"]"
|
|
672
|
+
],
|
|
673
|
+
[
|
|
674
|
+
"wait",
|
|
675
|
+
600
|
|
676
|
+
]
|
|
677
|
+
],
|
|
678
|
+
"expect": {
|
|
679
|
+
"visible": [
|
|
680
|
+
"#streetCard",
|
|
681
|
+
".mcard-fact",
|
|
682
|
+
"[data-act=\"switch_on\"]"
|
|
683
|
+
]
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
"automation-more": {
|
|
687
|
+
"auth": true,
|
|
688
|
+
"_": "the same card with more open: the switch reason in full (task 1004568).",
|
|
689
|
+
"actions": [
|
|
690
|
+
[
|
|
691
|
+
"wait",
|
|
692
|
+
900
|
|
693
|
+
],
|
|
694
|
+
[
|
|
695
|
+
"clickIfVisible",
|
|
696
|
+
"[data-craft-close]"
|
|
697
|
+
],
|
|
698
|
+
[
|
|
699
|
+
"wait",
|
|
700
|
+
300
|
|
701
|
+
],
|
|
702
|
+
[
|
|
703
|
+
"dispatchClick",
|
|
704
|
+
".street-pin[data-matter=\"autobongos_fence:fence\"]"
|
|
705
|
+
],
|
|
706
|
+
[
|
|
707
|
+
"wait",
|
|
708
|
+
600
|
|
709
|
+
],
|
|
710
|
+
[
|
|
711
|
+
"click",
|
|
712
|
+
"[data-act=\"more\"]"
|
|
713
|
+
],
|
|
714
|
+
[
|
|
715
|
+
"wait",
|
|
716
|
+
300
|
|
717
|
+
]
|
|
718
|
+
],
|
|
719
|
+
"expect": {
|
|
720
|
+
"visible": [
|
|
721
|
+
"#mcardMore"
|
|
722
|
+
]
|
|
723
|
+
}
|
|
652
724
|
}
|
|
653
725
|
},
|
|
654
726
|
"reducedMotion": {
|
|
@@ -10,6 +10,7 @@ What it settled:
|
|
|
10
10
|
- **The artboard is the street band.** The prototype's 1440 x 900 board less its 64 px header and 128 px footer, which the page lays out as real rows; `street.js` scales the 1440 x 708 band to the window, and on a phone keeps it near full size and lets the street scroll sideways inside its own window, so a pin stays a pin. The header fits the 80 px top-bar floor at 390 and 320 by going to two rows, its three figures as their own sideways strip.
|
|
11
11
|
- **Real data where the prototype had samples.** One district per open goal (up to eight plots; past that seven stand and the eighth is a staked lot that opens the goals page), each goal's tasks drawn in proportion (built, scaffold, the prototype's `ready` slab), Review held while any source that stands in it can be read, its open matters as crates and the dashed "waiting for a tester" count pin, a reading off drawn as a dash and its reason. The full list is the header of `street-draw.js`.
|
|
12
12
|
- **Every building and district is a door** to `/street#building=<key>` or `/street#district=<goal id>`, where its interior opens in place of the street (below). A pin opens the matter's short card (task 1004528 makes it act); its link is offered only for an on-site path.
|
|
13
|
+
- **A card is only as tall as its words** (task 1004568). Every part of the compact card has a cap (`LINE` in `street-card.js`): the kicker, the fact and a listed line one line, the title about two, a voice's reason two, the proposer's words three. Longer text is cut at a whole word with an ellipsis, and whatever was cut is whole under "more". The kicker is building · floor only: the automation switch's off-reason is no longer joined into it, it is the card's fact. Nothing on the card has a min-height. Rendered through `street-card.states.json` (`automation`, `automation-more`, beside the blocker and Full Idea states) at 1440 and 390, light and dark, on the hall-preview harness.
|
|
13
14
|
|
|
14
15
|
Tests: `tests/hall_street.mjs` holds the kit, the row, the pins, the doors and the crates equal to the prototype by running its own `Component` class beside `street-draw.js` in both modes, then the docket rules and the page itself in a vm. The states file is `public/street.states.json`; render it against the hall-preview harness, whose `government__docket.json` fixture carries the seven sample matters.
|
|
15
16
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.42",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.42",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.42",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -9010,5 +9010,11 @@
|
|
|
9010
9010
|
"id": "1004496",
|
|
9011
9011
|
"text": "Artists can now write feedback on any page of the hall. In Artistry mode a Feedback button sits beside the mode switch. It opens a doc about the page you are on: write as much as you need, add reference images, and press \"Turn"
|
|
9012
9012
|
}
|
|
9013
|
+
],
|
|
9014
|
+
"1.21.42": [
|
|
9015
|
+
{
|
|
9016
|
+
"id": "1004568",
|
|
9017
|
+
"text": "Street cards are now only as tall as their words need. Long titles, reasons and lines are cut short with an ellipsis at a whole word, and the full text is one tap away under \"more\". The automation card no longer stretches: its"
|
|
9018
|
+
}
|
|
9013
9019
|
]
|
|
9014
9020
|
}
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.42'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -399,7 +399,9 @@ test('the fence: a matter only when it needs a governor, and only a fence manage
|
|
|
399
399
|
const off = autonomyDocket.fenceDocketMatters({ enabled: false, paused_reason: 'payments touched', goals: [{ goal_id: 1 }], updated_by: 3 }, true);
|
|
400
400
|
assert.equal(off.length, 1);
|
|
401
401
|
assert.equal(off[0].title, 'Unattended building is switched off');
|
|
402
|
-
assert.equal(off[0].where, 'automation · the switch ·
|
|
402
|
+
assert.equal(off[0].where, 'automation · the switch', 'the kicker is building · floor only (task 1004568)');
|
|
403
|
+
assert.equal(off[0].detail, 'payments touched', 'the reason rides in detail: the fact, and the more');
|
|
404
|
+
assert.equal(autonomyDocket.fenceDocketMatters({ enabled: false, goals: [] }, true)[0].detail, undefined, 'no reason, no detail');
|
|
403
405
|
assert.equal(off[0].state, 'needs');
|
|
404
406
|
assert.equal(autonomyDocket.fenceDocketMatters({ enabled: false, goals: [] }, false)[0].state, 'settled');
|
|
405
407
|
assert.equal(autonomyDocket.fenceDocketMatters({ enabled: true, goals: [] }, true)[0].title, 'Unattended building is on, but no goal is allowed');
|
|
@@ -507,6 +507,77 @@ test('the docket hands a matter its detail only as text, bounded, and only when
|
|
|
507
507
|
}
|
|
508
508
|
});
|
|
509
509
|
|
|
510
|
+
// ── the compact card sizes to its words (task 1004568) ─────────────────────
|
|
511
|
+
|
|
512
|
+
const allText = (n) => [n._text || ''].concat((n.children || []).map(allText)).join('\n');
|
|
513
|
+
|
|
514
|
+
test('a very long reason stays within the compact card and is whole under more; the kicker is building · floor only', () => {
|
|
515
|
+
const docketJs = require(path.join(ROOT, 'modules/government/docket.js'));
|
|
516
|
+
const fence = require(path.join(ROOT, 'modules/autonomy/governor-docket.js'));
|
|
517
|
+
const reason = 'lars: stop runner for performance evaluation (2026-10-02) while the grader is rebuilt, then switch it back on once the new numbers are in and the owner has read them';
|
|
518
|
+
// the whole path: the autonomy module's matter, through the docket's normaliser, into the card
|
|
519
|
+
const raw = fence.fenceDocketMatters({ enabled: false, paused_reason: reason, goals: [], updated_by: 3, updated_at: '2026-10-02T10:00:00.000Z' }, true)[0];
|
|
520
|
+
const m = docketJs.normalizeMatter(raw, { source: 'autobongos_fence', building: 'automation' });
|
|
521
|
+
assert.equal(m.where, 'automation · the switch', 'the reason is not joined into the kicker');
|
|
522
|
+
const d = docket([m]);
|
|
523
|
+
const cv = D.cardFor(m.id, d, { now: NOW });
|
|
524
|
+
assert.equal(cv.where, 'automation · the switch');
|
|
525
|
+
assert.ok(cv.fact.length <= C.LINE.fact && /…$/.test(cv.fact), 'the fact is one line, cut, and says so: ' + cv.fact);
|
|
526
|
+
assert.ok(reason.startsWith(cv.fact.replace(/…$/, '')), 'the fact is the reason, cut');
|
|
527
|
+
assert.equal(reason.charAt(cv.fact.length - 1), ' ', 'never mid-word');
|
|
528
|
+
const { document } = fakeDom();
|
|
529
|
+
const shut = document.createElement('div');
|
|
530
|
+
C.render(document, shut, cv, { act() {}, openForm() {}, ask() {}, more() {}, pick() {} }, { more: false });
|
|
531
|
+
assert.ok(!allText(shut).includes(reason), 'the compact card does not carry the whole reason');
|
|
532
|
+
assert.equal(walk(shut, (n) => n.className === 'mcard-fact').length, 1, 'one fact line');
|
|
533
|
+
const open = document.createElement('div');
|
|
534
|
+
C.render(document, open, cv, { act() {}, openForm() {}, ask() {}, more() {}, pick() {} }, { more: true });
|
|
535
|
+
assert.ok(allText(open).includes(reason), 'more shows the reason whole');
|
|
536
|
+
// no reason, no fact
|
|
537
|
+
const bare = docketJs.normalizeMatter(fence.fenceDocketMatters({ enabled: false, goals: [] }, true)[0], { source: 'autobongos_fence', building: 'automation' });
|
|
538
|
+
assert.equal(D.cardFor(bare.id, docket([bare]), { now: NOW }).fact, '');
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test('every part of the compact card is held to its line: the kicker, the title, the listed lines, a voice\'s reason', () => {
|
|
542
|
+
const long = (w) => Array.from({ length: 40 }, (_, i) => w + i).join(' ');
|
|
543
|
+
const m = matter({ id: 'blockers:9', source: 'blockers', building: 'review', rule: 'decide', where: long('where'), title: long('title'), detail: long('detail'), ref: { type: 'blocker', id: '9' } });
|
|
544
|
+
const cv = C.model({ docket: docket([m]), key: m.id, now: NOW });
|
|
545
|
+
for (const [k, n] of [['where', C.LINE.where], ['title', C.LINE.title], ['fact', C.LINE.fact]]) {
|
|
546
|
+
assert.ok(cv[k].length <= n && /…$/.test(cv[k]), k + ' is cut: ' + cv[k]);
|
|
547
|
+
const whole = long(k === 'fact' ? 'detail' : k);
|
|
548
|
+
assert.ok(whole.startsWith(cv[k].replace(/…$/, '')), k + ' keeps its own words');
|
|
549
|
+
assert.equal(whole.charAt(cv[k].length - 1), ' ', k + ' cut at a word, not inside one');
|
|
550
|
+
}
|
|
551
|
+
const words = cv.more.text.map((x) => x.words);
|
|
552
|
+
assert.ok(words.includes(long('title')), 'the whole title is under more');
|
|
553
|
+
assert.ok(words.includes(long('where').toLowerCase()), 'the whole kicker is under more');
|
|
554
|
+
assert.ok(words.includes(long('detail')), 'the whole detail is under more');
|
|
555
|
+
// the queue's listed lines and an objection's reason
|
|
556
|
+
const lines = C.compact({ where: '', title: '', fact: '', lines: [long('line')], voices: [{ side: 'against', by: 'Kai', words: long('because') }], more: null });
|
|
557
|
+
assert.ok(lines.lines[0].length <= C.LINE.line && lines.voices[0].words.length <= C.LINE.reason);
|
|
558
|
+
assert.deepEqual(lines.more.text.map((x) => x.words).sort(), [long('because'), long('line')].sort(), 'both whole under more');
|
|
559
|
+
// a text "more" already holds, shortened by its own bound, is not added twice
|
|
560
|
+
const huge = long('w').repeat(40);
|
|
561
|
+
assert.ok(huge.length > 4000, 'longer than more keeps');
|
|
562
|
+
const twice = C.compact({ where: '', title: huge, fact: '', lines: [], voices: [], more: { text: [{ label: '', words: huge.slice(0, 3999).trim() + '…' }], who: '', when: '', history: [] } });
|
|
563
|
+
assert.equal(twice.more.text.length, 1, 'no duplicate under more');
|
|
564
|
+
// short words are left alone, and add nothing to more
|
|
565
|
+
const short = C.compact({ where: 'review · blockers', title: 'Pick a host', fact: 'waits on you', lines: ['one'], voices: [], more: null });
|
|
566
|
+
assert.deepEqual([short.where, short.title, short.fact, short.lines, short.more], ['review · blockers', 'Pick a host', 'waits on you', ['one'], null]);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test('the card has no fixed height: no min-height on its parts, every one-line part is held to one line', () => {
|
|
570
|
+
const css = read('street-card.css') + read('street.css');
|
|
571
|
+
const rules = [...css.matchAll(/([^{}]*street-card[^{}]*)\{([^}]*)\}/g)];
|
|
572
|
+
for (const [, sel, body] of rules) if (!/__close/.test(sel)) assert.doesNotMatch(body, /(^|;)\s*(min-height|height)\s*:/, 'no fixed height on ' + sel.trim());
|
|
573
|
+
const card = read('street-card.css');
|
|
574
|
+
for (const sel of ['.street-card__head .street-label', '.mcard-fact', '.street-card__lines li']) {
|
|
575
|
+
const body = card.split(sel + ' {')[1];
|
|
576
|
+
assert.ok(body && /white-space: nowrap/.test(body.split('}')[0]) && /text-overflow: ellipsis/.test(body.split('}')[0]), sel + ' is one line');
|
|
577
|
+
}
|
|
578
|
+
assert.match(card, /\.street-card__title \{[^}]*-webkit-line-clamp: 3/);
|
|
579
|
+
});
|
|
580
|
+
|
|
510
581
|
// ── the Board Room names the new subjects ───────────────────────────────────
|
|
511
582
|
|
|
512
583
|
test('the Board Room labels a building\'s matter as that building\'s matter, and a who-decides change as its own amendment', () => {
|