@bongos/core 1.21.39 → 1.21.41

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.
Files changed (88) hide show
  1. package/.bongos-core.json +144 -84
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +16 -0
  4. package/clients/bongos-client/index.cjs +16 -0
  5. package/clients/bongos-client/index.d.ts +25 -0
  6. package/clients/bongos-client/index.mjs +16 -0
  7. package/docs/api/openapi.json +492 -3
  8. package/docs/api-reference.md +13 -1
  9. package/docs/copy-inventory.md +29 -13
  10. package/docs/copy-registry.json +177 -23
  11. package/docs/file-map.md +1 -0
  12. package/docs/module-api-changelog.md +4 -0
  13. package/docs/page-inventory.json +14 -2
  14. package/docs/page-readings.json +601 -554
  15. package/modules/government/docket.js +14 -0
  16. package/modules/hall-ui/public/atlas.html +3 -3
  17. package/modules/hall-ui/public/blockers.html +3 -3
  18. package/modules/hall-ui/public/board-room.html +3 -3
  19. package/modules/hall-ui/public/city.html +3 -3
  20. package/modules/hall-ui/public/collab.html +3 -3
  21. package/modules/hall-ui/public/commands.html +3 -3
  22. package/modules/hall-ui/public/copy-desk.html +3 -3
  23. package/modules/hall-ui/public/craft-mode.js +62 -0
  24. package/modules/hall-ui/public/deploy.html +3 -3
  25. package/modules/hall-ui/public/diagrams.html +3 -3
  26. package/modules/hall-ui/public/drachmae.html +3 -3
  27. package/modules/hall-ui/public/fleet.html +3 -3
  28. package/modules/hall-ui/public/gate.html +3 -3
  29. package/modules/hall-ui/public/goal-map.html +3 -3
  30. package/modules/hall-ui/public/goals.html +3 -3
  31. package/modules/hall-ui/public/government.html +3 -3
  32. package/modules/hall-ui/public/idea.html +3 -3
  33. package/modules/hall-ui/public/ideas.html +3 -3
  34. package/modules/hall-ui/public/index.html +3 -3
  35. package/modules/hall-ui/public/modules.html +3 -3
  36. package/modules/hall-ui/public/page-notes.js +320 -0
  37. package/modules/hall-ui/public/primer.html +3 -3
  38. package/modules/hall-ui/public/profile.html +3 -3
  39. package/modules/hall-ui/public/project-settings.html +3 -3
  40. package/modules/hall-ui/public/ranks.html +3 -3
  41. package/modules/hall-ui/public/roadmap.html +3 -3
  42. package/modules/hall-ui/public/roster.html +3 -3
  43. package/modules/hall-ui/public/sessions.html +3 -3
  44. package/modules/hall-ui/public/settings.html +3 -3
  45. package/modules/hall-ui/public/street-card.css +21 -2
  46. package/modules/hall-ui/public/street-card.js +198 -64
  47. package/modules/hall-ui/public/street-card.states.json +495 -7
  48. package/modules/hall-ui/public/street-desk.js +47 -2
  49. package/modules/hall-ui/public/street.css +1 -3
  50. package/modules/hall-ui/public/street.html +3 -4
  51. package/modules/hall-ui/public/street.js +42 -15
  52. package/modules/hall-ui/public/studio.html +3 -3
  53. package/modules/hall-ui/public/style.css +144 -0
  54. package/modules/hall-ui/public/task.html +3 -3
  55. package/modules/hall-ui/public/thinking.html +3 -3
  56. package/modules/hall-ui/public/tweak-editor.html +3 -3
  57. package/modules/hall-ui/public/watch.html +3 -3
  58. package/modules/hall-ui/public/work.html +3 -3
  59. package/modules/hall-ui/records/page-notes.md +16 -0
  60. package/modules/ideas/governor-docket.js +2 -0
  61. package/modules/lifecycle/governor-docket.js +4 -0
  62. package/modules/onboarding/routes/access-requests.js +2 -0
  63. package/modules/page-notes/CLAUDE.md +15 -0
  64. package/modules/page-notes/db.js +147 -0
  65. package/modules/page-notes/migrations/page_notes_001_notes.sql +68 -0
  66. package/modules/page-notes/module.json +13 -0
  67. package/modules/page-notes/notes.js +174 -0
  68. package/modules/page-notes/routes/page-notes.js +172 -0
  69. package/modules/page-notes/split.js +68 -0
  70. package/modules/provisioning/starter-bundles.js +1 -0
  71. package/modules/security/routes/reports.js +3 -0
  72. package/modules/ui-design/kit/serve.js +1 -1
  73. package/package-lock.json +2 -2
  74. package/package.json +1 -1
  75. package/release-notes.json +12 -0
  76. package/scripts/gds/publish-manifest.js +1 -0
  77. package/src/bongos/module-rechecks.js +12 -1
  78. package/src/bongos/module-scope-map.js +4 -0
  79. package/src/bongos/routes.js +4 -1
  80. package/src/module-api.js +1 -1
  81. package/tests/governor_docket.mjs +20 -0
  82. package/tests/hall_matter_card.mjs +143 -18
  83. package/tests/hall_street.mjs +1 -1
  84. package/tests/hall_tweak_editor.mjs +1 -1
  85. package/tests/module_loader.mjs +1 -1
  86. package/tests/page_notes.mjs +289 -0
  87. package/tests/page_notes_db.mjs +140 -0
  88. package/tests/page_notes_panel.mjs +145 -0
