@bongos/core 1.20.59 → 1.20.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.bongos-core.json +136 -86
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +10 -0
  4. package/clients/bongos-client/index.cjs +10 -0
  5. package/clients/bongos-client/index.d.ts +13 -0
  6. package/clients/bongos-client/index.mjs +10 -0
  7. package/docs/adr/0358-a-hall-wears-a-look-and-its-night-takes-a-tint.md +52 -0
  8. package/docs/adr/README.md +1 -0
  9. package/docs/api/openapi.json +308 -3
  10. package/docs/api-reference.md +7 -2
  11. package/docs/copy-inventory.md +338 -329
  12. package/docs/copy-registry.json +515 -430
  13. package/docs/module-api-changelog.md +2 -0
  14. package/docs/page-inventory.json +4 -1
  15. package/docs/page-readings.json +757 -706
  16. package/modules/hall-ui/public/atlas.html +1 -1
  17. package/modules/hall-ui/public/blockers.html +1 -1
  18. package/modules/hall-ui/public/board-room.html +1 -1
  19. package/modules/hall-ui/public/collab.html +1 -1
  20. package/modules/hall-ui/public/commands.html +1 -1
  21. package/modules/hall-ui/public/copy-desk.html +1 -1
  22. package/modules/hall-ui/public/deploy.html +1 -1
  23. package/modules/hall-ui/public/diagrams.html +1 -1
  24. package/modules/hall-ui/public/drachmae.html +1 -1
  25. package/modules/hall-ui/public/fleet.html +1 -1
  26. package/modules/hall-ui/public/gate.html +1 -1
  27. package/modules/hall-ui/public/goal-map.html +1 -1
  28. package/modules/hall-ui/public/goals.html +1 -1
  29. package/modules/hall-ui/public/government.html +1 -1
  30. package/modules/hall-ui/public/idea.html +1 -1
  31. package/modules/hall-ui/public/ideas.html +1 -1
  32. package/modules/hall-ui/public/index.html +1 -1
  33. package/modules/hall-ui/public/modules.html +1 -1
  34. package/modules/hall-ui/public/primer.html +1 -1
  35. package/modules/hall-ui/public/profile.html +1 -1
  36. package/modules/hall-ui/public/project-settings.html +1 -1
  37. package/modules/hall-ui/public/ranks.html +1 -1
  38. package/modules/hall-ui/public/roadmap.html +1 -1
  39. package/modules/hall-ui/public/roster.html +1 -1
  40. package/modules/hall-ui/public/sessions.html +1 -1
  41. package/modules/hall-ui/public/settings.html +1 -1
  42. package/modules/hall-ui/public/shell.js +5 -1
  43. package/modules/hall-ui/public/studio.html +1 -1
  44. package/modules/hall-ui/public/style.css +8 -0
  45. package/modules/hall-ui/public/task.html +1 -1
  46. package/modules/hall-ui/public/thinking.html +1 -1
  47. package/modules/hall-ui/public/tweak-editor.html +1 -1
  48. package/modules/hall-ui/public/watch.html +1 -1
  49. package/modules/hall-ui/public/work.html +1 -1
  50. package/modules/provisioning/look.js +169 -0
  51. package/modules/provisioning/migrations/provisioning_034_project_logo.sql +35 -0
  52. package/modules/provisioning/module.json +2 -1
  53. package/modules/provisioning/provisioning.js +4 -3
  54. package/modules/provisioning/routes/look.js +204 -0
  55. package/modules/provisioning/routes/provisioning.js +5 -59
  56. package/modules/provisioning/settings-push.js +76 -0
  57. package/modules/public-landing/public/assets/cosmos.css +1 -1
  58. package/modules/public-landing/public/projects.html +499 -28
  59. package/modules/public-landing/public/projects.probes.json +2 -2
  60. package/modules/public-landing/public/projects.states.json +6 -3
  61. package/modules/ui-design/kit/serve.js +50 -0
  62. package/package-lock.json +2 -2
  63. package/package.json +1 -1
  64. package/release-notes.json +6 -0
  65. package/scripts/gds/fitness.js +4 -0
  66. package/src/bongos/routes.js +4 -1
  67. package/src/bongos/serve-internal.js +6 -0
  68. package/src/branding.js +26 -1
  69. package/src/hall-look.js +99 -0
  70. package/src/module-api.js +1 -1
  71. package/src/night-tint.js +26 -0
  72. package/tests/currency_label.mjs +7 -4
  73. package/tests/hall_tokens.mjs +10 -1
  74. package/tests/hall_tweak_editor.mjs +1 -1
  75. package/tests/look_night_tint.mjs +169 -0
  76. package/tests/nav_permission_atoms.mjs +1 -1
  77. package/tests/projects_hub.mjs +5 -4
  78. package/tests/projects_hub_app_status.mjs +1 -1
  79. package/tests/projects_hub_app_step.mjs +8 -7
  80. package/tests/projects_hub_look.mjs +255 -0
  81. package/tests/projects_hub_module_picker.mjs +2 -2
  82. package/tests/provision_settings_apply.mjs +12 -4
  83. package/tests/provisioning_look.mjs +376 -0
  84. package/tests/provisioning_settings.mjs +10 -7
  85. package/tests/provisioning_settings_apply.mjs +4 -4
  86. package/tests/provisioning_settings_env.mjs +4 -0
  87. package/tests/wizard_draft_resume.mjs +2 -2
  88. package/tests/wizard_front_door.mjs +2 -2
  89. package/tests/wizard_intent_resume.mjs +16 -15
