@bongos/core 1.21.36 → 1.21.37
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 +59 -54
- package/docs/copy-inventory.md +8 -8
- package/docs/copy-registry.json +8 -8
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +234 -234
- 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.html +3 -3
- 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 +44 -6
- 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/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.html +3 -3
- package/modules/hall-ui/public/studio.html +3 -3
- package/modules/hall-ui/public/style.css +121 -2
- 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 +8 -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/craft_mode_looks.mjs +170 -0
- package/tests/craft_mode_nav.mjs +7 -5
- package/tests/hall_tweak_editor.mjs +1 -1
|
@@ -1206,8 +1206,8 @@ html[data-fullscreen="on"][data-fullscreen-plain="on"] .app-content { padding-to
|
|
|
1206
1206
|
/* THE CRAFT MODE (task 1004492, craft-mode.js). The top bar's switch: one pill
|
|
1207
1207
|
holding the four crafts and Legacy, the current one lit. Narrower windows
|
|
1208
1208
|
drop the words, and a phone shows only the current mode (or "Mode"), which
|
|
1209
|
-
opens the picker. The picker below asks once per session.
|
|
1210
|
-
|
|
1209
|
+
opens the picker. The picker below asks once per session. Each mode's look
|
|
1210
|
+
(its colour on the current pill) is THE CRAFT MODE LOOKS, further down. */
|
|
1211
1211
|
.craft-switch {
|
|
1212
1212
|
display: inline-flex;
|
|
1213
1213
|
align-items: center;
|
|
@@ -1431,6 +1431,125 @@ html.craft-picker-open { overflow: hidden; }
|
|
|
1431
1431
|
.craft-picker__legacy { height: auto; min-height: 40px; white-space: normal; text-align: left; }
|
|
1432
1432
|
}
|
|
1433
1433
|
|
|
1434
|
+
/* THE CRAFT MODE LOOKS (task 1004495; direction docs/design/craft-mode-direction.md).
|
|
1435
|
+
Three modes wear a look: Artistry, Ideating, Governing. Building, Legacy and no
|
|
1436
|
+
mode wear none — nothing below names them, so they render as the hall always
|
|
1437
|
+
did (tests/craft_mode_looks.mjs pins it). --craft marks WHERE you are, never
|
|
1438
|
+
what to press: the accent stays the action colour in every mode. Light values
|
|
1439
|
+
clear 5.4:1 on the hall's paper; the dark values are the prototype's. Light
|
|
1440
|
+
keeps the paper ground; only dark changes the ground and the glass. */
|
|
1441
|
+
html[data-craft="artistry"] { --craft: #2e3c6e; --craft-on: #f1e6d2; }
|
|
1442
|
+
html[data-craft="ideating"] { --craft: #7a5a0c; --craft-on: #fdf6dd; }
|
|
1443
|
+
html[data-craft="governing"] { --craft: #33479e; --craft-on: #eef1ff; }
|
|
1444
|
+
html[data-theme="dark"][data-craft="artistry"] {
|
|
1445
|
+
--craft: #f1e6d2; --craft-on: #0c1122;
|
|
1446
|
+
--bg: #07080d; --bg-frame: #0a0b12;
|
|
1447
|
+
--glass: rgba(150, 170, 230, .11); --glass-strong: rgba(170, 190, 240, .17);
|
|
1448
|
+
--glass-edge: rgba(241, 230, 210, .16);
|
|
1449
|
+
}
|
|
1450
|
+
html[data-theme="dark"][data-craft="ideating"] {
|
|
1451
|
+
--craft: #f2d27a; --craft-on: #1d1604;
|
|
1452
|
+
--bg: #05060d;
|
|
1453
|
+
--glass: rgba(140, 150, 225, .10); --glass-strong: rgba(160, 170, 240, .16);
|
|
1454
|
+
--glass-edge: rgba(242, 210, 122, .16);
|
|
1455
|
+
}
|
|
1456
|
+
html[data-theme="dark"][data-craft="governing"] {
|
|
1457
|
+
--craft: #aabeff; --craft-on: #0a1030;
|
|
1458
|
+
--glass-edge: rgba(170, 190, 255, .16);
|
|
1459
|
+
}
|
|
1460
|
+
/* The shared treatment: the switch's current pill, the current tab and the top
|
|
1461
|
+
bar's hairline take the mode colour. */
|
|
1462
|
+
html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing"]) .craft-switch__opt[aria-pressed="true"] {
|
|
1463
|
+
background: var(--craft); color: var(--craft-on);
|
|
1464
|
+
}
|
|
1465
|
+
html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing"]) .nav-item--active {
|
|
1466
|
+
background: color-mix(in srgb, var(--craft) 14%, transparent); color: var(--craft);
|
|
1467
|
+
}
|
|
1468
|
+
html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing"]) .nav-item--active .nav-icon,
|
|
1469
|
+
html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing"]) .nav-item--active .nav-item__label { color: var(--craft); }
|
|
1470
|
+
html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing"]) .nav-item--active::before { background: var(--craft); }
|
|
1471
|
+
html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing"]) .app-topbar {
|
|
1472
|
+
border-bottom-color: color-mix(in srgb, var(--craft) 30%, transparent);
|
|
1473
|
+
}
|
|
1474
|
+
/* The header mark: the mode's own words beside the page title (craft-mode.js
|
|
1475
|
+
applyLook). Hidden on a phone — the switch's one pill already says the mode,
|
|
1476
|
+
and the bar has no room for both. */
|
|
1477
|
+
.craft-eyebrow {
|
|
1478
|
+
font-family: var(--label-face);
|
|
1479
|
+
font-size: 11px;
|
|
1480
|
+
font-weight: 500;
|
|
1481
|
+
letter-spacing: .14em;
|
|
1482
|
+
text-transform: uppercase;
|
|
1483
|
+
color: var(--craft);
|
|
1484
|
+
white-space: nowrap;
|
|
1485
|
+
flex-shrink: 0;
|
|
1486
|
+
}
|
|
1487
|
+
@media (max-width: 640px) { .craft-eyebrow { display: none; } }
|
|
1488
|
+
/* The signature: the mode's door scene (craft-mode.js artHtml, the same art the
|
|
1489
|
+
picker draws) framed under the brand, 40px in the closed rail and 64px open.
|
|
1490
|
+
The 110px-tall scene is scaled to the plate's height and widened to fill it,
|
|
1491
|
+
never redrawn: each scene's pieces are placed from its edges or its centre. */
|
|
1492
|
+
.craft-plate {
|
|
1493
|
+
position: relative;
|
|
1494
|
+
height: 40px;
|
|
1495
|
+
margin: 10px 8px 0;
|
|
1496
|
+
border-radius: var(--radius-sm);
|
|
1497
|
+
overflow: hidden;
|
|
1498
|
+
flex-shrink: 0;
|
|
1499
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--craft) 30%, transparent);
|
|
1500
|
+
}
|
|
1501
|
+
.craft-plate .craft-art {
|
|
1502
|
+
position: absolute;
|
|
1503
|
+
left: 0;
|
|
1504
|
+
top: 0;
|
|
1505
|
+
width: calc(100% / .364);
|
|
1506
|
+
height: 110px;
|
|
1507
|
+
transform: scale(.364);
|
|
1508
|
+
transform-origin: 0 0;
|
|
1509
|
+
}
|
|
1510
|
+
.craft-plate .craft-art__city { top: 0; }
|
|
1511
|
+
.app-sidebar:hover .craft-plate,
|
|
1512
|
+
.app-sidebar:focus-within .craft-plate { height: 64px; margin: 10px 16px 0; }
|
|
1513
|
+
.app-sidebar:hover .craft-plate .craft-art,
|
|
1514
|
+
.app-sidebar:focus-within .craft-plate .craft-art { width: calc(100% / .582); transform: scale(.582); }
|
|
1515
|
+
@media (max-width: 768px) {
|
|
1516
|
+
.craft-plate { height: 64px; margin: 10px 16px 0; }
|
|
1517
|
+
.craft-plate .craft-art { width: calc(100% / .582); transform: scale(.582); }
|
|
1518
|
+
}
|
|
1519
|
+
/* The plate follows the theme (owner, 2026-10-03): light shows each scene's day. */
|
|
1520
|
+
html[data-theme="light"] .craft-plate .craft-art--artistry { background: #dfe3ef; }
|
|
1521
|
+
html[data-theme="light"] .craft-plate .craft-art__dot--star { background: #2e3c6e; }
|
|
1522
|
+
html[data-theme="light"] .craft-plate .craft-art__moon { background: #f3c98b; box-shadow: 0 0 0 9px rgba(243, 201, 139, .28); }
|
|
1523
|
+
html[data-theme="light"] .craft-plate .craft-art__ground { background: #c3c9dc; }
|
|
1524
|
+
html[data-theme="light"] .craft-plate .craft-art__house { background: #8d97b8; }
|
|
1525
|
+
html[data-theme="light"] .craft-plate .craft-art--ideating { background: #f6f0dc; }
|
|
1526
|
+
html[data-theme="light"] .craft-plate .craft-art__dot--gold { background: #c9a23a; }
|
|
1527
|
+
html[data-theme="light"] .craft-plate .craft-art__dot--glow { background: #b8891c; box-shadow: 0 0 10px rgba(184, 137, 28, .45); }
|
|
1528
|
+
html[data-theme="light"] .craft-plate .craft-art__dot--proto { box-shadow: inset 0 0 0 1px rgba(122, 90, 12, .6); }
|
|
1529
|
+
html[data-theme="light"] .craft-plate .craft-art__ring { box-shadow: inset 0 0 0 1px rgba(122, 90, 12, .35); }
|
|
1530
|
+
html[data-theme="light"] .craft-plate .craft-art--governing { background: #e9ecf6; }
|
|
1531
|
+
html[data-theme="light"] .craft-plate .craft-art__city { filter: invert(1) hue-rotate(180deg); }
|
|
1532
|
+
/* Ideating's star dust: a few faint fixed specks across the rail itself. */
|
|
1533
|
+
html[data-craft="ideating"] .app-sidebar {
|
|
1534
|
+
background-image:
|
|
1535
|
+
radial-gradient(1px 1px at 14px 40px, rgba(242, 210, 122, .5), transparent),
|
|
1536
|
+
radial-gradient(1px 1px at 46px 130px, rgba(242, 210, 122, .35), transparent),
|
|
1537
|
+
radial-gradient(1.5px 1.5px at 120px 90px, rgba(242, 210, 122, .4), transparent),
|
|
1538
|
+
radial-gradient(1px 1px at 200px 190px, rgba(242, 210, 122, .35), transparent),
|
|
1539
|
+
radial-gradient(1px 1px at 30px 230px, rgba(242, 210, 122, .3), transparent),
|
|
1540
|
+
radial-gradient(1.5px 1.5px at 170px 260px, rgba(242, 210, 122, .3), transparent);
|
|
1541
|
+
background-size: 246px 280px;
|
|
1542
|
+
}
|
|
1543
|
+
html[data-theme="light"][data-craft="ideating"] .app-sidebar {
|
|
1544
|
+
background-image:
|
|
1545
|
+
radial-gradient(1px 1px at 14px 40px, rgba(184, 137, 28, .45), transparent),
|
|
1546
|
+
radial-gradient(1px 1px at 46px 130px, rgba(184, 137, 28, .3), transparent),
|
|
1547
|
+
radial-gradient(1.5px 1.5px at 120px 90px, rgba(184, 137, 28, .35), transparent),
|
|
1548
|
+
radial-gradient(1px 1px at 200px 190px, rgba(184, 137, 28, .3), transparent),
|
|
1549
|
+
radial-gradient(1px 1px at 30px 230px, rgba(184, 137, 28, .25), transparent),
|
|
1550
|
+
radial-gradient(1.5px 1.5px at 170px 260px, rgba(184, 137, 28, .25), transparent);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1434
1553
|
/* THE BACK LINK (task 1004430): at the head of the bar when the page was
|
|
1435
1554
|
reached from inside another hall page. 24px floor on both axes. */
|
|
1436
1555
|
.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-03-
|
|
58
|
-
<script src="/builders/craft-mode.js?v=2026-10-03-
|
|
59
|
-
<script src="/builders/shell.js?v=2026-10-03-
|
|
57
|
+
<script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
|
|
58
|
+
<script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
|
|
59
|
+
<script src="/builders/shell.js?v=2026-10-03-looks"></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-03-
|
|
142
|
-
<script src="/builders/craft-mode.js?v=2026-10-03-
|
|
143
|
-
<script src="/builders/shell.js?v=2026-10-03-
|
|
141
|
+
<script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
|
|
142
|
+
<script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
|
|
143
|
+
<script src="/builders/shell.js?v=2026-10-03-looks"></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-03-
|
|
146
|
-
<script src="/builders/craft-mode.js?v=2026-10-03-
|
|
147
|
-
<script src="/builders/shell.js?v=2026-10-03-
|
|
145
|
+
<script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
|
|
146
|
+
<script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
|
|
147
|
+
<script src="/builders/shell.js?v=2026-10-03-looks"></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-03-
|
|
166
|
-
<script src="/builders/craft-mode.js?v=2026-10-03-
|
|
167
|
-
<script src="/builders/shell.js?v=2026-10-03-
|
|
165
|
+
<script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
|
|
166
|
+
<script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
|
|
167
|
+
<script src="/builders/shell.js?v=2026-10-03-looks"></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-03-
|
|
175
|
-
<script src="/builders/craft-mode.js?v=2026-10-03-
|
|
176
|
-
<script src="/builders/shell.js?v=2026-10-03-
|
|
174
|
+
<script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
|
|
175
|
+
<script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
|
|
176
|
+
<script src="/builders/shell.js?v=2026-10-03-looks"></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. -->
|
|
@@ -27,3 +27,11 @@ Covers: every shelled hall page (`craft-mode.js`, `shell.js`, `style.css`), and
|
|
|
27
27
|
- **Government shows only in Governing**, and outside that group Governing is the shortest list (Blockers, Board Room). Building is every tab except Government and the Board Room; Artistry is Studio, Goals, Your thinking; Ideating is Sky, Your thinking, Goals. The prototype names rooms that do not exist yet (Store, Page reviews, Treasury); a list names only real NAV items, and `tests/craft_mode_nav.mjs` requires every NAV item to sit in some mode.
|
|
28
28
|
- **The mode only ever takes away, and never through `hidden`.** The access gate owns `hidden`; `applyNav()` marks what the mode leaves out with `[data-craft-off]`, which `style.css` hides. So a mode can never show a tab the gate hid, and switching mode just moves the attribute. A group head goes too once nothing under it shows.
|
|
29
29
|
- **Two callers.** `apply()` runs it on every mode change (live, no reload), and `shell.js` `applyAccess` runs it right after its gate loop, so a later `/me` answer (a promotion, a permission) brings a head back with its tabs. Hidden tabs still open by their URL; nothing is enforced (ADR 0016).
|
|
30
|
+
|
|
31
|
+
## Each mode's look (task 1004495)
|
|
32
|
+
|
|
33
|
+
- **Three modes wear a look; Building, Legacy and no mode wear none.** Artistry, Ideating and Governing each set `--craft` (where you are) and `--craft-on` (text on it) on `html[data-craft="<mode>"]`, with a dark twin on `html[data-theme="dark"][data-craft="<mode>"]` that also darkens the ground and tints the glass (Artistry, Ideating). Values and contrast are the direction's, `docs/design/craft-mode-direction.md`. No rule in `style.css` names `building` or `legacy`, and none matches any mode at all; `tests/craft_mode_looks.mjs` pins both, and re-measures every colour pair.
|
|
34
|
+
- **The accent stays the action colour.** `--craft` paints only the switch's current pill, the current tab (tint, icon, label, marker) and the top bar's hairline.
|
|
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
|
+
- **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
|
+
- **Governing's header tallies** ("needs you · moving") are task 1004563, not this look.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.37",
|
|
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.37",
|
|
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.37",
|
|
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
|
@@ -8980,5 +8980,11 @@
|
|
|
8980
8980
|
"id": "1004560",
|
|
8981
8981
|
"text": "The Planning building now has three working desks on its first floors. Priorities lists the open goals in order with a square for each promise; arrows move a goal up or down, and a save bar (with undo) sets the new order, or,"
|
|
8982
8982
|
}
|
|
8983
|
+
],
|
|
8984
|
+
"1.21.37": [
|
|
8985
|
+
{
|
|
8986
|
+
"id": "1004495",
|
|
8987
|
+
"text": "The hall now looks different in each craft mode. In Artistry, Ideating and Governing the mode button, the current sidebar tab and the top edge of the page take the mode's own colour (moonlight or night ink, gold, city blue), t"
|
|
8988
|
+
}
|
|
8983
8989
|
]
|
|
8984
8990
|
}
|
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.37'; // 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');
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// tests/craft_mode_looks.mjs — each craft mode's look (task 1004495, goal 1000095
|
|
2
|
+
// WA6; direction docs/design/craft-mode-direction.md).
|
|
3
|
+
//
|
|
4
|
+
// Artistry, Ideating and Governing each wear a look: --craft / --craft-on tokens
|
|
5
|
+
// (light + dark twins) in style.css, the header eyebrow beside the page title and
|
|
6
|
+
// the door's scene framed under the brand. Building, Legacy and no mode wear
|
|
7
|
+
// NOTHING — the hall as it always was. These cases run the REAL craft-mode.js over
|
|
8
|
+
// a small DOM and read the REAL style.css, so a look that leaks into Building, a
|
|
9
|
+
// plate that is a second copy of the art, or a colour that loses its contrast reds.
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import vm from 'node:vm';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
const HALL = path.join(ROOT, 'modules/hall-ui/public');
|
|
19
|
+
const CRAFT = fs.readFileSync(path.join(HALL, 'craft-mode.js'), 'utf8');
|
|
20
|
+
const CSS = fs.readFileSync(path.join(HALL, 'style.css'), 'utf8');
|
|
21
|
+
|
|
22
|
+
const LOOKED = ['artistry', 'ideating', 'governing'];
|
|
23
|
+
const WORDS = { artistry: 'the studio · night', ideating: 'the sky', governing: 'the city' };
|
|
24
|
+
|
|
25
|
+
// A small DOM: enough tree for insertBefore / removeChild / nextSibling to be real.
|
|
26
|
+
function node(tag, id, cls) {
|
|
27
|
+
const attrs = {};
|
|
28
|
+
const el = {
|
|
29
|
+
tagName: tag, id: id || '', className: cls || '', dataset: {}, children: [], parentNode: null,
|
|
30
|
+
textContent: '', innerHTML: '',
|
|
31
|
+
setAttribute(k, v) { attrs[k] = String(v); }, getAttribute(k) { return k in attrs ? attrs[k] : null; },
|
|
32
|
+
hasAttribute(k) { return k in attrs; }, removeAttribute(k) { delete attrs[k]; },
|
|
33
|
+
get nextSibling() { const sib = this.parentNode && this.parentNode.children; return sib ? sib[sib.indexOf(this) + 1] || null : null; },
|
|
34
|
+
appendChild(c) { c.parentNode = this; this.children.push(c); return c; },
|
|
35
|
+
insertBefore(c, ref) {
|
|
36
|
+
c.parentNode = this;
|
|
37
|
+
const at = ref ? this.children.indexOf(ref) : -1;
|
|
38
|
+
if (at === -1) this.children.push(c); else this.children.splice(at, 0, c);
|
|
39
|
+
return c;
|
|
40
|
+
},
|
|
41
|
+
removeChild(c) { this.children.splice(this.children.indexOf(c), 1); c.parentNode = null; return c; },
|
|
42
|
+
querySelector(sel) { return all(this).find((n) => sel === '.' + n.className) || null; },
|
|
43
|
+
querySelectorAll() { return []; },
|
|
44
|
+
addEventListener() {}, classList: { toggle() {} },
|
|
45
|
+
};
|
|
46
|
+
return el;
|
|
47
|
+
}
|
|
48
|
+
function all(root) { return root.children.flatMap((c) => [c, ...all(c)]); }
|
|
49
|
+
|
|
50
|
+
function boot(session = {}) {
|
|
51
|
+
const sidebar = node('NAV', 'app-sidebar');
|
|
52
|
+
const brand = sidebar.appendChild(node('DIV', '', 'app-sidebar__brand'));
|
|
53
|
+
sidebar.appendChild(node('NAV', '', 'app-sidebar__nav'));
|
|
54
|
+
const topbar = node('DIV', 'app-topbar');
|
|
55
|
+
topbar.appendChild(node('BUTTON', 'topbar-menu'));
|
|
56
|
+
const title = topbar.appendChild(node('H2', 'topbar-title'));
|
|
57
|
+
topbar.appendChild(node('DIV', '', 'topbar-side'));
|
|
58
|
+
const root = node('HTML');
|
|
59
|
+
const body = node('BODY');
|
|
60
|
+
const document = {
|
|
61
|
+
documentElement: root, body, activeElement: null,
|
|
62
|
+
getElementById: (id) => [sidebar, topbar, ...all(sidebar), ...all(topbar)].find((n) => n.id === id) || null,
|
|
63
|
+
querySelectorAll: () => [],
|
|
64
|
+
createElement: (t) => node(t.toUpperCase()),
|
|
65
|
+
addEventListener() {}, removeEventListener() {}, dispatchEvent() { return true; },
|
|
66
|
+
};
|
|
67
|
+
const mk = (obj) => ({ getItem: (k) => (k in obj ? obj[k] : null), setItem: (k, v) => { obj[k] = String(v); }, removeItem: (k) => { delete obj[k]; } });
|
|
68
|
+
const window = { localStorage: mk({}), sessionStorage: mk(session), matchMedia: () => ({ matches: false }) };
|
|
69
|
+
const sandbox = { window, document, CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } } };
|
|
70
|
+
vm.createContext(sandbox);
|
|
71
|
+
vm.runInContext(CRAFT, sandbox, { filename: 'craft-mode.js' });
|
|
72
|
+
const craft = window.OTBCraftMode;
|
|
73
|
+
craft.mount();
|
|
74
|
+
return {
|
|
75
|
+
craft, root, sidebar, brand, topbar, title,
|
|
76
|
+
plates: () => all(sidebar).filter((n) => n.id === 'craft-plate'),
|
|
77
|
+
marks: () => all(topbar).filter((n) => n.id === 'craft-eyebrow'),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
test('each looked mode frames its own door scene under the brand, and names itself beside the title', () => {
|
|
82
|
+
const p = boot();
|
|
83
|
+
for (const m of LOOKED) {
|
|
84
|
+
p.craft.choose(m);
|
|
85
|
+
assert.equal(p.plates().length, 1, `${m}: exactly one plate`);
|
|
86
|
+
const plate = p.plates()[0];
|
|
87
|
+
assert.equal(p.brand.nextSibling, plate, `${m}: the plate sits right under the brand`);
|
|
88
|
+
assert.equal(plate.getAttribute('data-plate'), m);
|
|
89
|
+
assert.equal(plate.getAttribute('aria-hidden'), 'true', 'the plate is decoration');
|
|
90
|
+
assert.equal(plate.innerHTML, p.craft.artHtml(m), `${m}: the plate IS the picker's door art, not a second copy`);
|
|
91
|
+
assert.equal(p.marks().length, 1, `${m}: exactly one eyebrow`);
|
|
92
|
+
assert.equal(p.title.nextSibling, p.marks()[0], `${m}: the eyebrow sits right after the page title`);
|
|
93
|
+
assert.equal(p.marks()[0].textContent, WORDS[m], `${m}: in the prototype's own words`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('BUILDING, LEGACY AND NO MODE WEAR NO LOOK: no plate, no eyebrow, and switching to them takes the look away', () => {
|
|
98
|
+
const none = boot();
|
|
99
|
+
assert.equal(none.root.dataset.craft, undefined);
|
|
100
|
+
assert.equal(none.plates().length + none.marks().length, 0, 'no mode draws nothing');
|
|
101
|
+
for (const m of ['building', 'legacy']) {
|
|
102
|
+
const p = boot({ 'hall-craft': m });
|
|
103
|
+
assert.equal(p.plates().length + p.marks().length, 0, `${m} at boot draws nothing`);
|
|
104
|
+
p.craft.choose('governing');
|
|
105
|
+
assert.equal(p.plates().length, 1);
|
|
106
|
+
p.craft.choose(m);
|
|
107
|
+
assert.equal(p.plates().length + p.marks().length, 0, `switching to ${m} removes the plate and the eyebrow`);
|
|
108
|
+
}
|
|
109
|
+
assert.deepEqual(Object.keys(none.craft.LOOKS).sort(), [...LOOKED].sort(), 'only the three crafts have a look');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('the art is drawn once: craft-mode.js has one artHtml and exports it', () => {
|
|
113
|
+
assert.equal(CRAFT.match(/function artHtml\(/g).length, 1);
|
|
114
|
+
assert.match(CRAFT, /artHtml: artHtml,/);
|
|
115
|
+
assert.match(CRAFT, /plate\.innerHTML = artHtml\(mode\);/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Every selector in style.css that names a craft mode.
|
|
119
|
+
const CRAFT_SELECTORS = [...CSS.matchAll(/data-craft="([a-z]+)"/g)].map((m) => m[1]);
|
|
120
|
+
|
|
121
|
+
test('style.css names only the three looked modes — Building and Legacy have no rule at all', () => {
|
|
122
|
+
assert.ok(CRAFT_SELECTORS.length >= 12, `found ${CRAFT_SELECTORS.length} craft selectors`);
|
|
123
|
+
for (const m of CRAFT_SELECTORS) assert.ok(LOOKED.includes(m), `a rule names data-craft="${m}"`);
|
|
124
|
+
assert.doesNotMatch(CSS, /data-craft="(building|legacy)"/);
|
|
125
|
+
assert.doesNotMatch(CSS, /data-craft\]/, 'no rule matches ANY mode, which would dress Building too');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// The token values, light and dark, read from the stylesheet itself.
|
|
129
|
+
function tokens(selector) {
|
|
130
|
+
const at = CSS.indexOf(selector + ' {');
|
|
131
|
+
assert.ok(at !== -1, `style.css has a ${selector} block`);
|
|
132
|
+
const block = CSS.slice(at, CSS.indexOf('}', at));
|
|
133
|
+
const get = (name) => (block.match(new RegExp(name + ':\\s*(#[0-9a-f]{6})')) || [])[1];
|
|
134
|
+
return { craft: get('--craft'), on: get('--craft-on'), bg: get('--bg') };
|
|
135
|
+
}
|
|
136
|
+
function lum(hex) {
|
|
137
|
+
const c = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255)
|
|
138
|
+
.map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
|
|
139
|
+
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
|
|
140
|
+
}
|
|
141
|
+
function contrast(a, b) { const [x, y] = [lum(a), lum(b)].sort((p, q) => q - p); return (x + 0.05) / (y + 0.05); }
|
|
142
|
+
|
|
143
|
+
test('every mode colour has a light and a dark twin that keep their contrast', () => {
|
|
144
|
+
const LIGHT_BG = '#edecee', DARK_BG = '#000000';
|
|
145
|
+
for (const m of LOOKED) {
|
|
146
|
+
const light = tokens(`html[data-craft="${m}"]`);
|
|
147
|
+
const dark = tokens(`html[data-theme="dark"][data-craft="${m}"]`);
|
|
148
|
+
for (const [k, t] of [['light', light], ['dark', dark]]) {
|
|
149
|
+
assert.ok(t.craft && t.on, `${m} ${k} sets --craft and --craft-on`);
|
|
150
|
+
}
|
|
151
|
+
assert.ok(contrast(light.craft, LIGHT_BG) >= 5.4, `${m} light --craft on paper: ${contrast(light.craft, LIGHT_BG).toFixed(2)}`);
|
|
152
|
+
assert.ok(contrast(dark.craft, dark.bg || DARK_BG) >= 5.4, `${m} dark --craft on its ground`);
|
|
153
|
+
assert.ok(contrast(light.on, light.craft) >= 5.8, `${m} light pill text: ${contrast(light.on, light.craft).toFixed(2)}`);
|
|
154
|
+
assert.ok(contrast(dark.on, dark.craft) >= 5.8, `${m} dark pill text`);
|
|
155
|
+
assert.equal(light.bg, undefined, `${m}: light keeps the hall's paper ground`);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('the shared treatment, the eyebrow and the plate are written as the direction says', () => {
|
|
160
|
+
const looked = 'html:is([data-craft="artistry"], [data-craft="ideating"], [data-craft="governing"])';
|
|
161
|
+
for (const target of ['.craft-switch__opt[aria-pressed="true"]', '.nav-item--active', '.nav-item--active::before', '.app-topbar']) {
|
|
162
|
+
assert.ok(CSS.includes(`${looked} ${target}`), `the looked modes restyle ${target}`);
|
|
163
|
+
}
|
|
164
|
+
assert.match(CSS, /@media \(max-width: 640px\) \{ \.craft-eyebrow \{ display: none; \} \}/, 'the eyebrow is hidden on a phone');
|
|
165
|
+
assert.match(CSS, /\.craft-plate \{[^}]*height: 40px;/, '40px in the closed rail');
|
|
166
|
+
assert.match(CSS, /\.app-sidebar:focus-within \.craft-plate \{ height: 64px;/, '64px when the rail opens');
|
|
167
|
+
assert.match(CSS, /html\[data-theme="light"\] \.craft-plate \.craft-art__city \{ filter: invert\(1\) hue-rotate\(180deg\); \}/,
|
|
168
|
+
'the light city is the dark one inverted, not a second drawing');
|
|
169
|
+
assert.equal(CSS.match(/prefers-reduced-motion/g).length, 1, 'no second reduced-motion block');
|
|
170
|
+
});
|
package/tests/craft_mode_nav.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Each craft mode shows only the tabs its craft needs; Legacy mode and no mode
|
|
5
5
|
// show every tab, exactly as before. The owner's rule (2026-10-01): some tabs
|
|
6
6
|
// stay in EVERY mode, Builders first among them, written down as ONE shared list
|
|
7
|
-
// that each mode's list adds to. These cases boot the REAL shell-trail.js +
|
|
7
|
+
// that each mode's list adds to; on 2026-10-03 Collab and Profile joined it. These cases boot the REAL shell-trail.js +
|
|
8
8
|
// craft-mode.js + shell.js and read the sidebar the shell actually rendered, so a
|
|
9
9
|
// tab id that drifts from NAV, or a gate pass that runs after the mode and
|
|
10
10
|
// re-shows a tab, reds here.
|
|
@@ -126,13 +126,15 @@ function boot({ session = {}, perms = { 'board.vote.cast': true } } = {}) {
|
|
|
126
126
|
|
|
127
127
|
const CRAFTS = ['building', 'artistry', 'ideating', 'governing'];
|
|
128
128
|
|
|
129
|
-
test('THE ALWAYS-PRESENT LIST: one shared list, Builders first, inside every mode', () => {
|
|
129
|
+
test('THE ALWAYS-PRESENT LIST: one shared list, Builders first, then Collab and Profile, inside every mode', () => {
|
|
130
130
|
const { craft } = boot();
|
|
131
|
-
assert.
|
|
131
|
+
assert.deepEqual([...craft.ALWAYS_TABS], ['roster', 'collab', 'profile'],
|
|
132
|
+
'Builders (NAV id roster) heads the list (owner, 2026-10-01); Collab and Profile join it (owner, 2026-10-03)');
|
|
132
133
|
for (const m of CRAFTS) {
|
|
133
134
|
const tabs = craft.tabsFor(m);
|
|
134
135
|
assert.ok(Array.isArray(tabs), `${m} has a named tab list`);
|
|
135
136
|
for (const id of craft.ALWAYS_TABS) assert.ok(tabs.includes(id), `${m} keeps ${id}`);
|
|
137
|
+
assert.equal(new Set(tabs).size, tabs.length, `${m} names no tab twice`);
|
|
136
138
|
}
|
|
137
139
|
assert.match(CRAFT, /return own \? ALWAYS_TABS\.concat\(own\) : null;/, 'each list is ALWAYS_TABS plus its own, not a copy of it');
|
|
138
140
|
});
|
|
@@ -173,7 +175,7 @@ test('each mode shows its own tabs, and switching updates the nav live', () => {
|
|
|
173
175
|
p.craft.choose(m);
|
|
174
176
|
assert.equal(p.root.dataset.craft, m);
|
|
175
177
|
assert.deepEqual(new Set(p.showing()), new Set(p.craft.tabsFor(m)), `${m} shows exactly its list`);
|
|
176
|
-
assert.ok(p.showing().includes(
|
|
178
|
+
for (const id of ['roster', 'collab', 'profile']) assert.ok(p.showing().includes(id), `${id} stays in ${m}`);
|
|
177
179
|
assert.equal(p.headShowing('government'), m === 'governing', `the Government head shows only where its tabs do (${m})`);
|
|
178
180
|
}
|
|
179
181
|
p.craft.choose('legacy');
|
|
@@ -185,7 +187,7 @@ test('a mode never shows a tab the gate hid, and a later gate pass keeps the mod
|
|
|
185
187
|
p.signIn('thetes');
|
|
186
188
|
assert.ok(!p.showing().includes('watch'), 'a thetes in Governing does not get the metic-floored Watch tab');
|
|
187
189
|
assert.ok(!p.headShowing('government'), 'nor its group head');
|
|
188
|
-
assert.deepEqual(p.showing(), ['blockers', 'board-room', 'roster'], 'what a thetes board member in Governing sees');
|
|
190
|
+
assert.deepEqual(p.showing(), ['blockers', 'board-room', 'profile', 'collab', 'roster'], 'what a thetes board member in Governing sees');
|
|
189
191
|
p.signIn('archon');
|
|
190
192
|
assert.ok(p.showing().includes('watch'), 'promoted, the gate lets it through and the mode keeps it');
|
|
191
193
|
assert.ok(p.headShowing('government'), 'and its group head comes back with it, which only a mode pass after the gate can know');
|
|
@@ -635,7 +635,7 @@ try {
|
|
|
635
635
|
assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
|
|
636
636
|
assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
|
|
637
637
|
assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
|
|
638
|
-
assert.match(HTML, /shell\.js\?v=2026-10-03-
|
|
638
|
+
assert.match(HTML, /shell\.js\?v=2026-10-03-looks/);
|
|
639
639
|
assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
|
|
640
640
|
for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
|
|
641
641
|
assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
|