@@ -25,10 +25,10 @@
25
25
  //
26
26
  // WHERE THE REAL DATA BENDS THE DESIGN (said once here and in the ship notes):
27
27
  // * Objection sections. The server keeps one closed vocabulary of sections an
28
- // objection may name (board.js FULL_IDEA_SECTION_KEYS, a DB CHECK). A sitting
29
- // that is not a Full Idea offers five of those keys in plain words ("what it
30
- // changes", ...). A Full Idea is voted on in the Board Room, where its
31
- // sections are in front of the voter (ADR 0175 §8) — the card links there.
28
+ // objection may name (board.js FULL_IDEA_SECTION_KEYS, a DB CHECK). The card
29
+ // offers five of those keys as one-tap chips, the desk row's (GovDesk.CHIPS).
30
+ // A Full Idea is voted on here too: its sections open under "more", in front
31
+ // of the voter (ADR 0175 §8), so nobody is sent to the Board Room to vote.
32
32
  // * Abstain. The ballot has yes and no and nothing else, so abstaining records
33
33
  // nothing: the card says so and the matter still waits on you.
34
34
  // * Counsel. There is no model behind it: the note is written from the sitting's
@@ -40,9 +40,17 @@
40
40
  // its rows, a building's matter with its revised detail. The constitution's own
41
41
  // fields are changed in the Board Room's amendment form, not here.
42
42
  // * Release. An override and a PR held at the gate are released from the card
43
- // through their own decide routes. A newer CORE is not: moving the live pin
44
- // runs its dry-run check first, on its own page, so the card's release goes
45
- // there.
43
+ // through their own decide routes. A newer CORE keeps its check-then-take:
44
+ // the card draws the Release desk's core row (check, then take it), the same
45
+ // as the floor desk; only a hall without that row links to the pin's page.
46
+ //
47
+ // ONE ACTION MODEL (task 1004566). The card's buttons and chips are the desk
48
+ // row's for the same item: GovDesk.cardFor() (street-desk.js) puts that row on
49
+ // the model as `row`, and render() draws row.actions and row.choices, sending
50
+ // each act through request() with the row's own card. "more" opens the detail
51
+ // in place (moreOf): the full text, who and when, and the record. Nothing on
52
+ // the card links to an older page to decide; the one link left is a sign-off
53
+ // that needs a recording ('record it'), which only the goal's page can take.
46
54
 
47
55
  (function (root) {
48
56
  'use strict';
@@ -342,9 +350,12 @@
342
350
  } else cv.result = 'You objected to ' + sectionWords(b.mine.section_key) + '. It is on the record.';
343
351
  return cv;
344
352
  }
345
- if (kind === 'idea') {
353
+ // A Full Idea is voted on here too (task 1004566): its sections open under
354
+ // "more", in front of the voter. Its author cannot vote on it (the server's
355
+ // anti-self rule) and does not withdraw it from here.
356
+ if (kind === 'idea' && !b.youOpen) {
346
357
  cv.readOnly = true;
347
- cv.result = m.state === 'needs' ? 'Read the idea in the Board Room and vote there, with its sections in front of you.' : 'Not yours to vote on.';
358
+ cv.result = b.isAuthor ? 'Yours. It is with the sitting now.' : 'Not yours to vote on.';
348
359
  return cv;
349
360
  }
350
361
  // task 1004564: the proposer may withdraw their own sitting until anyone has
@@ -358,10 +369,10 @@
358
369
  return cv;
359
370
  }