@@ -159,7 +159,7 @@
159
159
  "steps": [["wait",900],["click","[data-disc-ask]"],["wait",200],["fill","#mDiscName","old-lantern"],["wait",100],["click","[data-disc-yes]"],["wait",900]],
160
160
  "expect": {"text":[["#mDisc .addr","This project is being disconnected."],["#mName","old-lantern"]],"hidden":["#mDiscName","[data-disc-ask]"]} },
161
161
  { "name": "the wizard driver (task 1003346): name, type, about, home (fresh; the stub grants repo access, so the new repository is named from the project), address (the handle's verdict), the review as a ledger, then the filing — the done panel with the standup rail and the human steps", "auth": true, "width": 1440, "url": "/projects?view=new",
162
- "steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500]],
162
+ "steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#panel-look button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500]],
163
163
  "expect": {"visible":["#panel-done","#liveProgress .prog","#liveProgress .prog .stages li.now","#humanSteps li"],"hidden":["#panel-review","#panel-name"],"text":[["#done-h","The drums are rolling"]],"attr":[["#panel-done","class","panel on"]]} },
164
164
  { "name": "the front door (task 1004417): Proceed is refused until the terms are ticked, the fork's demo door says the demo is not open yet, and Begin planet creation goes on to the name step with the rail back", "auth": true, "width": 1440, "url": "/projects?view=new",
165
165
  "steps": [["wait",600],["click","#panel-terms button[type=submit]"],["wait",300],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",200],["click","#panel-fork button[type=submit]"],["wait",400]],
@@ -168,7 +168,7 @@
168
168
  "steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400]],
169
169
  "expect": {"visible":["#panel-fork",".doorPill","#wizDemo","#panel-fork button[type=submit]"],"hidden":["#view-new .steps"]} },
170
170
  { "name": "the wizard driver at 390: the rail folds to two rows of three and the walk still reaches the review", "auth": true, "width": 390, "url": "/projects?view=new",
171
- "steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400]],
171
+ "steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#panel-look button[type=submit]"],["wait",400]],
172
172
  "expect": {"visible":["#panel-review","#reviewList li","#doCreate"],"hidden":["#panel-address"],"text":[["#rev-h","Ready to roll"]]} }
173
173
  ]
174
174
  }
@@ -22,6 +22,7 @@
22
22
  "in-manage-address": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mAddress", "#mAddress h2", "#mDom .plain", "#mDomName", "#mDom [data-m-attach]", "#mDomWhat summary"], "count": [["#mDom .attach input[type=text]", 1], ["#mDom .attach button", 1]] } },
23
23
  "in-manage-planet": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mPlanetPick", "#mPick .pick .pickPreview .worldOrb .disc", "#mPick [data-pick-save]", "#mPick [data-pick-reset]", "#mPickWhat summary"], "count": [["#mPick .pickCells[role=\"radiogroup\"]", 3], ["#mPick .pickCell", 10], ["#mPick .pickCell.on", 3], ["#mPick .pickSw", 5], ["#mPick .pickSw.on", 1]], "attr": [["#mPick .pickPreview .worldOrb", "data-template", "pair-obsidian-tint"], ["#mOrb .worldOrb", "data-template", "pair-obsidian-tint"]], "checked": "#mPick input[name=\"mPickAccent\"][value=\"sea\"]", "text": [["#mPlanet .v small", "your choice."]] } },
24
24
  "in-manage-card": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mCard", "#mCardPick .pick .pickPreview .cardBand", "#mCardPick [data-card-save]", "#mCardWhat summary"], "count": [["#mCardPick .pickCells[role=\"radiogroup\"]", 1], ["#mCardPick .pickCell", 5], ["#mCardPick .pickCell.on", 1]], "checked": "#mCardPick input[name=\"mCardBackdrop\"][value=\"\"]", "attr": [["#mCardPick .cardBand", "data-own", ""]] } },
25
+ "in-manage-look": {"auth":true,"url":"/projects?view=manage&id=101","actions":[["wait",900],["focus","#mLkLook-grove"],["wait",200]],"expect":{"visible":["#mLook","#mLookPick .lookOpts","#mLkLook-grove","#mLkTint","#mLookSave",".lookPrev .mh.dark","#mLookWhat summary"]}},
25
26
  "in-manage-kind": { "auth": true, "url": "/projects?view=manage&id=101", "expect": {"visible":["#mKind","#mKindPick .modBand","#mKindWhat summary"],"count":[["#mKindPick .modOpt",5],["#mKindPick .modOpt[aria-pressed=\"true\"]",0]],"text":[["#mKindPick .modOpt b","Game"],["#mKindPick .finePrint","This project has never said what kind it is. Saying so puts the word beside its name wherever it is listed; it changes nothing else."]]} },
