@bongos/core 1.21.43 → 1.21.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +80 -70
- package/docs/copy-inventory.md +34 -22
- package/docs/copy-registry.json +144 -26
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +5 -2
- package/docs/page-readings.json +573 -583
- package/modules/hall-ui/public/atlas.html +3 -3
- package/modules/hall-ui/public/blockers.html +3 -3
- package/modules/hall-ui/public/board-room.html +3 -3
- package/modules/hall-ui/public/city-draw.js +51 -13
- package/modules/hall-ui/public/city.css +9 -2
- package/modules/hall-ui/public/city.html +6 -6
- package/modules/hall-ui/public/city.js +23 -12
- package/modules/hall-ui/public/city.states.json +146 -12
- package/modules/hall-ui/public/collab.html +3 -3
- package/modules/hall-ui/public/commands.html +3 -3
- package/modules/hall-ui/public/copy-desk.html +3 -3
- package/modules/hall-ui/public/craft-mode.js +8 -1
- package/modules/hall-ui/public/deploy.html +3 -3
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/drachmae.html +3 -3
- package/modules/hall-ui/public/fleet.html +3 -3
- package/modules/hall-ui/public/gate.html +3 -3
- package/modules/hall-ui/public/goal-map.html +3 -3
- package/modules/hall-ui/public/goals.html +3 -3
- package/modules/hall-ui/public/government.html +3 -3
- package/modules/hall-ui/public/idea.html +3 -3
- package/modules/hall-ui/public/ideas.html +3 -3
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/modules.html +3 -3
- package/modules/hall-ui/public/paintbrush.js +398 -0
- package/modules/hall-ui/public/primer.html +3 -3
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/project-settings.html +3 -3
- package/modules/hall-ui/public/ranks.html +3 -3
- package/modules/hall-ui/public/roadmap.html +3 -3
- package/modules/hall-ui/public/roster.html +3 -3
- package/modules/hall-ui/public/sessions.html +3 -3
- package/modules/hall-ui/public/settings.html +3 -3
- package/modules/hall-ui/public/street-card.css +6 -0
- package/modules/hall-ui/public/street-card.js +31 -3
- package/modules/hall-ui/public/street-draw.js +96 -19
- package/modules/hall-ui/public/street.css +23 -7
- package/modules/hall-ui/public/street.html +11 -10
- package/modules/hall-ui/public/street.js +45 -13
- package/modules/hall-ui/public/street.states.json +68 -0
- package/modules/hall-ui/public/studio.html +3 -3
- package/modules/hall-ui/public/style.css +78 -0
- package/modules/hall-ui/public/task.html +3 -3
- package/modules/hall-ui/public/thinking.html +3 -3
- package/modules/hall-ui/public/tweak-editor.html +3 -3
- package/modules/hall-ui/public/watch.html +3 -3
- package/modules/hall-ui/public/work.html +3 -3
- package/modules/hall-ui/records/craft-mode.md +9 -0
- package/modules/hall-ui/records/governor-street.md +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/hall-preview/server.js +5 -2
- package/src/module-api.js +1 -1
- package/tests/hall_city.mjs +42 -11
- package/tests/hall_matter_card.mjs +17 -0
- package/tests/hall_street.mjs +185 -12
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/page_notes_panel.mjs +4 -3
- package/tests/paintbrush.mjs +351 -0
|
@@ -1560,6 +1560,14 @@ html[data-theme="light"][data-craft="ideating"] .app-sidebar {
|
|
|
1560
1560
|
.craft-tool { width: auto; gap: 6px; padding: 0 12px 0 10px; }
|
|
1561
1561
|
}
|
|
1562
1562
|
@media (max-width: 1180px) { .craft-tool__label { display: none; } }
|
|
1563
|
+
/* Two tools (Artistry's palette and paintbrush) must still fit a 390px top bar
|
|
1564
|
+
(task 1004497): at phone width the side packs tighter and a tool is 28px. */
|
|
1565
|
+
@media (max-width: 640px) {
|
|
1566
|
+
.topbar-side, .topbar-menu { flex-shrink: 0; }
|
|
1567
|
+
.topbar-side { gap: 4px; }
|
|
1568
|
+
.craft-tool { width: 28px; height: 28px; }
|
|
1569
|
+
.topbar-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1570
|
+
}
|
|
1563
1571
|
|
|
1564
1572
|
/* The page feedback doc: a sheet from the right over a scrim. */
|
|
1565
1573
|
html.pn-open { overflow: hidden; }
|
|
@@ -1676,6 +1684,76 @@ html.pn-open { overflow: hidden; }
|
|
|
1676
1684
|
.pn__body { padding: 14px 16px 24px; }
|
|
1677
1685
|
}
|
|
1678
1686
|
|
|
1687
|
+
/* THE PAINTBRUSH (task 1004497, paintbrush.js): the page's own words, edited
|
|
1688
|
+
where they stand. Not modal: the page stays live under it, so the bar docks
|
|
1689
|
+
at the foot and the words carry the mode's colour as a dashed underline. A
|
|
1690
|
+
changed line keeps a quiet wash so the artist can see what moved. */
|
|
1691
|
+
.pb {
|
|
1692
|
+
position: fixed;
|
|
1693
|
+
left: 50%;
|
|
1694
|
+
bottom: calc(16px + env(safe-area-inset-bottom, 0px));
|
|
1695
|
+
z-index: var(--z-drawer);
|
|
1696
|
+
width: min(640px, calc(100% - 24px));
|
|
1697
|
+
transform: translateX(-50%);
|
|
1698
|
+
box-sizing: border-box;
|
|
1699
|
+
border-radius: 18px;
|
|
1700
|
+
background: var(--bg-card);
|
|
1701
|
+
color: var(--ink);
|
|
1702
|
+
box-shadow: 0 0 0 1px var(--glass-edge), var(--shadow-pop);
|
|
1703
|
+
}
|
|
1704
|
+
.pb__inner { display: flex; flex-direction: column; gap: 8px; max-height: min(70vh, 560px); overflow-y: auto; padding: 14px 18px 16px; }
|
|
1705
|
+
.pb__eyebrow {
|
|
1706
|
+
display: flex; align-items: center; gap: 8px; margin: 0;
|
|
1707
|
+
font-family: var(--label-face); font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
|
|
1708
|
+
color: var(--craft, var(--ink-faint));
|
|
1709
|
+
}
|
|
1710
|
+
.pb__dot { width: 8px; height: 8px; border-radius: 50%; background: var(--craft, var(--accent)); }
|
|
1711
|
+
.pb__line { margin: 0; font-size: 15px; line-height: 1.45; }
|
|
1712
|
+
.pb__line strong { font-weight: 500; }
|
|
1713
|
+
.pb__hint { margin: 0; font-size: 13px; line-height: 1.45; color: var(--ink-soft); }
|
|
1714
|
+
.pb__hint a, .pb__line a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
|
|
1715
|
+
.pb__error { margin: 0; font-size: 13.5px; color: var(--danger); }
|
|
1716
|
+
.pb__error:empty { display: none; }
|
|
1717
|
+
.pb__error-inline { color: var(--danger); }
|
|
1718
|
+
.pb__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; padding-top: 4px; }
|
|
1719
|
+
.pb__btn {
|
|
1720
|
+
min-height: 40px; padding: 0 18px; border: 0; border-radius: var(--radius-pill);
|
|
1721
|
+
background: none; color: var(--ink); font: inherit; font-size: 14px; cursor: pointer;
|
|
1722
|
+
box-shadow: inset 0 0 0 1px var(--rule);
|
|
1723
|
+
}
|
|
1724
|
+
.pb__btn:hover { background: var(--glass); }
|
|
1725
|
+
.pb__btn--go { background: var(--accent); color: var(--accent-on); box-shadow: none; font-weight: 500; }
|
|
1726
|
+
.pb__btn--go:hover { background: var(--accent-lift); }
|
|
1727
|
+
.pb__btn:disabled { opacity: .5; cursor: default; }
|
|
1728
|
+
.pb__btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
1729
|
+
.pb__changes { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
1730
|
+
.pb__change { display: grid; gap: 3px; padding: 10px 12px; border-radius: var(--radius-sm); background: var(--glass); overflow-wrap: anywhere; }
|
|
1731
|
+
.pb__where { font-size: 12px; color: var(--ink-faint); }
|
|
1732
|
+
.pb__was { font-size: 14px; color: var(--ink-soft); text-decoration: line-through; text-decoration-color: var(--ink-faint); }
|
|
1733
|
+
.pb__now { font-size: 14px; color: var(--ink); }
|
|
1734
|
+
.pb__tag {
|
|
1735
|
+
display: inline-block; min-width: 34px; margin-right: 4px;
|
|
1736
|
+
font-family: var(--label-face); font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase;
|
|
1737
|
+
color: var(--ink-faint); text-decoration: none;
|
|
1738
|
+
}
|
|
1739
|
+
html.pb-editing main { padding-bottom: 180px; }
|
|
1740
|
+
.pb-word {
|
|
1741
|
+
outline: none;
|
|
1742
|
+
cursor: text;
|
|
1743
|
+
text-decoration: underline dashed color-mix(in srgb, var(--craft, var(--accent)) 70%, transparent);
|
|
1744
|
+
text-decoration-thickness: 1px;
|
|
1745
|
+
text-underline-offset: 4px;
|
|
1746
|
+
border-radius: 3px;
|
|
1747
|
+
}
|
|
1748
|
+
.pb-word:hover { background: color-mix(in srgb, var(--craft, var(--accent)) 8%, transparent); }
|
|
1749
|
+
.pb-word:focus, .pb-word:focus-visible { background: color-mix(in srgb, var(--craft, var(--accent)) 12%, transparent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--craft, var(--accent)) 55%, transparent); }
|
|
1750
|
+
.pb-word--changed { background: color-mix(in srgb, var(--craft, var(--accent)) 14%, transparent); text-decoration-style: solid; }
|
|
1751
|
+
@media (max-width: 640px) {
|
|
1752
|
+
.pb { bottom: calc(10px + env(safe-area-inset-bottom, 0px)); width: calc(100% - 20px); border-radius: 16px; }
|
|
1753
|
+
.pb__inner { padding: 12px 14px 14px; max-height: 64vh; }
|
|
1754
|
+
.pb__actions .pb__btn { flex: 1 1 auto; }
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1679
1757
|
/* THE BACK LINK (task 1004430): at the head of the bar when the page was
|
|
1680
1758
|
reached from inside another hall page. 24px floor on both axes. */
|
|
1681
1759
|
.topbar-back {
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
|
|
55
55
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
56
56
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
57
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
58
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
59
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
57
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
58
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
59
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
60
60
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
61
61
|
<script src="/builders/palette.js" defer></script>
|
|
62
62
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
|
|
@@ -138,9 +138,9 @@
|
|
|
138
138
|
</div>
|
|
139
139
|
</div>
|
|
140
140
|
|
|
141
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
142
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
143
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
141
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
142
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
143
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
144
144
|
<script src="/builders/palette.js" defer></script>
|
|
145
145
|
|
|
146
146
|
<script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
|
|
@@ -142,9 +142,9 @@
|
|
|
142
142
|
|
|
143
143
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
144
144
|
|
|
145
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
146
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
147
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
145
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
146
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
147
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
148
148
|
<script src="/builders/palette.js" defer></script>
|
|
149
149
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
150
150
|
<script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
|
|
@@ -162,9 +162,9 @@
|
|
|
162
162
|
|
|
163
163
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
164
164
|
chrome exists before the page script runs. -->
|
|
165
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
166
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
167
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
165
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
166
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
167
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
168
168
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
169
169
|
<script src="/builders/palette.js" defer></script>
|
|
170
170
|
<!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
|
|
@@ -171,9 +171,9 @@
|
|
|
171
171
|
|
|
172
172
|
<!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
|
|
173
173
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
174
|
-
<script src="/builders/shell-trail.js?v=2026-10-04-
|
|
175
|
-
<script src="/builders/craft-mode.js?v=2026-10-04-
|
|
176
|
-
<script src="/builders/shell.js?v=2026-10-04-
|
|
174
|
+
<script src="/builders/shell-trail.js?v=2026-10-04-paintbrush"></script>
|
|
175
|
+
<script src="/builders/craft-mode.js?v=2026-10-04-paintbrush"></script>
|
|
176
|
+
<script src="/builders/shell.js?v=2026-10-04-paintbrush"></script>
|
|
177
177
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
178
178
|
<script src="/builders/palette.js" defer></script>
|
|
179
179
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
|
|
@@ -35,3 +35,12 @@ Covers: every shelled hall page (`craft-mode.js`, `shell.js`, `style.css`), and
|
|
|
35
35
|
- **Two pieces are drawn by `craft-mode.js` `applyLook()`, on every mode change:** the eyebrow beside `#topbar-title` (`LOOKS`: "the studio · night", "the sky", "the city"; hidden at <=640px, where it overflowed phones) and the scene plate under `.app-sidebar__brand` (40px closed rail, 64px open). The plate's markup IS `artHtml(mode)`, the picker's door art, exported rather than copied; light mode recolours it to each scene's day (the city is the dark one through `invert(1) hue-rotate(180deg)`). It does not animate.
|
|
36
36
|
- **Always-present tabs widened** (owner, 2026-10-03): `ALWAYS_TABS` is Builders, Collab and Profile, so every mode keeps the You and Community groups.
|
|
37
37
|
- **Governing's header tallies** ("needs you · moving") are task 1004563, not this look.
|
|
38
|
+
|
|
39
|
+
## The paintbrush (task 1004497)
|
|
40
|
+
|
|
41
|
+
- **Artistry carries two tools**: the palette (a feedback doc, `page-notes.js`, record `page-notes.md`) and the paintbrush (`paintbrush.js`). Both are `TOOLS` entries in `craft-mode.js` and load their own script on first press.
|
|
42
|
+
- **The paintbrush edits the page's own words where they stand, as a REAL wording change** (owner, 2026-10-04): it is the copy desk's page round, the same one the tweak editor and the studio's own words write (ADR 0341). Pressing it reads `GET /copy-desk/pages/builders:<page>/draft`; a page nobody holds is OFFERED, never taken silently (taking it is the copy desk's artists-only claim, and the page shows as being tweaked while held). Typing saves the whole draft; "Review" lists every change as was/now; "Send for approval" submits the round. It never writes `page-notes` (owner: kept separate from the feedback docs).
|
|
43
|
+
- **Only a leaf element inside `<main>` whose words are one reading line becomes editable.** Shared chrome, a line with a `{…}` hole, controls (`button`, form fields, `code`), and anything hidden stay as they are; the bar counts the lines it cannot reach and links to the tweak editor for them. A click on edited words inside a link does not navigate.
|
|
44
|
+
- **Nobody else's view changes, and neither does yours once editing ends**: "Stop editing" and "Send" both put the page's own words back. The draft stays on the round.
|
|
45
|
+
- **The top bar fits two tools at 390px** because, at phone width, the side packs to a 4px gap, a tool is 28px, the side and the menu do not shrink, and a long page title trims with an ellipsis instead. All 34 hall pages measure 0px of sideways scroll in Artistry at 390px.
|
|
46
|
+
- `tests/paintbrush.mjs` runs the real script and both shared libs over a small page.
|
|
@@ -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
|
+
- **One pin per building, its importance at a glance** (task 1004569, owner direction 2026-10-04: the pins stacked and their text overlapped). The prototype stood one pin per matter, up to three stacked; now one pin stands over a building where the prototype stands its pin, in the tier of its most important matter (`GovCity.buildingPin`, shared with the home city so both agree): urgent (needs you, due within 24 hours or overdue) largest, orange and pulsing; needs you orange, the prototype's size; moving small and ink; settled a grey dot with no label and no count. A badge counts the building's open matters when there is more than one. Only urgent and needs-you pins show a label, one short line cut by the line rule's `fit()` (the deadline always survives the cut); a moving pin shows its label on hover or focus. A collision pass (`placeLabels`) places labels most important first: a label that would meet one already placed steps up to an alternate height, and if it still meets one it waits for hover (its count and colour stay). Tapping a pin opens the most important matter's card; with more, the card carries "1 of N" with previous and next, which wrap. The header's dots wear the pin colours, so the counts double as the legend. Rendered through `street.states.json` (`pins`, `pins-pager`) and `city.states.json` (`pins`) at 1440 and 390, light and dark, on the hall-preview harness, whose docket fixture now holds every tier.
|
|
13
14
|
- **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.
|
|
14
15
|
|
|
15
16
|
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.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.45",
|
|
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.45",
|
|
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.45",
|
|
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
|
@@ -9022,5 +9022,17 @@
|
|
|
9022
9022
|
"id": "1004567",
|
|
9023
9023
|
"text": "Legacy mode is gone from the builders hall. The mode picker now offers four doors (Building, Artistry, Ideating, Governing) and the top-bar switch shows those four. Anyone who still had Legacy saved simply lands with no mode c"
|
|
9024
9024
|
}
|
|
9025
|
+
],
|
|
9026
|
+
"1.21.44": [
|
|
9027
|
+
{
|
|
9028
|
+
"id": "1004569",
|
|
9029
|
+
"text": "The street's pins no longer pile up or talk over each other. Each building now has one pin, and its colour and size show how much it matters: largest and pulsing orange when something needs you within a day, orange when it nee"
|
|
9030
|
+
}
|
|
9031
|
+
],
|
|
9032
|
+
"1.21.45": [
|
|
9033
|
+
{
|
|
9034
|
+
"id": "1004497",
|
|
9035
|
+
"text": "Artists can now edit any hall page's words right where they stand. In Artistry mode, \"Edit words\" offers to take the page; the words get an underline and can be typed over, a review lists each change as was/now, and \"Send for"
|
|
9036
|
+
}
|
|
9025
9037
|
]
|
|
9026
9038
|
}
|
|
@@ -195,18 +195,21 @@ const CANNED_WRITES = [
|
|
|
195
195
|
// year from now. Any string field may use it; nothing else changes, so a fixture
|
|
196
196
|
// with absolute dates is served exactly as before.
|
|
197
197
|
//
|
|
198
|
+
// A deadline still to come is "@+20.5h" (task 1004569: the street's urgent
|
|
199
|
+
// tier is a matter that needs you within a day, so it must stay a day ahead).
|
|
200
|
+
//
|
|
198
201
|
// WRITE A FRACTIONAL OFFSET. "@-3d" lands exactly on the 3-day band boundary, so
|
|
199
202
|
// whether it reads as 2 days or 3 depends on the milliseconds between this
|
|
200
203
|
// resolve and the browser's own clock — the row flickers between two bands
|
|
201
204
|
// across reloads and a screenshot pass cannot be compared. "@-3.4d" cannot.
|
|
202
|
-
const RELATIVE_AT =
|
|
205
|
+
const RELATIVE_AT = /^@([-+])(\d+(?:\.\d+)?)([dh])$/;
|
|
203
206
|
const HOUR_MS = 60 * 60 * 1000;
|
|
204
207
|
|
|
205
208
|
function resolveRelativeDates(v, nowMs) {
|
|
206
209
|
if (typeof v === 'string') {
|
|
207
210
|
const m = RELATIVE_AT.exec(v);
|
|
208
211
|
if (!m) return v;
|
|
209
|
-
const hours = Number(m[
|
|
212
|
+
const hours = Number(m[2]) * (m[3] === 'd' ? 24 : 1) * (m[1] === '+' ? -1 : 1);
|
|
210
213
|
return new Date(nowMs - hours * HOUR_MS).toISOString();
|
|
211
214
|
}
|
|
212
215
|
if (Array.isArray(v)) return v.map((x) => resolveRelativeDates(x, nowMs));
|
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.45'; // 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');
|
package/tests/hall_city.mjs
CHANGED
|
@@ -142,12 +142,38 @@ test('the line is the docket\'s own line, word for word', () => {
|
|
|
142
142
|
assert.equal(m.line, '1 matter waits on you');
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
// task 1004569 (owner, 2026-10-04): deliberate change from the prototype's
|
|
146
|
+
// drawing — ONE pin per building, in its most important matter's tier, the
|
|
147
|
+
// same tiers as the street (GovCity.buildingPin is the one rule both use).
|
|
148
|
+
test('one pin per building, in the tier of its most important matter, with the count of its open matters', () => {
|
|
149
|
+
const NOW = Date.parse('2026-10-02T12:00:00.000Z');
|
|
150
|
+
const ms = ['moving', 'needs', 'moving', 'settled'].map((state, i) => matter({ id: `blockers:${i}`, state }));
|
|
151
|
+
ms.push(matter({ id: 'blockers:9', state: 'needs', deadline: '2026-10-03T06:00:00.000Z' }));
|
|
152
|
+
const m = City.cityModel({ docket: docket(ms), mode: 'dark', now: NOW });
|
|
153
|
+
assert.equal(m.pins.length, 1, 'one pin, never more');
|
|
154
|
+
assert.deepEqual([m.pins[0].key, m.pins[0].state, m.pins[0].count], ['blockers:9', 'urgent', 4], 'urgent (needs you, due within a day); four open');
|
|
155
|
+
assert.deepEqual(m.pins[0].matters, ['blockers:9', 'blockers:1', 'blockers:0', 'blockers:2', 'blockers:3']);
|
|
156
|
+
const k = City.isoKit(60, 30, 720, 176, 30 / 28, City.isoTheme('dark'), false);
|
|
157
|
+
const top = k.P(6, 6, City.ROOF.planning + 8);
|
|
158
|
+
assert.equal(m.pins[0].x, Math.round((top[0] - 15) * 10) / 10, 'where the prototype stands a single pin');
|
|
159
|
+
const tier = (list) => City.cityModel({ docket: docket(list), mode: 'dark', now: NOW }).pins[0].state;
|
|
160
|
+
assert.equal(tier([matter({ state: 'needs', deadline: '2026-10-05T12:00:00.000Z' })]), 'needs', 'due after a day');
|
|
161
|
+
assert.equal(tier([matter({ state: 'settled' }), matter({ id: 'b:2', state: 'moving' })]), 'moving');
|
|
162
|
+
const settled = City.cityModel({ docket: docket([matter({ state: 'settled' })]), mode: 'dark', now: NOW }).pins[0];
|
|
163
|
+
assert.deepEqual([settled.state, settled.count], ['settled', 0], 'settled: a dot, no count');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('the city and the street agree: the same docket gives each building the same tier and the same top matter', () => {
|
|
167
|
+
const S = require(path.join(PUB, 'street-draw.js'));
|
|
168
|
+
const NOW = Date.parse('2026-10-02T12:00:00.000Z');
|
|
169
|
+
const ms = [
|
|
170
|
+
matter({ id: 'board:1', building: 'charter', state: 'needs', deadline: '2026-10-02T20:00:00.000Z' }), matter({ id: 'board:2', building: 'charter', state: 'moving' }),
|
|
171
|
+
matter({ id: 'blockers:3', state: 'moving' }), matter({ id: 'blockers:4', state: 'needs' }),
|
|
172
|
+
matter({ id: 'override_requests:5', building: 'release', state: 'settled' }),
|
|
173
|
+
];
|
|
174
|
+
const city = City.cityModel({ docket: docket(ms), mode: 'dark', now: NOW }).pins.map((p) => [p.building, p.state, p.key, p.matters.join(' ')]).sort();
|
|
175
|
+
const street = S.streetModel({ docket: docket(ms), goals: [], rollups: [], mode: 'dark', now: NOW }).pins.map((p) => [p.building, p.state, p.key, p.matters.join(' ')]).sort();
|
|
176
|
+
assert.deepEqual(city, street);
|
|
151
177
|
});
|
|
152
178
|
|
|
153
179
|
test('a matter at a building the city does not have is not drawn, and an unknown state is never "needs"', () => {
|
|
@@ -346,16 +372,21 @@ const FEEDS = (ms, goals = [goal(1)], rollups = [rollup(1, { done: 2, active: 1
|
|
|
346
372
|
});
|
|
347
373
|
|
|
348
374
|
test('the page reads the three feeds and draws the docket\'s pins and line', async () => {
|
|
349
|
-
const
|
|
375
|
+
const soon = new Date(Date.now() + 2 * 3600000).toISOString();
|
|
376
|
+
const ms = [matter({ id: 'board:1', building: 'charter', state: 'needs', deadline: soon }), matter({ id: 'blockers:2', state: 'needs' }), matter({ id: 'blockers:4', state: 'moving' }), matter({ id: 'gate_approvals:3', building: 'release', state: 'moving' }), matter({ id: 'removal_proposals:5', building: 'disputes', state: 'settled' })];
|
|
350
377
|
const p = await bootPage({ feeds: FEEDS(ms) });
|
|
351
378
|
assert.deepEqual([...p.calls].sort(), ['/api/bongos/goals', '/api/bongos/goals/rollups', '/api/bongos/government/docket']);
|
|
352
379
|
const pins = p.byId.get('cityPins').children;
|
|
353
|
-
assert.equal(pins.length,
|
|
354
|
-
assert.deepEqual(pins.map((s) => s.className).sort(), ['city-pin city-pin--moving', 'city-pin city-pin--needs', 'city-pin city-pin--
|
|
355
|
-
const
|
|
356
|
-
assert.equal(
|
|
380
|
+
assert.equal(pins.length, 4, 'one per building');
|
|
381
|
+
assert.deepEqual(pins.map((s) => s.className).sort(), ['city-pin city-pin--moving', 'city-pin city-pin--needs', 'city-pin city-pin--settled', 'city-pin city-pin--urgent']);
|
|
382
|
+
const urgent = pins.find((s) => s.attrs['data-matter'] === 'board:1');
|
|
383
|
+
assert.equal(urgent.children[0].children[0].className, 'city-pin__ring', 'the urgent pin pulses');
|
|
384
|
+
const needs = pins.find((s) => s.className.includes('needs'));
|
|
385
|
+
assert.ok(!needs.children[0].children.some((c) => c.className === 'city-pin__ring'), 'a needs-you pin is solid orange, no ring');
|
|
386
|
+
assert.equal(needs.children[0].children.find((c) => c.className === 'city-pin__count').textContent, '2', 'it counts its building\'s two open matters');
|
|
357
387
|
const moving = pins.find((s) => s.className.includes('moving'));
|
|
358
388
|
assert.ok(!moving.children[0].children.some((c) => c.className === 'city-pin__ring'), 'a moving pin only bobs');
|
|
389
|
+
assert.equal(pins.find((s) => s.className.includes('settled')).children[0].children.length, 0, 'a settled pin is a plain dot');
|
|
359
390
|
assert.equal(p.byId.get('cityLine').textContent, '2 matters wait on you · 1 moving');
|
|
360
391
|
assert.equal(p.byId.get('cityStart').attrs.href, '/builders/street');
|
|
361
392
|
assert.ok(p.byId.get('cityBlds').children.length > 9, 'the civic quarter and the district are drawn');
|
|
@@ -590,3 +590,20 @@ test('the Board Room labels a building\'s matter as that building\'s matter, and
|
|
|
590
590
|
assert.equal(subjectLine({ subject_type: 'constitutional_amendment', subject: { kind: 'decision_rules' } }), 'Who decides what [amendment]');
|
|
591
591
|
assert.equal(subjectLine({ subject_type: 'constitutional_amendment', subject: { proposed: {} } }), 'Constitutional amendment [amendment]');
|
|
592
592
|
});
|
|
593
|
+
|
|
594
|
+
// task 1004569: a street pin stands for every matter at its building; the card
|
|
595
|
+
// pages through them so each is reachable without leaving the street.
|
|
596
|
+
test('the card pager: "i of N" over the pin\'s matters, previous and next wrapping; none for one matter', () => {
|
|
597
|
+
assert.equal(C.pager(['a'], 'a'), null, 'one matter, no pager');
|
|
598
|
+
assert.equal(C.pager(null, 'a'), null);
|
|
599
|
+
assert.equal(C.pager(['a', 'b'], 'zz'), null, 'a key the pin does not hold');
|
|
600
|
+
assert.deepEqual(C.pager(['a', 'b', 'c'], 'a'), { i: 0, n: 3, words: '1 of 3', prev: 'c', next: 'b' });
|
|
601
|
+
assert.deepEqual(C.pager(['a', 'b', 'c'], 'c'), { i: 2, n: 3, words: '3 of 3', prev: 'b', next: 'a' });
|
|
602
|
+
// walking next from the first visits every key exactly once
|
|
603
|
+
const keys = ['k1', 'k2', 'k3', 'k4', 'k5'];
|
|
604
|
+
const seen = [];
|
|
605
|
+
let k = keys[0];
|
|
606
|
+
for (let i = 0; i < keys.length; i++) { seen.push(k); k = C.pager(keys, k).next; }
|
|
607
|
+
assert.deepEqual(seen, keys);
|
|
608
|
+
assert.equal(k, keys[0]);
|
|
609
|
+
});
|