360
371
  if (b.youOpen) {
372
+ // the reason for an objection is one tap: the desk row's chips (GovDesk.CHIPS)
361
373
  cv.actions = [act('for', 'vote for', 'primary'), act('object', 'object', 'quiet')];
362
374
  cv.textActions = [act('abstain', 'abstain', 'text')];
363
- cv.objectForm = { sections: SECTIONS.slice() };
364
- cv.result = '';
375
+ cv.result = kind === 'idea' ? 'Read its sections under more, then vote.' : '';
365
376
  return cv;
366
377
  }
367
378
  cv.readOnly = true;
@@ -370,13 +381,16 @@
370
381
  }
371
382
 
372
383
  // ---------- the card for every other source ----------
373
- var READ_ONLY_WORDS = {
384
+ // The one fact a source's card says (task 1004566). Its acts are the ones the
385
+ // server handed the matter (`acts`), drawn from the desk row for the same item
386
+ // (GovDesk.cardFor), so the card never sends anyone to another page to decide.
387
+ var SOURCE_WORDS = {
374
388
  criteria_uat: 'Someone who did not build it tries it and signs it off.',
375
- blockers: 'It holds work until someone decides. It is decided where it lives.',
376
- removal_proposals: 'A proposal to take work out of a goal. It is decided where it lives.',
377
- redteam_reports: 'A report on the project\'s own walls. It is judged where it lives.',
378
- autobongos_fence: 'Whether work is built while no one is watching. It is switched where it lives.',
379
- module_rechecks: 'A module waits for its re-check. It is decided where it lives.',
389
+ blockers: 'It holds work until someone decides.',
390
+ removal_proposals: 'A proposal to take work out of a goal.',
391
+ redteam_reports: 'A report on the project\'s own walls.',
392
+ autobongos_fence: 'Whether work is built while no one is watching.',
393
+ module_rechecks: 'A module version waits for its check.',
380
394
  };
381
395
  function joinGoalId(m) { var hit = /goal\/(\d+)/.exec(str(m.href)); return hit ? hit[1] : null; }
382
396
 
@@ -386,21 +400,20 @@
386
400
  cv.subject = { kind: m.source, id: id, who: who, goalId: m.source === 'goal_join_requests' ? joinGoalId(m) : null };
387
401
  var mine = m.state === 'needs';
388
402
  if (m.source === 'access_requests') {
389
- if (who) cv.quote = { by: '', words: who + ' asked to join as a builder' + (/vouched/.test(str(m.where)) ? ', and someone vouched for them.' : '.') };
403
+ if (who) cv.asked = who + ' asked to join as a builder' + (/vouched/.test(str(m.where)) ? ', and someone vouched for them.' : '.');
390
404
  if (!mine) { cv.readOnly = true; cv.result = 'Not yours to decide.'; return cv; }
391
405
  cv.result = 'The call is yours. Fine by you lets them in; an objection keeps them out.';
392
406
  cv.actions = [act('fine', 'fine by me', 'primary'), act('refuse', 'object', 'quiet')];
393
407
  return cv;
394
408
  }
395
409
  if (m.source === 'goal_join_requests') {
396
- if (who) cv.quote = { by: '', words: who + ' asked to join this goal\'s team.' };
397
- if (!mine || !cv.subject.goalId) { cv.readOnly = true; cv.result = mine ? 'Decide it on the goal\'s page.' : 'You asked. The goal\'s owner decides.'; return cv; }
410
+ if (who) cv.asked = who + ' asked to join this goal\'s team.';
411
+ if (!mine || !cv.subject.goalId) { cv.readOnly = true; cv.result = mine ? 'The goal could not be named, so it cannot be decided here.' : 'You asked. The goal\'s owner decides.'; return cv; }
398
412
  cv.result = 'The goal\'s team is yours to keep. Fine by you lets them in.';
399
413
  cv.actions = [act('fine', 'fine by me', 'primary'), act('refuse', 'object', 'quiet')];
400
414
  return cv;
401
415
  }
402
416
  if (m.source === 'override_requests' || m.source === 'gate_approvals') {
403
- if (who) cv.quote = { by: '', words: 'Raised by ' + who + '.' };
404
417
  if (!mine) { cv.readOnly = true; cv.result = 'Someone who may decide it will.'; return cv; }
405
418
  cv.result = m.source === 'gate_approvals'
406
419
  ? 'It is held at the gate. Releasing it lets it merge and go live. The call is yours.'
@@ -410,15 +423,68 @@
410
423
  }
411
424
  if (m.source === 'core_update') {
412
425
  if (!mine) { cv.readOnly = true; cv.result = 'Someone who may move the core will.'; return cv; }
413
- cv.result = 'A newer core is out. Taking it moves the live pin, so it runs its check first, where the pin is moved.';
426
+ // Its check-then-take is the Release desk's core row (over: core_update),
427
+ // which GovDesk.cardFor draws here. Only where no such row came (the hall
428
+ // runs without the provisioning module) is the pin moved on its own page.
429
+ cv.result = 'A newer core is out. Check it first; taking it moves the live pin.';
414
430
  cv.actions = [act('release', 'release, with its check', 'primary', { go: safeHref(m.href) || '/settings#software-update' }), act('hold', 'hold', 'quiet')];
415
431
  return cv;
416
432
  }
417
- cv.readOnly = true;
418
- cv.result = READ_ONLY_WORDS[m.source] || (mine ? 'This waits on you, where it lives.' : 'This is moving without you.');
433
+ // the server handed this viewer acts on it: the card draws the desk row's
434
+ cv.readOnly = !(Array.isArray(m.acts) && m.acts.length);
435
+ cv.result = SOURCE_WORDS[m.source] || (mine ? 'This waits on you.' : 'This is moving without you.');
419
436
  return cv;
420
437
  }