26
27
  "in-manage-invite": { "auth": true, "url": "/projects?view=manage&id=101", "expect": { "visible": ["#mInvite", "#mInvite h2", "#mInviteWho", "#mInvite [data-invite-send]"] } },
27
28
  "in-manage-invite-handoff": { "auth": true, "url": "/projects?view=manage&id=101", "actions": [["wait",900],["fill","#mInviteWho","octo-builder"],["click","#mInvite [data-invite-send]"],["wait",400]], "expect": {"visible":["#mInviteList a","#mInviteMsg"],"hidden":[]} },
@@ -49,9 +50,11 @@
49
50
  "in-new-address": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-address"],"hidden":["#panel-home","#qWrap"]} },
50
51
  "in-new-app": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-app","#appOpts","#appOptRender"],"hidden":["#panel-address","#panel-review","#qWrap"]} },
51
52
  "in-new-app-render": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#appOptRender input"],["wait",300]], "expect": {"visible":["#panel-app","#renderSetup","#f-renderKey","#renderCheck"],"hidden":["#renderWs","#renderSvc","#renderNew"]} },
52
- "in-new-review": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-review","#reviewList li"],"hidden":["#panel-address","#qWrap"]} },
53
- "in-new-done": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500]], "expect": {"visible":["#panel-done","#liveProgress .prog","#humanSteps li"],"hidden":["#panel-review","#qWrap"]} },
54
- "in-new-resume-offer": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500],["click","#projTrig"],["wait",350],["click","#menuProjects [data-nav=\"new\"]"],["wait",500]], "expect": {"visible":["#panel-terms","#wizResume","#wizResumeGo"],"hidden":["#panel-done","#panel-name"],"text":[["#wizResumeName","Mercury"]]} },
53
+ "in-new-look": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400]],"expect":{"visible":["#panel-look","#wizLook","#wizLook-chrome-world","#wizTint","#wizLogo",".lookPrev .mh.light",".lookPrev .mh.dark"],"hidden":["#panel-app","#panel-review"]}},
54
+ "in-new-look-custom": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#wizLook-custom"],["wait",200]],"expect":{"visible":["#panel-look","#wizAccent","#wizAccentNote"],"hidden":["#panel-review"]}},
55
+ "in-new-review": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#panel-look button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-review","#reviewList li"],"hidden":["#panel-address","#qWrap"]} },
56
+ "in-new-done": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#panel-look button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500]], "expect": {"visible":["#panel-done","#liveProgress .prog","#humanSteps li"],"hidden":["#panel-review","#qWrap"]} },
57
+ "in-new-resume-offer": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#panel-look button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500],["click","#projTrig"],["wait",350],["click","#menuProjects [data-nav=\"new\"]"],["wait",500]], "expect": {"visible":["#panel-terms","#wizResume","#wizResumeGo"],"hidden":["#panel-done","#panel-name"],"text":[["#wizResumeName","Mercury"]]} },
55
58
  "out-map-projects": { "auth": false, "actions": [["click", "#projTrig"], ["wait", 350]], "expect": { "expanded": true, "hidden": ["#menuProjects [data-nav=\"mine\"]"] } },
56
59
  "card": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://example.com\"]"], ["wait", 900]], "expect": { "dialog": true, "hasClass": [["#projCard", "has-art"]], "visible": ["#cardArt", "#cardFaces", "#cardBuildersFig", "#cardShippedFig", "#cardOpen", "#cardHall"], "text": [["#cardBuilders", "11"], ["#cardShipped", "0"], ["#cardRel", "you build here"], ["#cardRuns", "example.com"], ["#cardFaces .facesMore", "+6"]], "count": [["#cardFaces img", 5]], "href": [["#cardHall", "https://builders.example.com/"]] } },
57
60
  "card-noart": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://emersonian-circles.cloudbongos.com\"]"], ["wait", 600]], "expect": { "dialog": true, "hasClass": [["#projCard", "has-art", false]], "hidden": ["#cardArt", "#cardFaces", "#cardHall"], "visible": ["#cardBuildersFig", "#cardShippedFig", "#cardOpen"], "text": [["#cardShipped", "30+"], ["#cardRel", "you build here"]] } },
