@bongos/core 1.21.36 → 1.21.38
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 +90 -70
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +77 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +8 -8
- package/docs/copy-registry.json +8 -8
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +234 -234
- package/modules/government/board-withdraw.js +38 -0
- package/modules/government/board.js +1 -0
- package/modules/government/db.js +20 -0
- package/modules/government/migrations/government_026_board_outcome_withdrawn.sql +29 -0
- package/modules/government/routes/government.js +26 -0
- 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-card.js +16 -0
- package/modules/hall-ui/public/street-desk.js +2 -0
- package/modules/hall-ui/public/street.html +3 -3
- package/modules/hall-ui/public/street.js +1 -1
- 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 +12 -0
- package/src/bongos/route-rank-check.js +4 -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/government_board_withdraw.mjs +144 -0
- package/tests/government_seed.mjs +1 -0
- package/tests/hall_street_desk.mjs +37 -0
- package/tests/hall_tweak_editor.mjs +1 -1
|
@@ -111,9 +111,9 @@
|
|
|
111
111
|
|
|
112
112
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
113
113
|
chrome exists before the page script runs. -->
|
|
114
|
-
<script src="/builders/shell-trail.js?v=2026-10-03-
|
|
115
|
-
<script src="/builders/craft-mode.js?v=2026-10-03-
|
|
116
|
-
<script src="/builders/shell.js?v=2026-10-03-
|
|
114
|
+
<script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
|
|
115
|
+
<script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
|
|
116
|
+
<script src="/builders/shell.js?v=2026-10-03-looks"></script>
|
|
117
117
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
118
118
|
<script src="/builders/palette.js" defer></script>
|
|
119
119
|
<!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
|
|
@@ -435,9 +435,9 @@
|
|
|
435
435
|
|
|
436
436
|
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
437
437
|
chrome exists before the page scripts run. -->
|
|
438
|
-
<script src="/builders/shell-trail.js?v=2026-10-03-
|
|
439
|
-
<script src="/builders/craft-mode.js?v=2026-10-03-
|
|
440
|
-
<script src="/builders/shell.js?v=2026-10-03-
|
|
438
|
+
<script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
|
|
439
|
+
<script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
|
|
440
|
+
<script src="/builders/shell.js?v=2026-10-03-looks"></script>
|
|
441
441
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
442
442
|
<script src="/builders/palette.js" defer></script>
|
|
443
443
|
<!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
|
|
@@ -325,6 +325,7 @@
|
|
|
325
325
|
if (item.closed_at) {
|
|
326
326
|
cv.readOnly = true;
|
|
327
327
|
if (item.outcome === 'passed') cv.result = 'Passed ' + b.forN + '–' + b.againstN + '. In force from ' + str(item.closed_at).slice(0, 10) + '.';
|
|
328
|
+
else if (item.outcome === 'withdrawn') cv.result = b.isAuthor ? 'You withdrew it before anyone voted.' : 'Withdrawn by ' + (proposer || 'its author') + ' before anyone voted.';
|
|
328
329
|
else if (b.isAuthor && cv.counsel && cv.counsel.revise) { cv.result = 'It came back to you.'; cv.readOnly = false; cv.reviseForm = { text: words, label: kind === 'matter' ? 'the matter, in your words' : 'why, in your words' }; }
|
|
329
330
|
else cv.result = 'It went back to ' + (proposer || 'its author') + ' to revise.';
|
|
330
331
|
return cv;
|
|
@@ -346,6 +347,16 @@
|
|
|
346
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.';
|
|
347
348
|
return cv;
|
|
348
349
|
}
|
|
350
|
+
// task 1004564: the proposer may withdraw their own sitting until anyone has
|
|
351
|
+
// voted (POST .../withdraw; the server holds the same two walls). If they may
|
|
352
|
+
// also vote on it, withdraw takes the second place: an objection to your own
|
|
353
|
+
// proposal is a withdraw said the long way.
|
|
354
|
+
var withdrawable = b.isAuthor && !((item.votes || []).length);
|
|
355
|
+
if (withdrawable) {
|
|
356
|
+
cv.actions = b.youOpen ? [act('for', 'vote for', 'primary'), act('withdraw', 'withdraw', 'quiet')] : [act('withdraw', 'withdraw', 'quiet')];
|
|
357
|
+
cv.result = 'Yours. No one has voted yet, so you can still withdraw it.';
|
|
358
|
+
return cv;
|
|
359
|
+
}
|
|
349
360
|
if (b.youOpen) {
|
|
350
361
|
cv.actions = [act('for', 'vote for', 'primary'), act('object', 'object', 'quiet')];
|
|
351
362
|
cv.textActions = [act('abstain', 'abstain', 'text')];
|
|
@@ -575,6 +586,7 @@
|
|
|
575
586
|
if (s.itemId != null) {
|
|
576
587
|
if (key === 'for') return post('/government/board/items/' + enc(s.itemId) + '/votes', { direction: 'yes' });
|
|
577
588
|
if (key === 'object') return post('/government/board/items/' + enc(s.itemId) + '/votes', { direction: 'no', reason_md: str(inp.reason).trim(), section_key: str(inp.section) });
|
|
589
|
+
if (key === 'withdraw') return post('/government/board/items/' + enc(s.itemId) + '/withdraw', {});
|
|
578
590
|
if (key === 'revise') {
|
|
579
591
|
var text = str(inp.text).trim();
|
|
580
592
|
if (s.kind === 'amendment') return post('/government/board/items', { subject_type: 'constitutional_amendment', proposal: s.proposed, rationale_md: text });
|
|
@@ -629,6 +641,7 @@
|
|
|
629
641
|
return { toast: r.already_voted ? 'Your vote was already on the record.' : 'Your yes is on the record.', building: b, kind: 'light', close: false };
|
|
630
642
|
}
|
|
631
643
|
if (key === 'object') return { toast: r.item_closed ? 'Your objection is on the record. It goes back to its author to revise.' : 'Your objection is on the record.', building: b, kind: 'settle', close: false };
|
|
644
|
+
if (key === 'withdraw') return { toast: 'Withdrawn. The sitting is closed and nothing was decided.', building: b, kind: 'settle', close: true };
|
|
632
645
|
if (key === 'abstain') return { toast: 'Noted. Nothing goes on the record, and it still waits for your vote until it closes.', building: null, kind: null, close: true };
|
|
633
646
|
if (key === 'revise') return { toast: 'Revised. It sits again on the new wording.', building: b, kind: 'light', close: true };
|
|
634
647
|
if (key === 'hold') return { toast: 'Held. It waits for you.', building: null, kind: null, close: true };
|
|
@@ -678,6 +691,9 @@
|
|
|
678
691
|
backup_already_running: 'A backup is already running.',
|
|
679
692
|
intent_conflict: 'Another core run is going. Try again when it settles.',
|
|
680
693
|
criterion_needs_recording: 'This one needs a recording. Sign it off on its goal page.',
|
|
694
|
+
not_proposer: 'Only the person who put it up may withdraw it.',
|
|
695
|
+
votes_cast: 'Someone has voted on it, so it is the sitting\'s to decide now.',
|
|
696
|
+
item_changed: 'Someone voted just now, so it stays with the sitting.',
|
|
681
697
|
};
|
|
682
698
|
function refusal(status, body) {
|
|
683
699
|
var e = body && body.error ? body.error : {};
|
|
@@ -119,6 +119,8 @@
|
|
|
119
119
|
decide: 'decide', ratify: 'ratify', ratify_goal: 'make goal', up: 'move up', down: 'move down',
|
|
120
120
|
// task 1004561: a builder's monthly limit, and a sign-off undone
|
|
121
121
|
budget: 'set limit', unsign: 'undo',
|
|
122
|
+
// task 1004564: the proposer takes back their own sitting before any vote
|
|
123
|
+
withdraw: 'withdraw',
|
|
122
124
|
});
|
|
123
125
|
// What a row's chips ask, per act (a reason or an option is one tap, never text).
|
|
124
126
|
const ASKS = Object.freeze({ budget: 'monthly limit', unsign: 'why', decide: 'pick one', ratify_goal: 'which version', check: 'check', take_core: 'move to', rot: 'stale after', module_off: 'sure', invite: 'their GitHub name', object: 'why', force: 'why', switch_off: 'why', pause: 'why', deactivate: 'why', signoff: 'confirm', allow: 'which goal', tier: 'which tier', give: 'to whom', take: 'from whom' });
|
|
@@ -208,9 +208,9 @@
|
|
|
208
208
|
</div>
|
|
209
209
|
</div>
|
|
210
210
|
|
|
211
|
-
<script src="/builders/shell-trail.js?v=2026-10-03-
|
|
212
|
-
<script src="/builders/craft-mode.js?v=2026-10-03-
|
|
213
|
-
<script src="/builders/shell.js?v=2026-10-03-
|
|
211
|
+
<script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
|
|
212
|
+
<script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
|
|
213
|
+
<script src="/builders/shell.js?v=2026-10-03-looks"></script>
|
|
214
214
|
<script src="/builders/palette.js" defer></script>
|
|
215
215
|
<script src="/builders/city-draw.js?v=2026-10-02-street1"></script>
|
|
216
216
|
<script src="/builders/street-draw.js?v=2026-10-02-street1"></script>
|
|
@@ -751,7 +751,7 @@
|
|
|
751
751
|
pause: 'paused', resume: 'resumed', tier: 'new tier', deactivate: 'deactivated', give: 'gave', take: 'took back',
|
|
752
752
|
check: 'checked', rot: 'set', module_on: 'turned on', module_off: 'turned off', backup: 'backed up', invite: 'invited',
|
|
753
753
|
decide: 'decided', ratify: 'ratified', ratify_goal: 'made a goal of',
|
|
754
|
-
budget: 'set the limit of', unsign: 'undid',
|
|
754
|
+
budget: 'set the limit of', unsign: 'undid', withdraw: 'withdrew',
|
|
755
755
|
};
|
|
756
756
|
// One act from a row: checked, sent through the card's own request (the
|
|
757
757
|
// server's permission checks decide), then the row clears, the floor counts
|
|
@@ -176,9 +176,9 @@
|
|
|
176
176
|
|
|
177
177
|
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
178
178
|
|
|
179
|
-
<script src="/builders/shell-trail.js?v=2026-10-03-
|
|
180
|
-
<script src="/builders/craft-mode.js?v=2026-10-03-
|
|
181
|
-
<script src="/builders/shell.js?v=2026-10-03-
|
|
179
|
+
<script src="/builders/shell-trail.js?v=2026-10-03-looks"></script>
|
|
180
|
+
<script src="/builders/craft-mode.js?v=2026-10-03-looks"></script>
|
|
181
|
+
<script src="/builders/shell.js?v=2026-10-03-looks"></script>
|
|
182
182
|
<script src="/builders/palette.js" defer></script>
|
|
183
183
|
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
184
184
|
<!-- The kit, for its ONE renderer: the full-size ship-time visual figure
|
|
@@ -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.38",
|
|
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.38",
|
|
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.38",
|
|
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,17 @@
|
|
|
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
|
+
}
|
|
8989
|
+
],
|
|
8990
|
+
"1.21.38": [
|
|
8991
|
+
{
|
|
8992
|
+
"id": "1004564",
|
|
8993
|
+
"text": "If you put something up for a vote and nobody has voted yet, you can now withdraw it with one tap on your desk. Once anyone votes, it stays with the vote. A small database change makes \"withdrawn\" a valid way for a sitting to"
|
|
8994
|
+
}
|
|
8983
8995
|
]
|
|
8984
8996
|
}
|
|
@@ -292,6 +292,10 @@ const GOVERNMENT_PINS = {
|
|
|
292
292
|
// writes the table; neither route does.
|
|
293
293
|
'POST /government/board/decision-rules': 'perm:board.item.open',
|
|
294
294
|
'POST /government/board/matters': 'perm:board.item.open',
|
|
295
|
+
// task 1004564: a proposer withdraws their own open sitting before any vote —
|
|
296
|
+
// the proposer's half of putting one up, so the same atom. The proposer-only
|
|
297
|
+
// and no-votes walls are in withdrawItem (board-withdraw.js) and its write.
|
|
298
|
+
'POST /government/board/items/:itemId/withdraw': 'perm:board.item.open',
|
|
295
299
|
};
|
|
296
300
|
|
|
297
301
|
// Module-scoped privileged-rank pins: a privileged route that lives in a FEATURE
|
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.38'; // 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
|
+
});
|