421
438
 
439
+ // ---------- more: the detail, in place (task 1004566) ----------
440
+ // What the old page showed, read from what the docket and the Board Room
441
+ // already hand this viewer: the full text, who and when, and the record. A
442
+ // Full Idea's sections are all here, so it is voted on with them in front of
443
+ // the voter (ADR 0175 §8) without leaving the street.
444
+ var IDEA_WORDS = {
445
+ purpose: 'what it is for', big_picture: 'the big picture', why_had: 'why we had it', why_matters: 'why it matters',
446
+ fit: 'how it fits the rest', scope: 'what it changes', build_context: 'what building it takes', success: 'how we will know it worked',
447
+ };
448
+ var MORE_TEXT = 4000;
449
+ var MORE_HISTORY = 8;
450
+ function dayOf(v) { var t = Date.parse(v); return isNaN(t) ? '' : new Date(t).toISOString().slice(0, 10); }
451
+ // the full text keeps its paragraphs and lines (clip() folds them); bounded
452
+ function bound(s) { return s.length > MORE_TEXT ? s.slice(0, MORE_TEXT - 1).trim() + '…' : s; }
453
+ function moreOf(cv, m, item) {
454
+ var text = [], when = [], history = [], who = '';
455
+ var name = m && m.proposer ? personName(m.proposer) : '';
456
+ if (item) {
457
+ var s = item.subject || {};
458
+ var k = subjectKind(item);
459
+ if (k === 'idea') {
460
+ var f = s.field_values || {};
461
+ Object.keys(f).forEach(function (key) { var w = str(f[key]).trim(); if (w) text.push({ label: IDEA_WORDS[key] || key.replace(/_/g, ' '), words: bound(w) }); });
462
+ } else {
463
+ var full = str(k === 'matter' ? s.detail_md : s.rationale_md).trim();
464
+ if (full) text.push({ label: '', words: bound(full) });
465
+ }
466
+ if (name) who = 'Put up by ' + name + '.';
467
+ if (dayOf(item.opened_at)) when.push('opened ' + dayOf(item.opened_at));
468
+ if (item.closed_at) when.push('closed ' + dayOf(item.closed_at));
469
+ else if (dayOf(item.closes_at)) when.push('closes ' + dayOf(item.closes_at));
470
+ if (cv.revision) history.push('an earlier sitting sent it back to be revised');
471
+ (item.votes || []).slice().sort(function (a, b) { return (Date.parse(a.cast_at) || 0) - (Date.parse(b.cast_at) || 0); }).forEach(function (v) {
472
+ var by = personName(v) || 'someone';
473
+ var said = v.direction === 'yes' ? by + ' voted for' : by + ' objected to ' + sectionWords(v.section_key) + (v.reason_md ? ': ' + clip(str(v.reason_md).replace(/\s+/g, ' ').trim(), 160) : '');
474
+ history.push([dayOf(v.cast_at), said].filter(Boolean).join(' · '));
475
+ });
476
+ } else if (m) {
477
+ if (cv.asked) text.push({ label: '', words: cv.asked });
478
+ if (m.detail) text.push({ label: '', words: bound(str(m.detail).trim()) });
479
+ if (name) who = 'Raised by ' + name + '.';
480
+ if (dayOf(m.opened_at)) when.push('opened ' + dayOf(m.opened_at));
481
+ if (dayOf(m.deadline)) when.push('closes ' + dayOf(m.deadline));
482
+ if (m.rule && RULE_WORDS[m.rule]) history.push('how it is decided: ' + RULE_WORDS[m.rule]);
483
+ }
484
+ if (!text.length && !who && !when.length && !history.length) return null;
485
+ return { text: text, who: who, when: when.join(' · '), history: history.slice(0, MORE_HISTORY) };
486
+ }
487
+
422
488
  // The whole card for one matter. ctx: { docket, key, room, constitution, viewerId, now }.
