@bongos/core 1.25.25 → 1.25.27
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 +26 -26
- package/docs/copy-inventory.md +4 -4
- package/docs/copy-registry.json +6 -6
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +81 -81
- package/modules/hall-ui/public/city.css +3 -2
- package/modules/hall-ui/public/style.css +2 -1
- package/modules/hall-ui/public/thinking.css +8 -6
- package/modules/hall-ui/public/visor.css +65 -31
- package/modules/hall-ui/public/visor.js +121 -6
- package/modules/hall-ui/records/visor.md +14 -0
- package/modules/ui-design/scripts/gen-design-reference.js +3 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/bongos/gen-brand.js +2 -1
- package/src/module-api.js +1 -1
- package/tests/brand_mark.mjs +2 -1
- package/tests/design_counts_ratchet.mjs +21 -0
- package/tests/design_reference.mjs +1 -1
- package/tests/hall_dialog.mjs +20 -1
- package/tests/hall_visor.mjs +100 -1
|
@@ -89,8 +89,9 @@ html:has(body[data-page="city"]) { height: 100%; }
|
|
|
89
89
|
.city-main { position: fixed; top: var(--topbar-h, 56px); left: var(--sidebar-w-rail, 64px); right: 0; bottom: 0; max-width: none; margin: 0; contain: layout paint; z-index: 1; }
|
|
90
90
|
html[data-fullscreen="on"] .city-main { top: 0; left: 0; }
|
|
91
91
|
/* tasks 1005148, 1005195 and 1005209: with the visor up (game mode), the door (now its line alone) rises clear of the
|
|
92
|
-
foot row, whose one button is centred too
|
|
93
|
-
|
|
92
|
+
foot row, whose one button is centred too. Task 1005266: the hints are keys alone, so they no
|
|
93
|
+
longer wrap onto a second line, and the row sits --visor-drop lower (visor.css; 0 on a phone). */
|
|
94
|
+
html.visor-on .city-door { bottom: calc(180px - var(--visor-drop, 0px)); }
|
|
94
95
|
/* task 1005209 (ADR 0403): with the visor up the door's button and the archived links are the
|
|
95
96
|
visor's (D on the glass, the rest in its Actions sheet), so the scene carries none of its own. */
|
|
96
97
|
html.visor-on .city-start, html.visor-on .city-archived { display: none; }
|
|
@@ -940,7 +940,8 @@ html[data-theme="dark"] .brand-mark__mark { --mark-ink: #e2e1e4; }
|
|
|
940
940
|
and the shell’s corner buttons, so the top arc, its notch and the place all step aside. The foot row keeps its three parts in one line and nothing leans: the sprite
|
|
941
941
|
with the count over its word, the one accent button (its name may take two lines; its key is in the sheet), and of
|
|
942
942
|
the key hints only Actions, as its dots (every other hint is a row in its sheet). */
|
|
943
|
-
:root[data-game="on"] { --visor-inset-bottom: 52px; --visor-foot-room: 112px; }
|
|
943
|
+
:root[data-game="on"] { --visor-inset-bottom: 52px; --visor-foot-room: 112px; --visor-arc-foot: 8px; }
|
|
944
|
+
html[data-game="on"].visor-on { --visor-drop: 0px; } /* task 1005266: a phone's foot row did not move */
|
|
944
945
|
html[data-game="on"] .visor__place, html[data-game="on"] .visor__progress, html[data-game="on"] .visor__notch, html[data-game="on"] .visor__frame--top, html[data-game="on"] .visor__name { display: none; }
|
|
945
946
|
html[data-game="on"] .visor__foot { grid-template-columns: auto minmax(0, 1fr) auto; column-gap: var(--ds-space-hair); }
|
|
946
947
|
html[data-game="on"] .visor__me, html[data-game="on"] .visor__hints { transform: none; }
|
|
@@ -309,12 +309,14 @@ html[data-fullscreen="on"] .sky-stage .top { padding-right: 184px; }
|
|
|
309
309
|
clear of the glass, as ADR 0399 §2.3 asks of a scene's pop-ups: the top row, the composer, the
|
|
310
310
|
jump field and the two side panels step below the compass and the place line, and the foot and
|
|
311
311
|
the tour rise above the visor's corner pills. Task 1005203 put the version's progress under
|
|
312
|
-
the place, so the top offsets grew by its 24px.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
html.visor-on .
|
|
316
|
-
html.visor-on .
|
|
317
|
-
html.visor-on .
|
|
312
|
+
the place, so the top offsets grew by its 24px. Task 1005266 made the glass slimmer: the top
|
|
313
|
+
band is 40px shorter, and the foot row sits --visor-drop lower (visor.css; 0 on a phone, whose
|
|
314
|
+
row did not move). Outside game mode nothing here applies. */
|
|
315
|
+
html.visor-on .top { padding-top: calc(44px + env(safe-area-inset-top, 0px)); }
|
|
316
|
+
html.visor-on .composer, html.visor-on .jump { top: calc(100px + env(safe-area-inset-top, 0px)); }
|
|
317
|
+
html.visor-on .panel, html.visor-on .room { top: calc(164px + env(safe-area-inset-top, 0px)); bottom: calc(200px - var(--visor-drop, 0px) + env(safe-area-inset-bottom, 0px)); }
|
|
318
|
+
html.visor-on .foot { padding-bottom: calc(132px - var(--visor-drop, 0px) + env(safe-area-inset-bottom, 0px)); }
|
|
319
|
+
html.visor-on .tour, html.visor-on .start { bottom: calc(210px - var(--visor-drop, 0px) + env(safe-area-inset-bottom, 0px)); }
|
|
318
320
|
/* task 1005210 (ADR 0403): with the visor up the sky carries no control of its own. The top
|
|
319
321
|
row's lens switch and pills are rows in the visor's Actions sheet; the composer and the jump
|
|
320
322
|
field open from the visor ("Write a thought" on /, "Jump" on J) and close again when left
|
|
@@ -8,13 +8,22 @@
|
|
|
8
8
|
hour's sky runs to every edge of the view, so the glass has no bands and no vignette). The
|
|
9
9
|
marks carry themselves: every line is drawn over a wider stroke of the night, a key sits on a chip of
|
|
10
10
|
the night, and the place, each hint and your credits stand on a small night plate. Colours, sizes and corners
|
|
11
|
-
are the hall tier's tokens; nothing here is typed in. Record: modules/hall-ui/records/visor.md.
|
|
11
|
+
are the hall tier's tokens; nothing here is typed in. Record: modules/hall-ui/records/visor.md.
|
|
12
|
+
IT RESTS WHILE YOU LOOK AND WAKES SLIM WHEN YOU ACT (task 1005266, owner 2026-11-13: the glass
|
|
13
|
+
took a third of a laptop's height). Awake, the top arc is the version's progress in four
|
|
14
|
+
pieces, the place and the percentage share one small line, and the foot row is smaller and
|
|
15
|
+
sits lower. At rest (.visor--rest, set by visor.js on a desk with a pointer) only the arcs,
|
|
16
|
+
the sight and your picture stay. A phone is as it was (style.css's shared phone block). */
|
|
12
17
|
.visor { display: none; }
|
|
18
|
+
/* how far the foot row dropped (task 1005266), for a scene's own layers that sat above the old
|
|
19
|
+
row (thinking.css, city.css). A phone's row did not move, so the phone block sets it to 0. */
|
|
20
|
+
html.visor-on { --visor-drop: 48px; }
|
|
13
21
|
/* where the glass sits, so a scene's pop-up, and a window the glass opens, can keep clear of
|
|
14
22
|
it (ADR 0399 §2.3). On the root, not the visor, because the options window is not inside
|
|
15
|
-
it. --visor-foot-room is the foot row's height
|
|
23
|
+
it. --visor-foot-room is the foot row's height (task 1005266: the hints are keys alone, so
|
|
24
|
+
the row is one line), and --visor-arc-foot is where the foot arc sits under it. */
|
|
16
25
|
html[data-game="on"] {
|
|
17
|
-
--visor-arc:
|
|
26
|
+
--visor-arc: 44px; --visor-inset-top: 48px; --visor-inset-bottom: 36px; --visor-foot-room: 56px; --visor-arc-foot: 10px;
|
|
18
27
|
--visor-inset-right: max(var(--ds-space-gutter-narrow), var(--helm-inset)); --visor-inset-left: var(--visor-inset-right);
|
|
19
28
|
}
|
|
20
29
|
html[data-game="on"] .visor:not([hidden]) {
|
|
@@ -41,43 +50,44 @@ html[data-game="on"] .visor:not([hidden]) {
|
|
|
41
50
|
top arc, its notch and the place all sit inside the top inset, above where a scene starts its
|
|
42
51
|
own top row; the foot arc runs under the foot row. */
|
|
43
52
|
.visor__frame { position: absolute; left: 0; width: 100%; overflow: visible; }
|
|
44
|
-
.visor__frame--top { top: calc(var(--visor-arc) -
|
|
45
|
-
.visor__frame--foot { bottom:
|
|
53
|
+
.visor__frame--top { top: calc(var(--visor-arc) - 36px); height: 42px; }
|
|
54
|
+
.visor__frame--foot { bottom: var(--visor-arc-foot); height: 18px; }
|
|
46
55
|
.visor__frame path { vector-effect: non-scaling-stroke; }
|
|
47
56
|
.visor__line { fill: none; stroke: var(--char, var(--ink-soft)); stroke-opacity: .7; stroke-width: var(--line-strong); }
|
|
48
57
|
.visor__line--under { stroke: var(--bg); stroke-opacity: 1; stroke-width: 5px; }
|
|
49
58
|
.visor__dot { fill: var(--char, var(--ink-soft)); stroke: var(--bg); stroke-width: var(--line-strong); }
|
|
50
59
|
.visor__notch, .visor__sight { position: absolute; left: 50%; transform: translate(-50%, -50%); overflow: visible; }
|
|
51
|
-
.visor__notch { top: calc(var(--visor-arc) -
|
|
60
|
+
.visor__notch { top: calc(var(--visor-arc) - 22px); width: 12px; height: 9px; }
|
|
61
|
+
/* the top arc as the version's progress (task 1005266): the same curve in four pieces. A quarter
|
|
62
|
+
built is the soft ink, the quarter being built is the character's colour, the rest are faint.
|
|
63
|
+
At rest the one plain arc stands in for them. */
|
|
64
|
+
.visor__seg--done { stroke: var(--ink-soft); stroke-opacity: 1; stroke-width: 3px; }
|
|
65
|
+
.visor__seg--now { stroke-opacity: 1; stroke-width: 3px; }
|
|
66
|
+
.visor__seg--todo { stroke: var(--ink-soft); stroke-opacity: .35; }
|
|
67
|
+
.visor--rest .visor__seg, .visor:not(.visor--rest) .visor__frame--segs .visor__arc { display: none; }
|
|
52
68
|
.visor__sight { top: 50%; width: 24px; height: 24px; }
|
|
53
69
|
|
|
54
70
|
/* type over the scene stands on a small plate of the night, cut like everything pressable and
|
|
55
71
|
sized to its words: the place, each key hint, and your name and credits. No band, no halo. */
|
|
56
72
|
.visor__place, .visor__who, .visor__hint { background: var(--night-plate); clip-path: var(--cut-path-sm); }
|
|
57
73
|
/* the name voice, for every name on the glass */
|
|
58
|
-
.visor__place, .visor__name, .visor__unit, .visor__main, .visor__hint, .visor__key, .visor__sheet-title, .visor__cap {
|
|
74
|
+
.visor__place, .visor__name, .visor__unit, .visor__main, .visor__hint, .visor__key, .visor__sheet-title, .visor__cap, .visor__hints::after {
|
|
59
75
|
font-family: var(--name-face); font-weight: var(--name-weight); text-transform: var(--name-case);
|
|
60
76
|
}
|
|
61
77
|
|
|
62
|
-
/* the place, under the top arc */
|
|
78
|
+
/* the place and the version's reading, one small line under the top arc (task 1005266) */
|
|
79
|
+
.visor__top {
|
|
80
|
+
position: absolute; left: 50%; top: calc(var(--visor-arc) - 18px); transform: translateX(-50%);
|
|
81
|
+
display: flex; align-items: center; gap: var(--ds-space-hair); white-space: nowrap;
|
|
82
|
+
}
|
|
63
83
|
.visor__place {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
color: var(--ink-soft); white-space: nowrap; padding: 0 var(--ds-space-xs);
|
|
84
|
+
margin: 0; font-size: var(--ds-text-xs); letter-spacing: var(--name-space); line-height: 1;
|
|
85
|
+
color: var(--ink); white-space: nowrap; padding: var(--ds-space-hair) var(--ds-space-xs);
|
|
67
86
|
}
|
|
68
87
|
|
|
69
|
-
/* the version's progress
|
|
70
|
-
reading in words
|
|
71
|
-
|
|
72
|
-
reads over a midday sky. It sits under the arc band, inside the top inset (--visor-inset-top). */
|
|
73
|
-
.visor__progress {
|
|
74
|
-
position: absolute; left: 50%; top: calc(var(--visor-arc) + 6px); transform: translateX(-50%);
|
|
75
|
-
display: flex; align-items: center; gap: var(--ds-space-xs); white-space: nowrap;
|
|
76
|
-
}
|
|
77
|
-
.visor__segs { flex-wrap: nowrap; }
|
|
78
|
-
.visor__segs .seg { border-color: var(--ink-soft); outline: var(--line-strong) solid var(--night-plate); }
|
|
79
|
-
.visor__segs .seg--done { background: var(--ink-soft); border-color: transparent; }
|
|
80
|
-
.visor__segs .seg--now { background: var(--accent); border-color: transparent; }
|
|
88
|
+
/* the version's progress beside the place (task 1005203; task 1005266 moved the four segments
|
|
89
|
+
onto the top arc): the same reading in words, since a row of bars is never the only reading. */
|
|
90
|
+
.visor__progress { display: flex; align-items: center; white-space: nowrap; }
|
|
81
91
|
.visor__progress-words {
|
|
82
92
|
margin: 0; font-family: var(--name-face); font-weight: var(--name-weight); text-transform: var(--name-case);
|
|
83
93
|
font-size: var(--ds-text-xs); letter-spacing: var(--name-space); line-height: 1; color: var(--ink-soft);
|
|
@@ -93,6 +103,11 @@ html[data-game="on"] .visor:not([hidden]) {
|
|
|
93
103
|
background: var(--smoke-sheen), var(--smoke); box-shadow: var(--smoke-edge);
|
|
94
104
|
-webkit-backdrop-filter: var(--smoke-blur); backdrop-filter: var(--smoke-blur);
|
|
95
105
|
}
|
|
106
|
+
/* a key's name (task 1005266) is a plate like the four above, so it is the same pane */
|
|
107
|
+
.visor__hints[data-name]::after {
|
|
108
|
+
background: var(--smoke-sheen), var(--smoke); box-shadow: var(--smoke-edge);
|
|
109
|
+
-webkit-backdrop-filter: var(--smoke-blur); backdrop-filter: var(--smoke-blur);
|
|
110
|
+
}
|
|
96
111
|
}
|
|
97
112
|
|
|
98
113
|
/* the foot row: you on the left, the one thing to press in the middle, the key hints on the
|
|
@@ -136,23 +151,23 @@ html[data-game="on"] .visor:not([hidden]) {
|
|
|
136
151
|
.visor__card-open { font-size: var(--ds-text-sm); color: var(--accent-ink); }
|
|
137
152
|
.visor__card-name, .visor__card-open, .visor__fact-k { font-family: var(--name-face); font-weight: var(--name-weight); text-transform: var(--name-case); letter-spacing: var(--name-space); }
|
|
138
153
|
.visor__sprite {
|
|
139
|
-
flex: none; box-sizing: border-box; width:
|
|
154
|
+
flex: none; box-sizing: border-box; width: 40px; height: 40px; object-fit: cover; image-rendering: pixelated;
|
|
140
155
|
background: var(--bg-card); border: var(--line-strong) solid var(--char, var(--ink-soft)); clip-path: var(--cut-path-sm);
|
|
141
156
|
}
|
|
142
157
|
/* your name and credits stand in a box of the night, cut like the sprite beside it */
|
|
143
158
|
.visor__who { display: flex; flex-direction: column; gap: 2px; padding: var(--ds-space-hair) var(--ds-space-xs); }
|
|
144
159
|
.visor__name { font-size: var(--ds-text-xs); letter-spacing: var(--name-space); line-height: 1; color: var(--char, var(--ink-soft)); }
|
|
145
160
|
.visor__credits { display: flex; align-items: baseline; gap: var(--ds-space-xs); white-space: nowrap; }
|
|
146
|
-
.visor__num { font-family: var(--name-face); font-size: var(--ds-text-
|
|
147
|
-
.visor__unit { font-size: var(--ds-text-
|
|
161
|
+
.visor__num { font-family: var(--name-face); font-size: var(--ds-text-4xl); font-weight: 400; line-height: 1; color: var(--ink); font-variant-numeric: tabular-nums; }
|
|
162
|
+
.visor__unit { font-size: var(--ds-text-2xs); letter-spacing: var(--name-space); color: var(--ink-soft); }
|
|
148
163
|
|
|
149
164
|
/* the one accent button: the cut corner, the dark ink, the key that takes it */
|
|
150
165
|
.visor__main {
|
|
151
166
|
pointer-events: auto; box-sizing: border-box; justify-self: center;
|
|
152
167
|
display: inline-flex; align-items: center; justify-content: space-between; gap: var(--ds-space-sm);
|
|
153
|
-
min-height:
|
|
168
|
+
min-height: 44px; padding: var(--ds-space-hair) var(--ds-space-xs) var(--ds-space-hair) var(--ds-space-sm);
|
|
154
169
|
background: var(--accent); color: var(--accent-on); border: 0; border-radius: var(--press-radius); clip-path: var(--cut-path);
|
|
155
|
-
font-size: var(--ds-text-
|
|
170
|
+
font-size: var(--ds-text-xl); font-weight: var(--name-weight-lit); letter-spacing: var(--name-space-tight); line-height: 1.1;
|
|
156
171
|
text-decoration: none; cursor: pointer; transition: background-color var(--ds-dur-fast) var(--ds-ease);
|
|
157
172
|
}
|
|
158
173
|
.visor__main:hover { background: var(--accent-lift); color: var(--accent-on); text-decoration: none; }
|
|
@@ -160,18 +175,30 @@ html[data-game="on"] .visor:not([hidden]) {
|
|
|
160
175
|
/* the key hints: a key and a name, no box round the pair. The only other parts of the glass
|
|
161
176
|
that take a pointer. */
|
|
162
177
|
.visor__hints {
|
|
163
|
-
justify-self: end; display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 0 var(--ds-space-
|
|
178
|
+
position: relative; justify-self: end; display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 0 var(--ds-space-hair);
|
|
164
179
|
transform: perspective(900px) rotateY(-16deg); transform-origin: right center;
|
|
165
180
|
}
|
|
166
181
|
.visor__hint {
|
|
167
182
|
pointer-events: auto; box-sizing: border-box; display: inline-flex; align-items: center; gap: var(--ds-space-hair);
|
|
168
|
-
min-height: 44px; padding: 0 var(--ds-space-xs)
|
|
183
|
+
min-height: 44px; padding: 0 var(--ds-space-xs); border: 0; border-radius: var(--press-radius);
|
|
169
184
|
color: var(--ink-soft); font-size: var(--ds-text-lg); letter-spacing: var(--name-space-tight); white-space: nowrap;
|
|
170
185
|
text-decoration: none; cursor: pointer;
|
|
171
186
|
transition: color var(--ds-dur-fast) var(--ds-ease);
|
|
172
187
|
}
|
|
173
188
|
.visor__hint:hover { color: var(--ink); text-decoration: none; }
|
|
174
189
|
.visor__hint[hidden] { display: none; }
|
|
190
|
+
/* a hint is its key alone (task 1005266). Its name stays in the page for a screen reader, and
|
|
191
|
+
when you point at the key or reach it with the keyboard the name stands on one small plate
|
|
192
|
+
above the row (visor.js nameHint sets data-name), so naming a key never moves another. The
|
|
193
|
+
place's own lead action keeps its words: they are the place's call to you (ADR 0403). */
|
|
194
|
+
.visor__hint:not(.visor__hint--place) > span:not(.visor__dots) {
|
|
195
|
+
position: absolute; width: 1px; height: 1px; padding: 0; border: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
|
|
196
|
+
}
|
|
197
|
+
.visor__hints[data-name]::after {
|
|
198
|
+
content: attr(data-name); position: absolute; right: 0; bottom: calc(100% + var(--ds-space-hair)); pointer-events: none;
|
|
199
|
+
padding: var(--ds-space-hair) var(--ds-space-xs); background: var(--night-plate); clip-path: var(--cut-path-sm);
|
|
200
|
+
color: var(--ink); font-size: var(--ds-text-sm); letter-spacing: var(--name-space-tight); line-height: 1.2; white-space: nowrap;
|
|
201
|
+
}
|
|
175
202
|
/* Actions on a phone: three dots on the same chip of the night a key sits on (style.css shows it) */
|
|
176
203
|
.visor__dots {
|
|
177
204
|
display: none; box-sizing: content-box; width: 14px; height: 4px; padding: 10px 5px; border: var(--line-strong) solid currentColor;
|
|
@@ -219,7 +246,7 @@ html[data-game="on"] .visor:not([hidden]) {
|
|
|
219
246
|
hairlines like the sheet, never wider than a short paragraph. The words are the scene's facts;
|
|
220
247
|
the status dot is the hall's status vocabulary (--ok / --warn / --info), never a colour of its own. */
|
|
221
248
|
.visor__focus {
|
|
222
|
-
position: absolute; left: 50%; bottom: calc(var(--visor-inset-bottom) +
|
|
249
|
+
position: absolute; left: 50%; bottom: calc(var(--visor-inset-bottom) + 56px); transform: translateX(-50%);
|
|
223
250
|
pointer-events: auto; box-sizing: border-box; width: min(20rem, calc(100vw - 2 * var(--ds-space-gutter-narrow)));
|
|
224
251
|
padding: var(--ds-space-xs) var(--ds-space-sm) var(--ds-space-sm); background: var(--bg-card);
|
|
225
252
|
border: 0 solid var(--rule); border-left-width: var(--line-strong); border-right-width: var(--line-strong);
|
|
@@ -241,6 +268,13 @@ html[data-game="on"] .visor:not([hidden]) {
|
|
|
241
268
|
.visor__focus .visor__key { color: var(--ink-soft); background: none; }
|
|
242
269
|
.visor__main--lead[aria-busy="true"] { cursor: progress; background: var(--accent-lift); }
|
|
243
270
|
|
|
271
|
+
/* RESTING (task 1005266): while you look, pan and scroll, everything but the arcs, the sight and
|
|
272
|
+
your picture fades out; visor.js wakes it when you reach for the bottom of the view, press a
|
|
273
|
+
visor key or tab onto it. The parts stay in the page (a key press, a screen reader and the
|
|
274
|
+
keyboard still reach them), and nothing moves, so waking is a fade and never a jump. */
|
|
275
|
+
.visor__top, .visor__notch, .visor__who, .visor__main, .visor__hints { transition: opacity var(--ds-dur-base) var(--ds-ease); }
|
|
276
|
+
.visor--rest .visor__top, .visor--rest .visor__notch, .visor--rest .visor__who, .visor--rest .visor__main, .visor--rest .visor__hints { opacity: 0; }
|
|
277
|
+
|
|
244
278
|
/* A WINDOW THE GLASS OPENS OPENS IN THE GLASS (task 1005253, ADR 0403; owner 2026-11-13). The
|
|
245
279
|
options window and your profile (shell-game.js openOverlay, .opt-window) and the game menu
|
|
246
280
|
(openMenu, .game-menu) are reached from the visor, so they never cover it: in game mode each
|
|
@@ -179,9 +179,87 @@
|
|
|
179
179
|
}),
|
|
180
180
|
keys: (Array.isArray(s.keys) ? s.keys : []).filter(function (k) { return k && k.label && k.key; }),
|
|
181
181
|
};
|
|
182
|
+
// Arriving at a place shows the whole glass for a moment; a scene that only registers
|
|
183
|
+
// again (a toggle's row changed) does not.
|
|
184
|
+
if (scene.name !== arrived) { arrived = scene.name; wake(ARRIVE_FOR); }
|
|
182
185
|
render();
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
// ---------- resting and waking (task 1005266, owner 2026-11-13) ----------
|
|
189
|
+
// The glass took a third of a laptop's height, so while you look, pan and scroll it RESTS:
|
|
190
|
+
// only the arcs, the sight and your picture stay (visor.css .visor--rest). It WAKES when you
|
|
191
|
+
// reach for it: the pointer near the bottom of the view or on a control, a visor key, the
|
|
192
|
+
// keyboard's focus coming onto it, the scene picking something, and for a moment on
|
|
193
|
+
// arriving at a place. It stays awake while the sheet or a scene's card is open, while the
|
|
194
|
+
// pointer is still there and while the keyboard is on it; it rests the moment you press or
|
|
195
|
+
// wheel on the scene, and after a few idle seconds. Only on a desk with a pointer: a touch
|
|
196
|
+
// screen has nothing to reach with, so a phone or tablet keeps the whole glass.
|
|
197
|
+
var REST_AFTER = 3000, ARRIVE_FOR = 4000, REACH = 160;
|
|
198
|
+
var awake = true, reaching = false, restTimer = null, arrived = null, until = 0;
|
|
199
|
+
// The two questions are asked of the browser once; each list keeps its own answer current,
|
|
200
|
+
// so the pointer's every move reads two booleans and builds nothing.
|
|
201
|
+
var mqPointer = null, mqPhone = null;
|
|
202
|
+
function canRest() {
|
|
203
|
+
if (typeof window.matchMedia !== 'function') return false;
|
|
204
|
+
if (!mqPointer) { mqPointer = window.matchMedia('(hover: hover) and (pointer: fine)'); mqPhone = window.matchMedia('(max-width: 640px)'); }
|
|
205
|
+
return mqPointer.matches && !mqPhone.matches;
|
|
206
|
+
}
|
|
207
|
+
function keyboardOn() { return !!(root && document.activeElement && root.contains(document.activeElement)); }
|
|
208
|
+
// A window the glass opened (the options, your profile, the game menu; task 1005253) holds it too.
|
|
209
|
+
function windowUp() { var c = document.documentElement.classList; return !!(c && (c.contains('opt-window-open') || c.contains('game-menu-open'))); }
|
|
210
|
+
function held() { return !!sheet || !!focus || reaching || keyboardOn() || windowUp(); }
|
|
211
|
+
function resting() { return !awake && canRest(); }
|
|
212
|
+
function paintRest() { if (root && root.classList) root.classList.toggle('visor--rest', resting()); }
|
|
213
|
+
// Waking only moves the hour it may rest at; one clock is ever running, so a pointer
|
|
214
|
+
// crossing the bottom of the view repaints nothing and starts no timer of its own.
|
|
215
|
+
function wake(ms) {
|
|
216
|
+
var was = awake;
|
|
217
|
+
awake = true;
|
|
218
|
+
if (!was) paintRest();
|
|
219
|
+
if (typeof setTimeout !== 'function') return; // no clock (a test's bare page): it stays awake
|
|
220
|
+
until = Math.max(until, Date.now() + (ms || REST_AFTER));
|
|
221
|
+
if (restTimer == null) restTimer = setTimeout(settle, ms || REST_AFTER);
|
|
222
|
+
}
|
|
223
|
+
function settle() {
|
|
224
|
+
restTimer = null;
|
|
225
|
+
var left = until - Date.now();
|
|
226
|
+
if (held()) { restTimer = setTimeout(settle, REST_AFTER); return; }
|
|
227
|
+
if (left > 0) { restTimer = setTimeout(settle, left); return; }
|
|
228
|
+
awake = false;
|
|
229
|
+
paintRest();
|
|
230
|
+
}
|
|
231
|
+
// You went back to the world: rest at once, and hand the keyboard back with it.
|
|
232
|
+
function rest() {
|
|
233
|
+
if (sheet || focus) return;
|
|
234
|
+
clearTimeout(restTimer);
|
|
235
|
+
restTimer = null;
|
|
236
|
+
until = 0;
|
|
237
|
+
if (keyboardOn() && document.activeElement.blur) document.activeElement.blur();
|
|
238
|
+
awake = false;
|
|
239
|
+
reaching = false;
|
|
240
|
+
paintRest();
|
|
241
|
+
}
|
|
242
|
+
// A key hint is its key alone; pointing at one, or reaching it with the keyboard, names it
|
|
243
|
+
// on the one plate above the row (visor.css .visor__hints[data-name]). Only where there is a
|
|
244
|
+
// pointer to hover with: a tap must not leave a name standing.
|
|
245
|
+
function nameHint(e) {
|
|
246
|
+
var box = root && root.querySelector('.visor__hints');
|
|
247
|
+
if (!box) return;
|
|
248
|
+
var h = (e.type === 'mouseover' || e.type === 'focusin') && e.target && e.target.closest ? e.target.closest('.visor__hint:not(.visor__hint--place)') : null;
|
|
249
|
+
var label = h ? h.querySelector('span:not(.visor__dots)') : null;
|
|
250
|
+
var hover = typeof window.matchMedia === 'function' && window.matchMedia('(hover: hover)').matches;
|
|
251
|
+
if (label && hover) box.setAttribute('data-name', label.textContent);
|
|
252
|
+
else box.removeAttribute('data-name');
|
|
253
|
+
}
|
|
254
|
+
function onRoot(e) { return !!(root && e.target && e.target.nodeType === 1 && root.contains(e.target)); }
|
|
255
|
+
function onPointerMove(e) {
|
|
256
|
+
if (!shown() || e.buttons || e.pointerType === 'touch') return; // a drag is looking, not reaching
|
|
257
|
+
reaching = onRoot(e) || e.clientY >= window.innerHeight - REACH;
|
|
258
|
+
if (reaching) wake();
|
|
259
|
+
}
|
|
260
|
+
function onPointerDown(e) { if (!shown()) return; if (onRoot(e)) wake(); else rest(); }
|
|
261
|
+
function onWheel(e) { if (shown() && !onRoot(e)) rest(); }
|
|
262
|
+
|
|
185
263
|
// ---------- the keys: one resolver, so no key ever means two things (task 1005248) ----------
|
|
186
264
|
// Every key the visor shows or binds comes from here, and here hands each key out once.
|
|
187
265
|
// A key in ACTIONS or a scene's registration is a RECOMMENDATION: when it is already taken
|
|
@@ -239,6 +317,7 @@
|
|
|
239
317
|
}
|
|
240
318
|
if (focus && !was) document.addEventListener('keydown', onFocusKey, true);
|
|
241
319
|
if (!focus && was) document.removeEventListener('keydown', onFocusKey, true);
|
|
320
|
+
wake(); // the place has something for you; the card holds the glass awake while it is up
|
|
242
321
|
render();
|
|
243
322
|
}
|
|
244
323
|
// Closed by the person (Escape or the close mark): the scene is told, so it unpicks.
|
|
@@ -288,8 +367,26 @@
|
|
|
288
367
|
// The two arcs are each a 1000×100 box stretched across the stage (preserveAspectRatio
|
|
289
368
|
// none), with non-scaling strokes so each stays one hairline at any size. The notch and the
|
|
290
369
|
// sight are drawn at their own size, so neither is ever stretched.
|
|
370
|
+
// One piece of the top arc, from t0 to t1 along it (task 1005266): the same curve, cut.
|
|
371
|
+
var ARC = [[76, 100], [500, -100], [924, 100]];
|
|
372
|
+
function arcPiece(t0, t1) {
|
|
373
|
+
var at = function (t, i) { var u = 1 - t; return u * u * ARC[0][i] + 2 * u * t * ARC[1][i] + t * t * ARC[2][i]; };
|
|
374
|
+
var mid = function (i) { return (1 - t0) * ((1 - t1) * ARC[0][i] + t1 * ARC[1][i]) + t0 * ((1 - t1) * ARC[1][i] + t1 * ARC[2][i]); };
|
|
375
|
+
var n = function (v) { return String(Math.round(v * 10) / 10); };
|
|
376
|
+
return 'M' + n(at(t0, 0)) + ' ' + n(at(t0, 1)) + 'Q' + n(mid(0)) + ' ' + n(mid(1)) + ' ' + n(at(t1, 0)) + ' ' + n(at(t1, 1));
|
|
377
|
+
}
|
|
378
|
+
// The top arc is the version's progress while the glass is awake: four pieces, one per
|
|
379
|
+
// quarter, coloured by segStates (visor.css .visor__seg--*). With no version to read, and
|
|
380
|
+
// at rest, it is the one plain arc.
|
|
381
|
+
function arcSegs() {
|
|
382
|
+
if (!progress) return '';
|
|
383
|
+
return segStates(progress.pct).map(function (s, i) {
|
|
384
|
+
return twice('<path class="visor__line visor__seg visor__seg--' + s + '" d="' + arcPiece(i / 4 + 0.006, (i + 1) / 4 - 0.006) + '"/>');
|
|
385
|
+
}).join('');
|
|
386
|
+
}
|
|
291
387
|
function frameSvg() {
|
|
292
|
-
return svgOpen('visor__frame visor__frame--top'
|
|
388
|
+
return svgOpen('visor__frame visor__frame--top' + (progress ? ' visor__frame--segs' : ''), '0 0 1000 100', true) +
|
|
389
|
+
twice('<path class="visor__line visor__arc" d="M76 100Q500 -100 924 100"/>') + arcSegs() + '</svg>' +
|
|
293
390
|
svgOpen('visor__frame visor__frame--foot', '0 0 1000 100', true) + twice('<path class="visor__line" d="M76 0Q500 200 924 0"/>') + '</svg>' +
|
|
294
391
|
svgOpen('visor__notch', '0 0 12 9') + twice('<path class="visor__line" d="M0 0l6 9l6 -9"/>') + '</svg>' +
|
|
295
392
|
svgOpen('visor__sight', '0 0 24 24') + twice('<circle class="visor__line" cx="12" cy="12" r="11"/>') + '<circle class="visor__dot" cx="12" cy="12" r="2"/></svg>';
|
|
@@ -320,8 +417,7 @@
|
|
|
320
417
|
var sprite = C && typeof C.portraitSrc === 'function' ? C.portraitSrc(id) : (C && typeof C.spriteSrc === 'function' ? C.spriteSrc(id) : '');
|
|
321
418
|
var n = credits();
|
|
322
419
|
return frameSvg() +
|
|
323
|
-
(scene.place ? '<p class="visor__place">' + esc(scene.place) + '</p>' : '') +
|
|
324
|
-
progressHtml() +
|
|
420
|
+
'<div class="visor__top">' + (scene.place ? '<p class="visor__place">' + esc(scene.place) + '</p>' : '') + progressHtml() + '</div>' +
|
|
325
421
|
focusHtml() +
|
|
326
422
|
'<div class="visor__foot">' +
|
|
327
423
|
'<a class="visor__me" data-visor-me href="' + esc(hallHref('/profile')) + '">' +
|
|
@@ -355,13 +451,12 @@
|
|
|
355
451
|
'</span>' +
|
|
356
452
|
'<span class="visor__card-open">Open your profile</span></span>';
|
|
357
453
|
}
|
|
358
|
-
// The
|
|
454
|
+
// The version's reading in words, beside the place; the four segments are the top arc
|
|
455
|
+
// (arcSegs, task 1005266), so the bars are never the only reading.
|
|
359
456
|
function progressHtml() {
|
|
360
457
|
if (!progress) return '';
|
|
361
|
-
var segs = segStates(progress.pct).map(function (s) { return '<li class="seg' + (s === 'todo' ? '' : ' seg--' + s) + '"></li>'; }).join('');
|
|
362
458
|
var said = progress.id + (progress.name ? ', ' + progress.name : '') + ': ' + progress.pct + '% built';
|
|
363
459
|
return '<div class="visor__progress" data-visor-progress title="' + esc(said) + '">' +
|
|
364
|
-
'<ol class="segs visor__segs" aria-hidden="true">' + segs + '</ol>' +
|
|
365
460
|
'<p class="visor__progress-words"><span class="sr-only">' + esc(said) + '</span>' +
|
|
366
461
|
'<span aria-hidden="true">' + esc(progress.id) + ' · ' + progress.pct + '%</span></p></div>';
|
|
367
462
|
}
|
|
@@ -397,8 +492,13 @@
|
|
|
397
492
|
root.setAttribute('role', 'region');
|
|
398
493
|
root.setAttribute('aria-label', 'Visor');
|
|
399
494
|
root.addEventListener('click', onClick);
|
|
495
|
+
root.addEventListener('focusin', function (e) { wake(); nameHint(e); });
|
|
496
|
+
root.addEventListener('focusout', nameHint);
|
|
497
|
+
root.addEventListener('mouseover', nameHint);
|
|
498
|
+
root.addEventListener('mouseout', nameHint);
|
|
400
499
|
document.body.appendChild(root);
|
|
401
500
|
}
|
|
501
|
+
paintRest();
|
|
402
502
|
var open = !!sheet;
|
|
403
503
|
closeSheet(false);
|
|
404
504
|
root.hidden = false;
|
|
@@ -475,6 +575,9 @@
|
|
|
475
575
|
function onClick(e) {
|
|
476
576
|
var t = e.target && e.target.closest ? e.target : null;
|
|
477
577
|
if (!t) return;
|
|
578
|
+
// A press that lands on a control you could not see wakes the glass and does nothing else;
|
|
579
|
+
// your picture is drawn at rest, so it is exempt.
|
|
580
|
+
if (resting() && !t.closest('[data-visor-me]')) { e.preventDefault(); wake(); return; }
|
|
478
581
|
if (t.closest('[data-visor-close]')) { closeSheet(true); return; }
|
|
479
582
|
if (t.closest('[data-visor-unfocus]')) { dropFocus(); return; }
|
|
480
583
|
if (t.closest('[data-visor-lead]')) { if (focus && focus.lead && !focus.lead.busy) { closeSheet(false); focus.lead.run(); } return; }
|
|
@@ -533,6 +636,7 @@
|
|
|
533
636
|
var hit = keyFor(character(), e);
|
|
534
637
|
if (!hit) return;
|
|
535
638
|
e.preventDefault();
|
|
639
|
+
wake(); // a visor key is reaching for it
|
|
536
640
|
if (hit === 'sheet') { if (sheet) closeSheet(true); else openSheet(); return; }
|
|
537
641
|
closeSheet(false);
|
|
538
642
|
run(hit);
|
|
@@ -540,6 +644,12 @@
|
|
|
540
644
|
|
|
541
645
|
function start() {
|
|
542
646
|
window.addEventListener('keydown', onKey);
|
|
647
|
+
// Resting and waking: all three listen in capture and are passive, so they see a press or
|
|
648
|
+
// a wheel a scene stops, and never slow or change one (ADR 0399 §2.1: the glass is not layout).
|
|
649
|
+
window.addEventListener('pointermove', onPointerMove, { capture: true, passive: true });
|
|
650
|
+
window.addEventListener('pointerdown', onPointerDown, { capture: true, passive: true });
|
|
651
|
+
window.addEventListener('wheel', onWheel, { capture: true, passive: true });
|
|
652
|
+
wake(ARRIVE_FOR);
|
|
543
653
|
// A scene whose script ran before this file left its registration here.
|
|
544
654
|
if (window.__visorScene && !scene) registerScene(window.__visorScene);
|
|
545
655
|
// The character and the credits settle when /me answers (craft-mode.js), after this file.
|
|
@@ -560,6 +670,11 @@
|
|
|
560
670
|
open: openSheet,
|
|
561
671
|
close: function () { closeSheet(true); },
|
|
562
672
|
isOpen: function () { return !!sheet; },
|
|
673
|
+
// resting and waking (task 1005266): a scene may wake the glass; the tests read and drive it
|
|
674
|
+
wake: wake,
|
|
675
|
+
rest: rest,
|
|
676
|
+
resting: resting,
|
|
677
|
+
arcPiece: arcPiece,
|
|
563
678
|
// pure parts, for the tests
|
|
564
679
|
ACTIONS: ACTIONS,
|
|
565
680
|
actionsFor: actionsFor,
|
|
@@ -6,6 +6,20 @@ Covers: `visor.js` + `visor.css`, loaded on Home, `/city`, `/goal-map` and `/thi
|
|
|
6
6
|
|
|
7
7
|
The visor is the control and the scene is the view: every main call to action in game mode is on the glass (the accent button, the key hints, the Actions sheet), and a scene carries none of its own. A scene that has something to do registers it with `OTBVisor.scene({ actions, keys })`. ADR 0403 §3 lists the scenes that do not keep it yet; the Governor City keeps it since task 1005209 the sky since task 1005210, and Home's gallery since task 1005211 (below).
|
|
8
8
|
|
|
9
|
+
## It rests while you look and wakes slim when you act (task 1005266, owner 2026-11-13)
|
|
10
|
+
|
|
11
|
+
The owner: the design and the theme are right, but the glass took too much of the view (231 of a laptop's 720px of height). He chose from a six-way mock-up (private page, linked on the task): rest by default, and a slimmer glass when you need commands. Where this section and an older one below disagree about a size or where a part sits, this section is current. No action, key, id or data source changed.
|
|
12
|
+
|
|
13
|
+
- **Awake, it is slimmer.** The top arc is the version's progress: the same curve in four pieces (`arcSegs`, coloured by `segStates`: built is the soft ink, being built is the character's colour, the rest faint). The place and the reading in words share one small line under it (`.visor__top`); the separate row of bars is gone. The foot row is smaller and sits lower: picture 40px, button 44px, credits at `--ds-text-4xl`. Insets: `--visor-arc` 44px, `--visor-inset-top` 48px, `--visor-inset-bottom` 36px, and the foot arc at `--visor-arc-foot`.
|
|
14
|
+
- **A key hint is its key alone.** Its name stays in the page for a screen reader; pointing at the key, or reaching it with the keyboard, shows the name on one plate above the row (`nameHint`, `.visor__hints[data-name]::after`), so naming a key never moves another. Tried first and dropped: widening the hint on hover, which moved the row under the pointer and lost the click. The place's lead action (`.visor__hint--place`) keeps its words: they are the place's call to you (ADR 0403).
|
|
15
|
+
- **At rest** (`.visor--rest`) only the arcs (one plain top arc), the sight and your picture are drawn. The rest fades out and stays in the page, so a key, the keyboard and a screen reader still reach it. Nothing moves, so waking is a fade.
|
|
16
|
+
- **It wakes** when the pointer is within 160px of the bottom of the view or on a control, on a visor key, when keyboard focus comes onto it, when a scene hands it a pick (`focus`), and for 4s on arriving at a place. **It stays awake** while the sheet or a pick's card is open, while the pointer is still there, and while the keyboard is on it. **It rests** at once on a press or a wheel on the scene (handing the keyboard back), and after 3s idle. A press that lands on a control you could not see only wakes it. A scene may call `OTBVisor.wake()`.
|
|
17
|
+
- **Only on a desk with a pointer** (`(hover: hover) and (pointer: fine)`, wider than 640px). A phone or tablet has nothing to reach with and keeps the whole glass; a phone's layout is as it was.
|
|
18
|
+
- **Scenes.** `html.visor-on` carries `--visor-drop` (48px; 0 on a phone): how far the foot row dropped. The sky's bottom offsets and the city's door subtract it; the sky's top offsets are 40px smaller.
|
|
19
|
+
- **Not done:** the sky's start panel and the city's wordmark were placed for the old top band and were not re-laid; arrow-key panning does not rest the glass (the idle timer does).
|
|
20
|
+
|
|
21
|
+
Tests: `tests/hall_visor.mjs` (rest, wake, the drag that is not a reach, the sheet holding it awake, the unseen control, the key's name, the arc's pieces, a phone).
|
|
22
|
+
|
|
9
23
|
## A window the glass opens opens in the glass (task 1005253, owner 2026-11-13)
|
|
10
24
|
|
|
11
25
|
- **The bug.** The options window and your profile window (`shell-game.js` `openOverlay`, `.opt-window`), and the game menu (`openMenu`, `.game-menu`, task 1005250), are reached from the visor, yet they opened as a box over the whole screen and covered the visor itself: the arcs, the place, your credits, the accent button and the hints.
|
|
@@ -339,7 +339,9 @@ module.exports = { build, blockVars };
|
|
|
339
339
|
|
|
340
340
|
if (require.main === module) {
|
|
341
341
|
const next = build();
|
|
342
|
-
|
|
342
|
+
// task 1005296: compare content, not line endings. A `core.autocrlf=true` checkout reads
|
|
343
|
+
// brand.html back as CRLF while build() is LF, which read as stale forever.
|
|
344
|
+
const current = fs.existsSync(OUT) ? fs.readFileSync(OUT, 'utf8').replace(/\r\n/g, '\n') : '';
|
|
343
345
|
if (process.argv.includes('--check')) {
|
|
344
346
|
if (current === next) process.stdout.write('brand.html: current\n');
|
|
345
347
|
else {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.27",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.25.
|
|
9
|
+
"version": "1.25.27",
|
|
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.25.
|
|
3
|
+
"version": "1.25.27",
|
|
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
|
@@ -11918,5 +11918,21 @@
|
|
|
11918
11918
|
"id": "1005283",
|
|
11919
11919
|
"text": "Someone with no GitHub account can now start a project. The creation page explains why GitHub is needed and offers a 'Make one free with Google' link. GitHub's own sign-up page opens, they continue with their Google account, a"
|
|
11920
11920
|
}
|
|
11921
|
+
],
|
|
11922
|
+
"1.25.26": [
|
|
11923
|
+
{
|
|
11924
|
+
"id": "1005266",
|
|
11925
|
+
"text": "In game mode the visor now steps out of the way while you look around and fades back in, slimmer than before, when you reach for a command."
|
|
11926
|
+
}
|
|
11927
|
+
],
|
|
11928
|
+
"1.25.27": [
|
|
11929
|
+
{
|
|
11930
|
+
"id": "1005296",
|
|
11931
|
+
"text": "The pre-ship health check no longer wrongly says the brand page and logo files are out of date on Windows computers."
|
|
11932
|
+
},
|
|
11933
|
+
{
|
|
11934
|
+
"id": "1005197",
|
|
11935
|
+
"text": "A hall check that always failed on Windows computers now works there, so it can again spot a page loading its scripts in the wrong order."
|
|
11936
|
+
}
|
|
11921
11937
|
]
|
|
11922
11938
|
}
|
|
@@ -35,6 +35,7 @@ const path = require('node:path');
|
|
|
35
35
|
const crypto = require('node:crypto');
|
|
36
36
|
const { readMarkSource, markParts, markFile, faviconSvg } = require('../../src/brand-mark');
|
|
37
37
|
const { escapeHtml } = require('../../src/html-escape');
|
|
38
|
+
const { sameContent } = require('./gen-freshness'); // eol-insensitive freshness (CRLF checkouts, task 1005296)
|
|
38
39
|
|
|
39
40
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
40
41
|
|
|
@@ -158,7 +159,7 @@ async function main() {
|
|
|
158
159
|
for (const [name, text] of Object.entries(files)) {
|
|
159
160
|
const file = path.join(dir, name);
|
|
160
161
|
const cur = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
|
161
|
-
if (cur
|
|
162
|
+
if (cur !== null && sameContent(cur, text)) continue;
|
|
162
163
|
stale.push(name);
|
|
163
164
|
if (!args.includes('--check')) fs.writeFileSync(file, text);
|
|
164
165
|
}
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.25.
|
|
84
|
+
const CORE_VERSION = '1.25.27'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
package/tests/brand_mark.mjs
CHANGED
|
@@ -95,7 +95,8 @@ test('the brand folder holds exactly what gen-brand.js writes from the source (l
|
|
|
95
95
|
const { brandFiles, packColors } = require('../scripts/bongos/gen-brand.js');
|
|
96
96
|
const files = brandFiles(read(BRAND_DIR, 'mark.svg'), packColors());
|
|
97
97
|
for (const [name, text] of Object.entries(files)) {
|
|
98
|
-
assert.equal(read(BRAND_DIR, name), text,
|
|
98
|
+
assert.equal(read(BRAND_DIR, name).replace(/\r\n/g, '\n'), text, // CRLF checkouts (task 1005296)
|
|
99
|
+
`${name} is stale — run node scripts/bongos/gen-brand.js`);
|
|
99
100
|
}
|
|
100
101
|
const c = packColors();
|
|
101
102
|
assert.match(files['mark-light.svg'], new RegExp(c.ink));
|
|
@@ -99,6 +99,7 @@ const GENERATOR_SOURCES = [
|
|
|
99
99
|
'modules/hall-ui/public/controls.css',
|
|
100
100
|
'modules/hall-ui/public/dom-utils.js',
|
|
101
101
|
'scripts/bongos/gen-brand.js',
|
|
102
|
+
'scripts/bongos/gen-freshness.js',
|
|
102
103
|
'src/brand-mark.js',
|
|
103
104
|
'src/html-escape.js',
|
|
104
105
|
'DESIGN.md',
|
|
@@ -135,6 +136,26 @@ test('the freshness gate passes on a fresh tree and fails when the token sheet c
|
|
|
135
136
|
}
|
|
136
137
|
});
|
|
137
138
|
|
|
139
|
+
// task 1005296: a `core.autocrlf=true` checkout (every default Windows clone) hands the
|
|
140
|
+
// generators CRLF copies of brand.html and the logo SVGs, while they build LF. Freshness is
|
|
141
|
+
// about content, so line endings alone must not read as stale — but a real edit still must.
|
|
142
|
+
test('the freshness gate ignores CRLF line endings on the brand page and the logo set', () => {
|
|
143
|
+
const dir = copiedTree();
|
|
144
|
+
const toCrlf = (f) => fs.writeFileSync(f, fs.readFileSync(f, 'utf8').replace(/\r\n/g, '\n').replace(/\n/g, '\r\n'));
|
|
145
|
+
try {
|
|
146
|
+
const page = path.join(dir, 'modules/ui-design/public/brand.html');
|
|
147
|
+
const brand = path.join(dir, 'modules/ui-design/public/brand');
|
|
148
|
+
toCrlf(page);
|
|
149
|
+
for (const f of fs.readdirSync(brand).filter((n) => n.endsWith('.svg'))) toCrlf(path.join(brand, f));
|
|
150
|
+
assert.ok(fs.readFileSync(page, 'utf8').includes('\r\n'), 'the brand page should now carry CRLF');
|
|
151
|
+
assert.deepEqual(checkDesignGeneratorsFresh(dir), [], 'CRLF alone must not read as stale');
|
|
152
|
+
fs.appendFileSync(page, '<!-- hand edit -->\r\n');
|
|
153
|
+
assert.ok(checkDesignGeneratorsFresh(dir).some((x) => x.startsWith('modules/ui-design/public/brand.html')), 'a real edit must still read as stale');
|
|
154
|
+
} finally {
|
|
155
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
138
159
|
// task 1005133: the logo set joins the gate. Editing the one source (mark.svg) without
|
|
139
160
|
// running gen-brand.js fails it; so does a PNG whose source page moved (the pack's ground)
|
|
140
161
|
// or a PNG swapped by hand — the PNGs are checked by hash, not just their pixel size.
|
|
@@ -48,7 +48,7 @@ function sources() {
|
|
|
48
48
|
// ── 1. generated ─────────────────────────────────────────────────────────────
|
|
49
49
|
|
|
50
50
|
test('the committed page is exactly what the generator writes', () => {
|
|
51
|
-
assert.equal(PAGE, gen.build(),
|
|
51
|
+
assert.equal(PAGE.replace(/\r\n/g, '\n'), gen.build(), // CRLF checkouts (task 1005296)
|
|
52
52
|
'modules/ui-design/public/brand.html is stale or hand-edited: run node modules/ui-design/scripts/gen-design-reference.js');
|
|
53
53
|
});
|
|
54
54
|
|