@@ -201,6 +201,8 @@ const STUB_VOCAB = {
201
201
  join_grant: ['view', 'apply', 'full'],
202
202
  artist_gate: ['off', 'advisory', 'strict'],
203
203
  spend_payer: ['none', 'project', 'builder'],
204
+ look: ['own', 'chrome-world', 'expedition', 'grove', 'blueprint', 'custom'], // the look + its night (task 1004421)
205
+ night_tint: ['on', 'off'],
204
206
  planet_template: null, // the template set is long; shape is checked, not membership
205
207
  planet_accent: ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'],
206
208
  card_backdrop: ['nebula', 'dust', 'deepfield', 'chrome'],
@@ -224,6 +226,53 @@ function settingsPatch(res, id, body) {
224
226
  : { queued: false, restart: false, message: 'Saved. The sky, your list and the card draw it this way from here; nothing on your project restarts for it.' };
225
227
  return json(res, 200, { ok: true, saved: FLOW_RECEIPTS.settings.settings, planet: PLANETS.get(String(id)) || NO_PLANET, push });
226
228
  }
229
+ // a project's look (task 1004421, ADR 0358): GET/PATCH …/look and PUT/DELETE …/logo,
230
+ // validated the way routes/look.js validates and REMEMBERED for the run, so the manage
231
+ // page repaints from what it saved. The accent is stored as sent — the real route nudges
232
+ // it, and the page shows its own copy of that nudge before it ever asks.
233
+ const LOOK_STATE = new Map();
234
+ const LOGO_TYPES = ['image/png', 'image/jpeg', 'image/webp'];
235
+ function lookOf(id) {
236
+ return LOOK_STATE.get(String(id)) || { look: 'own', night_tint: 'on', look_accent: null, logo_url: null };
237
+ }
238
+ const LOOK_RECEIPT = { reachable: true, queued: true, restart: true, message: 'Saved. Your project will restart to pick this up — until then it keeps its current setting.' };
239
+ function lookRoute(req, res, rest) {
240
+ const m = rest.match(/^\/provisioning\/instances\/(\d+)\/(look|logo)$/);
241
+ if (!m) return false;
242
+ if (!AUTH) { json(res, 401, UNAUTH); return true; }
243
+ const id = m[1], cur = lookOf(id);
244
+ if (m[2] === 'look' && req.method === 'GET') { json(res, 200, { ok: true, look: cur, options: { look: STUB_VOCAB.look, night_tint: STUB_VOCAB.night_tint }, logo_max_bytes: 262144 }); return true; }
245
+ if (m[2] === 'look' && req.method === 'PATCH') {
246
+ readBody(req, (body) => {
247
+ const bad = Object.keys(body).find((k) => !['look', 'night_tint', 'accent'].includes(k))
248
+ || (body.look !== undefined && !STUB_VOCAB.look.includes(body.look) && 'look')
249
+ || (body.night_tint !== undefined && !STUB_VOCAB.night_tint.includes(body.night_tint) && 'night_tint');
250
+ if (bad) return json(res, 400, { error: { code: 'validation_failed', message: `bad field: ${bad}` } });
251
+ if (body.accent !== undefined && !/^#?[0-9a-f]{6}$/i.test(String(body.accent))) return json(res, 400, { error: { code: 'bad_accent', message: 'An accent is a colour written #rrggbb, for example #b48cf2.' } });
252
+ const next = { ...cur, ...(body.look ? { look: body.look } : {}), ...(body.night_tint ? { night_tint: body.night_tint } : {}), ...(body.accent ? { look_accent: String(body.accent).toLowerCase() } : {}) };
253
+ if (next.look === 'custom' && !next.look_accent) return json(res, 400, { error: { code: 'accent_required', message: 'Your own look needs an accent colour — send accent as #rrggbb.' } });
254
+ LOOK_STATE.set(id, next);
255
+ return json(res, 200, { ok: true, look: next, push: LOOK_RECEIPT, adjusted: null });
256
+ });
257
+ return true;
258
+ }
259
+ if (m[2] === 'logo' && req.method === 'PUT') {
260
+ readBody(req, (body) => {
261
+ if (!LOGO_TYPES.includes(body.content_type) || typeof body.image_b64 !== 'string' || !body.image_b64) return json(res, 400, { error: { code: 'logo_bad_type', message: 'A logo must be a PNG, JPEG or WebP image.' } });
262
+ const next = { ...cur, logo_url: 'data:' + body.content_type + ';base64,' + body.image_b64 };
263
+ LOOK_STATE.set(id, next);
264
+ return json(res, 200, { ok: true, look: next, push: LOOK_RECEIPT });
265
+ });
266
+ return true;
267
+ }
268
+ if (m[2] === 'logo' && req.method === 'DELETE') {
269
+ const next = { ...cur, logo_url: null };
270
+ LOOK_STATE.set(id, next);
271
+ json(res, 200, { ok: true, look: next, push: LOOK_RECEIPT });
272
+ return true;
273
+ }
274
+ return false;
275
+ }
227
276
  // the row carries its planet: a saved pick for the run, else the fixture's own (or null)
228
277
  function withPlanet(inst) {
229
278
  const saved = PLANETS.get(String(inst.id));
@@ -433,6 +482,7 @@ const server = http.createServer((req, res) => {
433
482
  // the manage flows (task 1003345): every door on a row and on the manage page only ASKS
434
483
  // (ADR 0111) — the stub answers each ask with the route's receipt so the probes can drive
435
484
  // attach / restart / teardown / revive / join end to end; nothing changes in the fixture
485
+ if (lookRoute(req, res, rest)) return;
436
486
  if (req.method !== 'GET') {
437
487
  const ps = rest.match(/^\/provisioning\/instances\/(\d+)\/settings$/);
438
488
  if (ps && req.method === 'PATCH') return AUTH ? readBody(req, (body) => settingsPatch(res, ps[1], body)) : json(res, 401, UNAUTH);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.20.59",
3
+ "version": "1.20.60",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.20.59",
9
+ "version": "1.20.60",
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.20.59",
3
+ "version": "1.20.60",
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",
@@ -8471,5 +8471,11 @@
8471
8471
  "id": "1004433",
8472
8472
  "text": "The profile now knows each person by their own work. An artist's page counts pages approved, how many pages of each part of the site they have tweaked, and what pages earned them; an ideator's counts thoughts filed, how many b"
8473
8473
  }
8474
+ ],
8475
+ "1.20.60": [
8476
+ {
8477
+ "id": "1004421",
8478
+ "text": "Every project can now have its own look, so builders who work in several projects always know which hall they are in. Creating a project has a new Look step: four ready-made looks or your own colour, a switch to tint dark mode"
8479
+ }
8474
8480
  ]
8475
8481
  }
@@ -566,6 +566,10 @@ const KERNEL_FILES = [
566
566
  // config / identity (host context a module reads, never hardcodes)
567
567
  'src/instance-config.js',
568
568
  'src/branding.js',
569
+ // task 1004421 (ADR 0358): branding.js lays the chosen look over the pack through this
570
+ // file — the same identity/config category, requiring only instance-config and node
571
+ // built-ins, so it joins the roster rather than reading as a kernel→domain leak.
572
+ 'src/hall-look.js',
569
573
  'src/build-info.js',
570
574
  // task 1003476 (goal 1000072): the project's stale-claim timer — host configuration
571
575
  // a module reads rather than hardcodes, exactly the category instance-config.js and
@@ -111,7 +111,10 @@ function buildGdsRouter() {
111
111
  // 'completed'. The list is NOT trimmable to fit: modules/grading/grader-score.js runs
112
112
  // the server-side permission-path pre-pass against it (SR-9 / SEC #863), so a short
113
113
  // list would quietly weaken a security check rather than merely lose detail.
114
- /^\/tasks\/\d+\/grade$/.test(req.path)
114
+ /^\/tasks\/\d+\/grade$/.test(req.path) ||
115
+ // task 1004421: a project's logo — up to 256 KB decoded, ~342 KB as base64.
116
+ // modules/provisioning/routes/look.js mounts its own 400 KB parser.
117
+ /^\/provisioning\/instances\/\d+\/logo$/.test(req.path)
115
118
  ) return next();
116
119
  return globalJson(req, res, next);
117
120
  });
@@ -43,6 +43,7 @@ const { buildStampedHtml } = require('../asset-version');
43
43
  const { buildInfo } = require('../build-info');
44
44
  const { buildBongosDownloads } = require('../bongos-downloads');
45
45
  const { clientBranding, branding, applyBrandConditionals } = require('../branding');
46
+ const { nightTintCss } = require('../night-tint');
46
47
  const { prelaunchGate } = require('./prelaunch-gate');
47
48
  const { platformVisibilityGate } = require('./platform-visibility-gate');
48
49
  const { schemaPending } = require('./schema-lag');
@@ -121,6 +122,11 @@ function themeMarkup(theme) {
121
122
  if (decls.length) css.push(`:root{${decls.join('')}}`);
122
123
  const backdrop = cssSafe(theme.backdrop);
123
124
  if (backdrop) css.push(`body{background:${backdrop};}`);
125
+ // The night tint (ADR 0358): a hall wearing a look takes a hint of its accent into
126
+ // its dark grounds. Written as the sheet's own two dark twins, after the sheet, so
127
+ // equal specificity wins on order. clientBranding only sets it with a look, so a
128
+ // hall that chose none keeps the sheet's pure black.
129
+ if (theme.nightTint === true) css.push(nightTintCss());
124
130
  if (css.length) out += `<style>${css.join('')}</style>`;
125
131
  // esc() is lossless in an attribute: the HTML parser decodes the entities back
126
132
  // before anything reads the URL, so the neutral pack's inline `data:image/svg+xml,
package/src/branding.js CHANGED
@@ -19,6 +19,7 @@
19
19
  const fs = require('node:fs');
20
20
  const path = require('node:path');
21
21
  const { resolveCoreRoot, resolveInstanceRoot, resolveEnv, FALLBACK_ENV_PREFIX } = require('./instance-config');
22
+ const { applyLook, readLibraryLook } = require('./hall-look');
22
23
 
23
24
  // ADR 0108 §1: the neutral starter ships WITH the core package; the instance
24
25
  // pack is host content. Both resolvers return the repo root today, so this is
@@ -83,6 +84,16 @@ const ENV_OVERRIDES = [
83
84
  // door is `open` and this is `none`. Like its neighbours it is memoized for the
84
85
  // process, which is why the platform restarts the instance to apply a change.
85
86
  ['SPEND_PAYER', ['project', 'spendPayer']],
87
+ // The look this hall wears (ADR 0358, task 1004421) — own | a style-library name |
88
+ // custom — and its night tint (on | off). Policy keys like their neighbours: the
89
+ // platform pushes them and a restart applies them. LOOK_ACCENT (a custom look's one
90
+ // accent, already made readable by the platform) and LOGO_URL (the platform's
91
+ // content-addressed logo) are their companions. src/hall-look.js turns them into
92
+ // the fifteen; an unknown or malformed value leaves the hall on its own pack.
93
+ ['LOOK', ['project', 'look']],
94
+ ['NIGHT_TINT', ['project', 'nightTint']],
95
+ ['LOOK_ACCENT', ['project', 'lookAccent']],
96
+ ['LOGO_URL', ['project', 'logoUrl']],
86
97
  ];
87
98
 
88
99
  const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
@@ -115,7 +126,7 @@ function deepFreeze(o) {
115
126
  // resolver. Decided in ADR 0142 (audit H11 / task 1002224).
116
127
  //
117
128
  // Pure resolver — all inputs injected, so tests drive it with zero I/O.
118
- function resolveBranding({ neutral = {}, instance = {}, env = {} } = {}) {
129
+ function resolveBranding({ neutral = {}, instance = {}, env = {}, readLook = readLibraryLook } = {}) {
119
130
  // structuredClone so the merged result shares no nested references with the
120
131
  // neutral/instance inputs — otherwise the deepFreeze below would also freeze
121
132
  // the caller's input objects (deepMerge shallow-copies only the top level).
@@ -142,6 +153,9 @@ function resolveBranding({ neutral = {}, instance = {}, env = {} } = {}) {
142
153
  if (missing.length) {
143
154
  throw new Error(`branding: missing required field(s): ${missing.map((p) => p.join('.')).join(', ')}`);
144
155
  }
156
+ // The chosen look, laid over the pack's theme (ADR 0358). A no-op for a hall that
157
+ // chose none, which is every hall the platform has not pushed a look to.
158
+ applyLook(resolved, readLook);
145
159
  return deepFreeze(resolved);
146
160
  }
147
161
 
@@ -240,6 +254,11 @@ function clientBranding(b = branding()) {
240
254
  },
241
255
  favicon: ui.favicon || '',
242
256
  backdrop: ui.backdrop || '',
257
+ // The look (ADR 0358): which one this hall wears ('' = its own pack), whether
258
+ // its night is tinted (only ever true with a look), and its logo address.
259
+ look: ui.look || '',
260
+ nightTint: ui.nightTint === true,
261
+ logo: ui.logo || '',
243
262
  },
244
263
  copy: {
245
264
  tagline: b.copy?.tagline || '',
@@ -282,6 +301,12 @@ function clientBranding(b = branding()) {
282
301
  // with?", not the platform's record of what they saved. The fallback is the
283
302
  // platform default, which for this key IS today's behaviour.
284
303
  spendPayer: b.project?.spendPayer || 'none',
304
+ // The look and its night (ADR 0358, task 1004421). Published for the read-back
305
+ // reason its neighbours are: the settings push compares this manifest with what
306
+ // the platform saved. The fallbacks are the platform defaults — 'own' is today's
307
+ // hall, and 'on' only ever tints a hall that wears a look.
308
+ look: b.project?.look || 'own',
309
+ nightTint: b.project?.nightTint || 'on',
285
310
  },
286
311
  };
287
312
  }
@@ -0,0 +1,99 @@
1
+ // src/hall-look.js — the look a hall wears, laid over its branding pack (ADR 0358,
2
+ // task 1004421).
3
+ //
4
+ // The platform chooses a project's look (provisioning settings `look`, `night_tint`
5
+ // plus the companion LOOK_ACCENT and LOGO_URL) and pushes only NAMES over web.env;
6
+ // src/branding.js maps them onto project.look / nightTint / lookAccent / logoUrl and
7
+ // hands the merged pack here. This file turns the name into the fifteen, reading the
8
+ // style library the core package ships (a module's styles/<name>/pack.json, ADR 0219),
9
+ // so a palette never travels as env text. The library is found by its SHAPE — any
10
+ // shipped module's styles/ directory — never by a module key, which core may not name
11
+ // (ADR 0083 §Decision #4).
12
+ //
13
+ // own (or absent) the instance's own pack — exactly today's hall.
14
+ // <library name> that look's thirteen colours and two faces.
15
+ // custom the chrome world with ONE accent, already made readable in both
16
+ // modes by the platform (lookAdjustAccent); a value that is not a
17
+ // plain #rrggbb is ignored and the hall stays on its own pack.
18
+ //
19
+ // Favicon and backdrop stay the instance's (ADR 0219 §1) except that a looked hall
20
+ // gets a favicon in its own colours, and a logo, when there is one, is the favicon.
21
+ // PURE given `readLook`; the default reader is the only I/O.
22
+ 'use strict';
23
+
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+
27
+ const HEX = /^#[0-9a-f]{6}$/i;
28
+ const LOOK_NAME = /^[a-z][a-z-]{0,39}$/;
29
+ // A logo address is the platform's content-addressed image URL. https only, and no
30
+ // character that could end the attribute or the CSS it is written into.
31
+ const LOGO_URL = /^https:\/\/[^\s"'<>()\\]+$/;
32
+
33
+ // The style library's packs, read once each. null for an unknown name or a pack that
34
+ // does not carry the thirteen as hex — a broken file never half-applies.
35
+ const _looks = new Map();
36
+ function readLibraryLook(name, { root } = {}) {
37
+ if (!LOOK_NAME.test(String(name || ''))) return null;
38
+ const key = `${root || ''}|${name}`;
39
+ if (_looks.has(key)) return _looks.get(key);
40
+ let ui = null;
41
+ try {
42
+ const base = root || require('./instance-config').resolveCoreRoot(); // eslint-disable-line global-require
43
+ const modulesDir = path.join(base, 'modules');
44
+ const file = fs.readdirSync(modulesDir).sort()
45
+ .map((m) => path.join(modulesDir, m, 'styles', name, 'pack.json'))
46
+ .find((f) => fs.existsSync(f));
47
+ const pack = file ? JSON.parse(fs.readFileSync(file, 'utf8')) : null;
48
+ const t = pack && pack.theme && pack.theme.ui;
49
+ if (t && t.colors && Object.values(t.colors).every((v) => HEX.test(v)) && t.fonts) ui = { colors: t.colors, fonts: t.fonts };
50
+ } catch { ui = null; }
51
+ _looks.set(key, ui);
52
+ return ui;
53
+ }
54
+
55
+ // The favicon a looked hall shows without a logo: the chrome world's two-circle mark
56
+ // redrawn in the look's own accent, terracotta and card.
57
+ function lookFavicon(colors) {
58
+ const c = (k) => encodeURIComponent(colors[k]);
59
+ return "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>" +
60
+ `<circle cx='12' cy='17' r='8' fill='${c('accent')}'/><circle cx='22' cy='17' r='6' fill='${c('terracotta')}'/>` +
61
+ `<circle cx='12' cy='17' r='3.5' fill='${c('bgCard')}'/><circle cx='22' cy='17' r='2.5' fill='${c('bgCard')}'/></svg>`;
62
+ }
63
+
64
+ // What the hall wears: { name, ui } or null for its own pack. PURE given readLook.
65
+ function chosenLook(project = {}, readLook = readLibraryLook) {
66
+ const name = String(project.look || 'own');
67
+ if (name === 'own') return null;
68
+ if (name === 'custom') {
69
+ const accent = String(project.lookAccent || '');
70
+ const base = HEX.test(accent) ? readLook('chrome-world') : null;
71
+ return base ? { name, ui: { colors: { ...base.colors, accent: accent.toLowerCase() }, fonts: base.fonts } } : null;
72
+ }
73
+ const ui = readLook(name);
74
+ return ui ? { name, ui } : null;
75
+ }
76
+
77
+ // Lay the look over a resolved (still mutable) pack, in place. A logo applies with or
78
+ // without a look; the night tint only with one (ADR 0358 D2), so a hall that never
79
+ // chose a look renders exactly as before whatever its tint setting says.
80
+ function applyLook(resolved, readLook = readLibraryLook) {
81
+ const project = resolved.project || {};
82
+ const look = chosenLook(project, readLook);
83
+ const logo = LOGO_URL.test(String(project.logoUrl || '')) ? project.logoUrl : '';
84
+ if (!look && !logo) return resolved;
85
+ resolved.theme = resolved.theme && typeof resolved.theme === 'object' ? resolved.theme : {};
86
+ const ui = { ...(resolved.theme.ui || {}) };
87
+ if (look) {
88
+ ui.colors = { ...look.ui.colors };
89
+ ui.fonts = { ...look.ui.fonts };
90
+ ui.look = look.name;
91
+ ui.nightTint = project.nightTint !== 'off';
92
+ ui.favicon = lookFavicon(ui.colors);
93
+ }
94
+ if (logo) { ui.logo = logo; ui.favicon = logo; }
95
+ resolved.theme.ui = ui;
96
+ return resolved;
97
+ }
98
+
99
+ module.exports = { applyLook, readLibraryLook };
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.20.59'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.20.60'; // 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,26 @@
1
+ // src/night-tint.js — the night tint a hall wears with a look (ADR 0358, task 1004421).
2
+ //
3
+ // The hall's dark tier (modules/hall-ui/public/style.css) is pure black. A hall that
4
+ // wears a look takes a hint of that look's own accent into its four dark grounds
5
+ // instead, so two halls stay told apart at night too. It is a FORMULA over the
6
+ // thirteen, never a palette a look authors: no token is added, and a custom accent
7
+ // gets its own night for free. This table is the one copy — themeMarkup() emits it
8
+ // and tests/hall_tokens.mjs (UI_DESIGN_TINT=1) measures it.
9
+ 'use strict';
10
+
11
+ const NIGHT_TINT = Object.freeze({
12
+ '--bg': 'color-mix(in srgb, var(--accent) 6%, #000000)',
13
+ '--bg-deep': 'color-mix(in srgb, var(--accent) 6%, #000000)',
14
+ '--bg-frame': 'color-mix(in srgb, var(--accent) 6%, #08080a)',
15
+ '--bg-card': 'color-mix(in srgb, var(--accent) 5%, #101012)',
16
+ });
17
+
18
+ // The CSS that applies it: the same two dark twins the sheet writes (the [data-theme]
19
+ // switch shell.js owns, and the media block that covers first paint), at the same
20
+ // specificity. Injected AFTER the sheet, so later wins. PURE.
21
+ function nightTintCss() {
22
+ const decls = Object.entries(NIGHT_TINT).map(([k, v]) => `${k}:${v};`).join('');
23
+ return `html[data-theme="dark"]{${decls}}@media (prefers-color-scheme: dark){html:not([data-theme]){${decls}}}`;
24
+ }
25
+
26
+ module.exports = { NIGHT_TINT, nightTintCss };
@@ -59,11 +59,14 @@ test('clientBranding exposes project policy — the settings read-back (task 100
59
59
  // is published for a reason of its own: it is the one key that can REFUSE a
60
60
  // module, so an owner looking at a module that will not turn on needs the
61
61
  // instance's own answer rather than the platform's record of what they saved.
62
+ // `look` and `nightTint` joined in task 1004421 (ADR 0358) — the look the hall wears
63
+ // and whether its night takes a tint. Public because the hall paints them in the
64
+ // browser; their fallbacks ('own', 'on') are what an unconfigured hall shows today.
62
65
  assert.deepEqual(clientBranding({}).project,
63
- { platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' });
66
+ { platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none', look: 'own', nightTint: 'on' });
64
67
  assert.deepEqual(
65
- clientBranding({ project: { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder' } }).project,
66
- { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder' },
68
+ clientBranding({ project: { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder', look: 'grove', nightTint: 'off' } }).project,
69
+ { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder', look: 'grove', nightTint: 'off' },
67
70
  'what the instance actually resolved (env override included) is what it publishes');
68
71
  });
69
72
 
@@ -86,7 +89,7 @@ test('clientBranding is a SMALL allow-list — no server-only field leaks to the
86
89
  // public for the same read-back reason as its neighbours. Its companion stamp
87
90
  // (artistGateSince) deliberately does NOT: that is platform bookkeeping the server
88
91
  // reads, and no browser surface has a question it answers.
89
- assert.deepEqual(Object.keys(projected.project).sort(), ['artistGate', 'joinGrant', 'joinability', 'platformVisibility', 'spendPayer', 'visibility']);
92
+ assert.deepEqual(Object.keys(projected.project).sort(), ['artistGate', 'joinGrant', 'joinability', 'look', 'nightTint', 'platformVisibility', 'spendPayer', 'visibility']);
90
93
  for (const leak of ['domains', 'repo', 'db', 'envPrefix', 'llmProjectName']) {
91
94
  assert.equal(projected[leak], undefined, `${leak} must not reach the client`);
92
95
  }
@@ -41,6 +41,7 @@ import assert from 'node:assert/strict';
41
41
  import fs from 'node:fs';
42
42
  import path from 'node:path';
43
43
  import { fileURLToPath } from 'node:url';
44
+ import { createRequire } from 'node:module';
44
45
 
45
46
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
46
47
  const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
@@ -102,9 +103,17 @@ const PACK = Object.fromEntries(
102
103
  Object.entries(packUi.colors).map(([k, v]) => [camelToVar(k), v]),
103
104
  );
104
105
 
106
+ // UI_DESIGN_TINT=1 measures the NIGHT TINT a hall wears with a look (ADR 0358, task
107
+ // 1004421): themeMarkup() lays src/night-tint.js's four grounds over the dark tier,
108
+ // after the sheet, so they win. tests/look_night_tint.mjs runs this file once per look
109
+ // with it set, and once per custom accent round the hue wheel.
110
+ const NIGHT_TINT = process.env.UI_DESIGN_TINT === '1'
111
+ ? createRequire(import.meta.url)('../src/night-tint.js').NIGHT_TINT
112
+ : {};
113
+
105
114
  const RAW = {
106
115
  light: { ...contractTier, ...PACK },
107
- dark: { ...contractTier, ...PACK, ...darkExplicit },
116
+ dark: { ...contractTier, ...PACK, ...darkExplicit, ...NIGHT_TINT },
108
117
  };
109
118
 
110
119
  // ── color resolution ─────────────────────────────────────────────────────────
@@ -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-09-30-navback2/);
638
+ assert.match(HTML, /shell\.js\?v=2026-10-01-look/);
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`);