423
489
  // null for an unknown key. A board sitting the room has not been read for yet
424
490
  // is the short card with `loading: true`.
@@ -430,7 +496,8 @@
430
496
  var cv = {
431
497
  key: short.key, building: short.building, where: short.where, when: short.when, title: short.title, href: short.href,
432
498
  source: null, quote: null, change: null, revision: null, seats: [], tally: '', voices: [], counsel: null,
433
- result: short.result, actions: [], textActions: [], objectForm: null, reviseForm: null, readOnly: false, loading: false, lines: short.lines || [], subject: null,
499
+ result: short.result, actions: [], textActions: [], reviseForm: null, readOnly: false, loading: false, lines: short.lines || [], subject: null,
500
+ more: null, row: null,
434
501
  };
435
502
  if (short.key === 'queue') { cv.source = 'queue'; cv.readOnly = true; return cv; }
436
503
  var m = findMatter(c.docket, c.key);
@@ -446,9 +513,13 @@
446
513
  cv.result = c.room ? 'This sitting could not be read just now.' : 'Reading the sitting…';
447
514
  return cv;
448
515
  }
449
- return boardCard(cv, m, item, { docket: c.docket, room: c.room, constitution: c.constitution, viewerId: viewerId });
516
+ boardCard(cv, m, item, { docket: c.docket, room: c.room, constitution: c.constitution, viewerId: viewerId });
517
+ cv.more = moreOf(cv, m, item);
518
+ return cv;
450
519
  }
451
- return sourceCard(cv, m);
520
+ sourceCard(cv, m);
521
+ cv.more = moreOf(cv, m, null);
522
+ return cv;
452
523
  }
453
524
 
454
525
  // ---------- the acts the server hands a row (task 1004559) ----------
@@ -461,7 +532,7 @@
461
532
  return {
462
533
  key: str(x.key), building: x.building || null, where: '', when: '', title: str(x.title), href: safeHref(x.href),
463
534
  source: str(x.kind), quote: null, change: null, revision: null, seats: [], tally: '', voices: [], counsel: null,
464
- result: '', actions: [], textActions: [], objectForm: null, reviseForm: null, readOnly: false, loading: false, lines: [],
535
+ result: '', actions: [], textActions: [], reviseForm: null, readOnly: false, loading: false, lines: [], more: null, row: null,
465
536
  subject: { kind: str(x.kind), id: str(x.id), who: str(x.who) },
466
537
  };
467
538
  }
@@ -521,6 +592,15 @@
521
592
  module_off: function (id) { return post('/modules/' + id + '/enable', { enabled: false }); },
522
593
  };
523
594
  ACT_ROUTES.backup = { backup: function () { return post('/backup/trigger', {}); } };
595
+ // A module version flagged by its security check is checked again (task
596
+ // 1004566): its ref id is "<module key>@<version>", the reassess route's two
597
+ // path parts; a reason is one tap.
598
+ ACT_ROUTES.module_rechecks = {
599
+ recheck: function (id, inp) {
600
+ var hit = /^([a-z0-9-]+)@(\d+\.\d+\.\d+)$/.exec(decoded(id));
601
+ return hit ? post('/store/modules/' + hit[1] + '/versions/' + hit[2] + '/reassess', { reason: 'Asked on the street: ' + (str(inp.label) || 'look again') + '.' }) : null;
602
+ },
603
+ };
524
604
  ACT_ROUTES.invite = { invite: function (id, inp) { return str(inp.option).trim() ? post('/access-requests/invite', { github_login: str(inp.option).trim() }) : null; } };
525
605
  ACT_ROUTES.autonomy_fence = ACT_ROUTES.autobongos_fence;
526
606
  // The Planning desks (task 1004560). A blocker is decided by one of its chips,
@@ -667,8 +747,9 @@
667
747
  rot: 'Rot timer set.', module_on: 'Turned on.', module_off: 'Turned off.', backup: 'Backing up now.', invite: '{who} is invited.',
668
748
  decide: 'Decided. The work it held moves again.', ratify: 'Ratified. The promise is on its version.', ratify_goal: 'Ratified. It is a goal now.',
669
749
  budget: '{who} has a new monthly limit.', unsign: 'Undone. The promise is open again.',
750
+ recheck: 'Checking it again. The answer lands at Release when it is done.',
670
751
  };
671
- var ACT_KINDS = { budget: 'light', unsign: 'settle', decide: 'rise', ratify: 'rise', ratify_goal: 'rise', check: 'light', rot: 'light', module_on: 'rise', module_off: 'settle', backup: 'light', confirm: 'settle', dispute: 'settle', fix: 'rise', remove: 'settle', keep: 'settle', signoff: 'rise', force: 'rise', switch_on: 'rise', switch_off: 'settle', allow: 'rise', disallow: 'settle', first: 'light', pause: 'settle', resume: 'rise', tier: 'light', deactivate: 'settle', give: 'light', take: 'settle' };
752
+ var ACT_KINDS = { recheck: 'light', budget: 'light', unsign: 'settle', decide: 'rise', ratify: 'rise', ratify_goal: 'rise', check: 'light', rot: 'light', module_on: 'rise', module_off: 'settle', backup: 'light', confirm: 'settle', dispute: 'settle', fix: 'rise', remove: 'settle', keep: 'settle', signoff: 'rise', force: 'rise', switch_on: 'rise', switch_off: 'settle', allow: 'rise', disallow: 'settle', first: 'light', pause: 'settle', resume: 'rise', tier: 'light', deactivate: 'settle', give: 'light', take: 'settle' };
672
753
 
673
754
  // A refused act, in words. The server's own message wins when it has one.
674
755
  var REFUSALS = {
@@ -720,8 +801,9 @@
720
801
  return b;
721
802
  }
722
803
 
723
- // Build the card's body into `host` (emptied first). on: { act(key, input), openForm(name) }.
724
- // ui: { form: 'object'|'revise'|null, error: '', busy: false, fresh: seatKey|null }.
804
+ // Build the card's body into `host` (emptied first). on: { act(key, input),
805
+ // openForm(name), ask(key), more() }. ui: { form: 'revise'|null, ask: actKey|null,
806
+ // more: false, error: '', busy: false, fresh: seatKey|null }.
725
807
  function render(doc, host, cv, on, ui) {
726
808
  var u = ui || {};
727
809
  while (host.firstChild) host.removeChild(host.firstChild);
@@ -785,20 +867,10 @@
785
867
  host.appendChild(co);
786
868
  }
787
869
  if (cv.result) host.appendChild(el(doc, 'p', 'street-card__result', cv.result));
788
- if (u.form === 'object' && cv.objectForm) host.appendChild(objectForm(doc, cv, on, u));
789
- else if (u.form === 'revise' && cv.reviseForm) host.appendChild(reviseForm(doc, cv, on, u));
870
+ if (u.form === 'revise' && cv.reviseForm) host.appendChild(reviseForm(doc, cv, on, u));
790
871
  else {
791
- if (cv.actions.length) {
792
- var row = el(doc, 'div', 'mcard-actions');
793
- cv.actions.forEach(function (a) {
794
- var btn;
795
- if (a.go) { btn = el(doc, 'a', 'mcard-btn mcard-btn--' + a.tone, a.label); btn.setAttribute('href', a.go); btn.setAttribute('data-act', a.key); }
796
- else btn = button(doc, 'mcard-btn mcard-btn--' + a.tone, a.label, a.key, function () { if (a.key === 'object') on.openForm('object'); else on.act(a.key, {}); }, u.busy);
797
- row.appendChild(btn);
798
- });
799
- host.appendChild(row);
800
- }
801
- if (cv.textActions.length) {
872
+ actionsInto(doc, host, cv, on, u);
873
+ if (cv.textActions.length && !cv.row) {
802
874
  var tr = el(doc, 'div', 'mcard-actions mcard-actions--text');
803
875
  cv.textActions.forEach(function (a) { tr.appendChild(button(doc, 'mcard-textbtn', a.label, a.key, function () { on.act(a.key, {}); }, u.busy)); });
804
876
  host.appendChild(tr);
@@ -809,9 +881,90 @@
809
881
  er.setAttribute('role', 'alert');
810
882
  host.appendChild(er);
811
883
  }
884
+ if (cv.more || cv.queue) moreInto(doc, host, cv, on, u);
812
885
  return host;
813
886
  }
814
887
 
888
+ // The acts (task 1004566): the desk row's buttons for this item, with its
889
+ // one-tap chips under the act that asks (a reason, an option, a GitHub name),
890
+ // or, for a card no row stands behind, the model's own. An act that asks
891
+ // opens its chips first; a chip sends the act with that chip's input.
892
+ function actionsInto(doc, host, cv, on, u) {
893
+ var r = cv.row;
894
+ var list = r ? r.actions : cv.actions;
895
+ if (!list.length) return;
896
+ var open = r ? (u.ask && r.choices[u.ask] ? u.ask : r.show || null) : null;
897
+ var row = el(doc, 'div', 'mcard-actions');
898
+ list.forEach(function (a) {
899
+ var btn;
900
+ if (a.go) { btn = el(doc, 'a', 'mcard-btn mcard-btn--' + a.tone, a.label); btn.setAttribute('href', a.go); btn.setAttribute('data-act', a.key); }
901
+ else {
902
+ btn = button(doc, 'mcard-btn mcard-btn--' + a.tone, a.label, a.key, function () { if (a.ask) on.ask(a.key); else on.act(a.key, {}); }, u.busy);
903
+ if (a.ask) btn.setAttribute('aria-expanded', open === a.key ? 'true' : 'false');
904
+ }
905
+ row.appendChild(btn);
906
+ });
907
+ host.appendChild(row);
908
+ if (cv.row && cv.textActions.length) {
909
+ var tr = el(doc, 'div', 'mcard-actions mcard-actions--text');
910
+ cv.textActions.forEach(function (a) { tr.appendChild(button(doc, 'mcard-textbtn', a.label, a.key, function () { on.act(a.key, {}); }, u.busy)); });
911
+ host.appendChild(tr);
912
+ }
913
+ var ch = open ? r.choices[open] : null;
914
+ if (!ch) return;
915
+ var box = el(doc, 'div', 'mcard-chips');
916
+ box.setAttribute('data-chips', open);
917
+ box.appendChild(el(doc, 'span', 'mcard-chips__ask', ch.ask));
918
+ if (ch.field) {
919
+ var input = el(doc, 'input', 'mcard-chips__field');
920
+ input.setAttribute('id', 'mcardField');
921
+ input.setAttribute('type', 'text');
922
+ input.setAttribute('aria-label', ch.ask);
923
+ input.setAttribute('maxlength', '39');
924
+ var send = function () { var v = input.value.trim(); if (v) on.act(open, { option: v, label: v }); };
925
+ input.addEventListener('keydown', function (e) { if (e.key === 'Enter') send(); });
926
+ box.appendChild(input);
927
+ box.appendChild(button(doc, 'mcard-chip', 'send', 'field-send', send, u.busy));
928
+ } else {
929
+ ch.options.forEach(function (c) { box.appendChild(button(doc, 'mcard-chip', c.label, 'chip-' + c.key, function () { on.act(open, c.input); }, u.busy)); });
930
+ }
931
+ host.appendChild(box);
932
+ }
933
+
934
+ // "more": the card grows in place with the detail (task 1004566). The toggle
935
+ // says whether it is open (aria-expanded); the queue's other pieces of work
936
+ // are listed there, each opening in this same card.
937
+ function moreInto(doc, host, cv, on, u) {
938
+ var open = !!u.more;
939
+ var t = button(doc, 'mcard-textbtn mcard-more__toggle', open ? 'less' : 'more', 'more', function () { on.more(); }, false);
940
+ t.setAttribute('aria-expanded', open ? 'true' : 'false');
941
+ t.setAttribute('aria-controls', 'mcardMore');
942
+ host.appendChild(t);
943
+ if (!open) return;
944
+ var box = el(doc, 'div', 'mcard-more');
945
+ box.setAttribute('id', 'mcardMore');
946
+ var mo = cv.more || { text: [], who: '', when: '', history: [] };
947
+ mo.text.forEach(function (x) {
948
+ if (x.label) box.appendChild(label(doc, x.label));
949
+ box.appendChild(el(doc, 'p', 'mcard-more__text', x.words));
950
+ });
951
+ var meta = [mo.who, mo.when].filter(Boolean).join(' ');
952
+ if (meta) box.appendChild(el(doc, 'p', 'mcard-more__meta', meta));
953
+ if (mo.history.length) {
954
+ box.appendChild(label(doc, 'the record'));
955
+ var ul = el(doc, 'ul', 'mcard-more__history');
956
+ mo.history.forEach(function (h) { ul.appendChild(el(doc, 'li', '', h)); });
957
+ box.appendChild(ul);
958
+ }
959
+ if (cv.queue && cv.queue.others.length) {
960
+ box.appendChild(label(doc, 'also waiting'));
961
+ var ql = el(doc, 'div', 'mcard-more__queue');
962
+ cv.queue.others.forEach(function (o) { ql.appendChild(button(doc, 'mcard-textbtn', o.title, 'pick', function () { on.pick(o.key); }, false)); });
963
+ box.appendChild(ql);
964
+ }
965
+ host.appendChild(box);
966
+ }
967
+
815
968
  function formShell(doc, name, labelText, field, fieldId) {
816
969
  var f = el(doc, 'div', 'mcard-form');
817
970
  f.setAttribute('data-form', name);
@@ -830,25 +983,6 @@
830
983
  return ta;
831
984
  }
832
985
 
833
- function objectForm(doc, cv, on, u) {
834
- var ta = textArea(doc, 'mcardReason', 3, '');
835
- var f = formShell(doc, 'object', 'why you object (it goes on the record)', ta, 'mcardReason');
836
- var sel = el(doc, 'select', 'mcard-form__select');
837
- sel.setAttribute('id', 'mcardSection');
838
- var none = el(doc, 'option', '', 'pick one');
839
- none.setAttribute('value', '');
840
- sel.appendChild(none);
841
- cv.objectForm.sections.forEach(function (s) { var o = el(doc, 'option', '', s.label); o.setAttribute('value', s.key); sel.appendChild(o); });
842
- var sl = el(doc, 'label', 'mcard-form__label', 'the part you object to');
843
- sl.setAttribute('for', 'mcardSection');
844
- f.appendChild(sl);
845
- f.appendChild(sel);
846
- var row = el(doc, 'div', 'mcard-actions');
847
- row.appendChild(button(doc, 'mcard-btn mcard-btn--danger', 'put it on the record', 'object-send', function () { on.act('object', { reason: ta.value, section: sel.value }); }, u.busy));
848
- row.appendChild(button(doc, 'mcard-btn mcard-btn--quiet', 'not now', 'form-close', function () { on.openForm(null); }, false));
849
- f.appendChild(row);
850
- return f;
851
- }
852
986
 
853
987
  function reviseForm(doc, cv, on, u) {
854
988
  var ta = textArea(doc, 'mcardRevise', 4, cv.reviseForm.text || '');
@@ -862,7 +996,7 @@
862
996
 
863
997
  var api = {
864
998
  API: API, SECTIONS: SECTIONS, needsRoom: needsRoom, itemIdOf: itemIdOf, subjectKind: subjectKind,
865
- model: model, actsCard: actsCard, request: request, orderRequest: orderRequest, check: check, answer: answer, refusal: refusal, render: render,
999
+ model: model, actsCard: actsCard, moreOf: moreOf, request: request, orderRequest: orderRequest, check: check, answer: answer, refusal: refusal, render: render,
866
1000
  memberWords: memberWords, windowWords: windowWords,
867
1001
  // the wording change, for a drafted amendment's struck and inserted text (task 1004530)
868
1002
  change: changeOf,