@bongos/core 1.19.543 → 1.19.545
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 +72 -22
- package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +115 -0
- package/docs/copy-inventory.md +37 -13
- package/docs/copy-registry.json +264 -18
- package/docs/module-api-changelog.md +4 -0
- package/docs/research/task-network-map/real-a-every-task.png +0 -0
- package/docs/research/task-network-map/real-b-dependency-forest.png +0 -0
- package/docs/research/task-network-map/real-c-goals.png +0 -0
- package/docs/research/task-network-map/real-d-joined-by-goal.png +0 -0
- package/modules/hall-ui/public/shell.js +30 -1
- package/modules/hall-ui/public/studio.css +268 -0
- package/modules/hall-ui/public/studio.html +74 -0
- package/modules/hall-ui/public/studio.js +213 -0
- package/modules/hall-ui/public/studio.states.json +54 -0
- package/modules/hall-ui/public/style.css +9 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/box.js +37 -3
- package/scripts/gds/spike-task-map.js +178 -9
- package/src/bongos/serve-internal.js +1 -1
- package/src/module-api.js +1 -1
- package/tests/box_env.mjs +64 -0
- package/tests/hall_nav.mjs +6 -1
- package/tests/hall_page_gate_map.mjs +5 -0
- package/tests/hall_studio_world.mjs +143 -0
- package/tests/spike_task_map.mjs +0 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/* modules/hall-ui/public/studio.css — the Studio (artist mode), /builders/studio.
|
|
2
|
+
R06 of goal 1000074 (the artist's loop), task 1003117.
|
|
3
|
+
|
|
4
|
+
The criterion (artist-mode): "a place an artist wants to be, not a form. A
|
|
5
|
+
focus environment on the Cloud Bongos screen — a lofi-girl-style zen garden of
|
|
6
|
+
focus and creativity — rather than another table of rows." So this sheet is
|
|
7
|
+
deliberately NOT the oversight row archetype the copy desk rides. It is a calm
|
|
8
|
+
room: one thing to tend at a time, generous air, tonal separation, and one
|
|
9
|
+
slow ambient presence — the warm "lamp" that gives a lofi room its light.
|
|
10
|
+
|
|
11
|
+
THE ONE WARM THING RULE, kept (DESIGN.md): the salmon accent lights only what
|
|
12
|
+
is pressable (the primary action) or LIVE. The ambient lamp is the live thing
|
|
13
|
+
— a slow-breathing warm presence, the room's own light — and nothing else here
|
|
14
|
+
is warm: the eyebrows, the step numbers and the stones are ink, never accent.
|
|
15
|
+
|
|
16
|
+
Tokens only — no colour literals (tests/hall_audit.mjs §9 holds every hall
|
|
17
|
+
sheet to that), so every colour is a var() or a color-mix() of one, and both
|
|
18
|
+
modes come for free from style.css's tier. The mode marker html[data-artist]
|
|
19
|
+
is set on this page's <html> (the social-mode pattern, shell.js applySocial):
|
|
20
|
+
entering the studio IS entering the mode, so the calm reads off the attribute. */
|
|
21
|
+
|
|
22
|
+
/* The room. Generous vertical air so the one focus sits in space, not in a list.
|
|
23
|
+
position:relative anchors the ambient layer below it. */
|
|
24
|
+
.studio {
|
|
25
|
+
position: relative;
|
|
26
|
+
padding: clamp(28px, 6vh, 76px) 0 64px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* The ambient presence — the lamp. A single soft warm disc, low and slow, the
|
|
30
|
+
only atmosphere the room draws (the hall's --plate ground is a deferred seam,
|
|
31
|
+
owner's call, so nothing is painted into it here). overflow:hidden clips the
|
|
32
|
+
disc to the room so it can never widen the document (the no-sideways rule). */
|
|
33
|
+
.studio-atmos {
|
|
34
|
+
position: absolute;
|
|
35
|
+
inset: 0;
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
pointer-events: none;
|
|
38
|
+
z-index: 0;
|
|
39
|
+
}
|
|
40
|
+
.studio-atmos::before {
|
|
41
|
+
content: "";
|
|
42
|
+
position: absolute;
|
|
43
|
+
top: -24%;
|
|
44
|
+
right: -10%;
|
|
45
|
+
width: min(680px, 82vw);
|
|
46
|
+
aspect-ratio: 1;
|
|
47
|
+
border-radius: 50%;
|
|
48
|
+
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
|
49
|
+
animation: studio-lamp 38s ease-in-out infinite;
|
|
50
|
+
}
|
|
51
|
+
/* A second, cooler and fainter presence low-left, so the room has depth rather
|
|
52
|
+
than one lamp in a corner. --glass is the world's tonal wash token (translucent
|
|
53
|
+
ink on paper in light, translucent white on black in dark) — the sanctioned way
|
|
54
|
+
to lay a neutral wash that reads right in both modes, never a raw --ink mix into
|
|
55
|
+
transparent (which inverts; tests/hall_audit.mjs bans it). */
|
|
56
|
+
.studio-atmos::after {
|
|
57
|
+
content: "";
|
|
58
|
+
position: absolute;
|
|
59
|
+
bottom: -30%;
|
|
60
|
+
left: -14%;
|
|
61
|
+
width: min(560px, 72vw);
|
|
62
|
+
aspect-ratio: 1;
|
|
63
|
+
border-radius: 50%;
|
|
64
|
+
background: var(--glass);
|
|
65
|
+
animation: studio-lamp 52s ease-in-out infinite reverse;
|
|
66
|
+
}
|
|
67
|
+
/* Long-period, transform/opacity only, and dead under the shell's global
|
|
68
|
+
reduced-motion kill switch (style.css) — the DESIGN.md rule for ambient loops. */
|
|
69
|
+
@keyframes studio-lamp {
|
|
70
|
+
0%, 100% { transform: scale(1); opacity: .85; }
|
|
71
|
+
50% { transform: scale(1.08); opacity: 1; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Everything the reader acts on sits above the lamp. */
|
|
75
|
+
.studio-head,
|
|
76
|
+
.studio-focus,
|
|
77
|
+
.studio-rest { position: relative; z-index: 1; }
|
|
78
|
+
|
|
79
|
+
/* The head: a mono eyebrow naming the mode, then the greeting as the display
|
|
80
|
+
voice (Manrope light, tight), then the live count as a quiet sub-line. */
|
|
81
|
+
.studio-head { margin-bottom: clamp(22px, 4vh, 40px); max-width: var(--measure); }
|
|
82
|
+
.studio-eyebrow {
|
|
83
|
+
margin: 0 0 10px;
|
|
84
|
+
font-family: var(--font-mono);
|
|
85
|
+
font-size: 11px;
|
|
86
|
+
letter-spacing: .14em;
|
|
87
|
+
text-transform: uppercase;
|
|
88
|
+
color: var(--ink-faint);
|
|
89
|
+
}
|
|
90
|
+
.studio-greet {
|
|
91
|
+
margin: 0 0 8px;
|
|
92
|
+
font-weight: 300;
|
|
93
|
+
font-size: clamp(28px, 4.4vw, 46px);
|
|
94
|
+
line-height: 1.04;
|
|
95
|
+
letter-spacing: -.028em;
|
|
96
|
+
color: var(--ink);
|
|
97
|
+
}
|
|
98
|
+
.studio-sub {
|
|
99
|
+
margin: 0;
|
|
100
|
+
font-size: 15.5px;
|
|
101
|
+
line-height: 1.5;
|
|
102
|
+
color: var(--ink-soft);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* The focus — the one thing to tend now. Tone separates it from the room (the
|
|
106
|
+
v3 hall rule: a --bg-card panel at the full radius, no drawn edge), and it is
|
|
107
|
+
held to a reading width so it stays a card and not a banner. */
|
|
108
|
+
.studio-focus {
|
|
109
|
+
max-width: 720px;
|
|
110
|
+
background: var(--bg-card);
|
|
111
|
+
border-radius: var(--radius);
|
|
112
|
+
padding: clamp(22px, 3.2vw, 34px);
|
|
113
|
+
}
|
|
114
|
+
.studio-card__eyebrow {
|
|
115
|
+
margin: 0 0 14px;
|
|
116
|
+
font-family: var(--font-mono);
|
|
117
|
+
font-size: 11px;
|
|
118
|
+
letter-spacing: .12em;
|
|
119
|
+
text-transform: uppercase;
|
|
120
|
+
color: var(--ink-faint);
|
|
121
|
+
}
|
|
122
|
+
.studio-card__title {
|
|
123
|
+
margin: 0 0 14px;
|
|
124
|
+
font-weight: 300;
|
|
125
|
+
font-size: clamp(20px, 2.6vw, 26px);
|
|
126
|
+
line-height: 1.2;
|
|
127
|
+
letter-spacing: -.02em;
|
|
128
|
+
color: var(--ink);
|
|
129
|
+
}
|
|
130
|
+
/* The product's own words, shown as evidence for a flag — mono, wrapping
|
|
131
|
+
preserved (leading/trailing space is often exactly what is wrong), in a soft
|
|
132
|
+
tonal well so the quote reads as quotation, not as this room's own voice. */
|
|
133
|
+
.studio-card__quote {
|
|
134
|
+
display: block;
|
|
135
|
+
margin: 0 0 16px;
|
|
136
|
+
padding: 12px 14px;
|
|
137
|
+
border-radius: var(--radius-sm);
|
|
138
|
+
background: var(--glass);
|
|
139
|
+
font-family: var(--font-mono);
|
|
140
|
+
font-size: 13.5px;
|
|
141
|
+
line-height: 1.5;
|
|
142
|
+
color: var(--ink);
|
|
143
|
+
white-space: pre-wrap;
|
|
144
|
+
overflow-wrap: anywhere;
|
|
145
|
+
}
|
|
146
|
+
.studio-card__context {
|
|
147
|
+
margin: 0 0 18px;
|
|
148
|
+
font-size: 14.5px;
|
|
149
|
+
line-height: 1.55;
|
|
150
|
+
color: var(--ink-soft);
|
|
151
|
+
max-width: var(--measure);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* The two steps of a review (R04): coach the grader, then remake or tweak. Not
|
|
155
|
+
a checklist widget — a gentle reminder of the shape of the work, so the room
|
|
156
|
+
describes the craft without becoming the form the criterion warns against. */
|
|
157
|
+
.studio-steps {
|
|
158
|
+
list-style: none;
|
|
159
|
+
margin: 0 0 20px;
|
|
160
|
+
padding: 0;
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-direction: column;
|
|
163
|
+
gap: 12px;
|
|
164
|
+
}
|
|
165
|
+
.studio-step {
|
|
166
|
+
display: flex;
|
|
167
|
+
gap: 12px;
|
|
168
|
+
align-items: baseline;
|
|
169
|
+
font-size: 14.5px;
|
|
170
|
+
line-height: 1.5;
|
|
171
|
+
color: var(--ink);
|
|
172
|
+
}
|
|
173
|
+
.studio-step__n {
|
|
174
|
+
flex: none;
|
|
175
|
+
min-width: 16px;
|
|
176
|
+
font-family: var(--font-mono);
|
|
177
|
+
font-size: 12px;
|
|
178
|
+
color: var(--ink-faint);
|
|
179
|
+
}
|
|
180
|
+
.studio-step small {
|
|
181
|
+
display: block;
|
|
182
|
+
margin-top: 2px;
|
|
183
|
+
font-size: 13px;
|
|
184
|
+
color: var(--ink-faint);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* The one place the accent is spent on a control. Ghost is the calm pass —
|
|
188
|
+
"let it be for now" advances to the next piece without changing anything. */
|
|
189
|
+
.studio-card__actions {
|
|
190
|
+
display: flex;
|
|
191
|
+
flex-wrap: wrap;
|
|
192
|
+
gap: 10px;
|
|
193
|
+
align-items: center;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* What else waits — a gentle line and a row of stones, one per remaining piece
|
|
197
|
+
(the zen garden), never a table. Non-interactive on purpose: the room asks the
|
|
198
|
+
reader to tend one thing, not to scan a queue. */
|
|
199
|
+
.studio-rest {
|
|
200
|
+
margin-top: clamp(20px, 3.5vh, 34px);
|
|
201
|
+
max-width: 720px;
|
|
202
|
+
display: flex;
|
|
203
|
+
flex-wrap: wrap;
|
|
204
|
+
align-items: center;
|
|
205
|
+
gap: 14px;
|
|
206
|
+
}
|
|
207
|
+
.studio-rest__line {
|
|
208
|
+
margin: 0;
|
|
209
|
+
font-size: 13.5px;
|
|
210
|
+
color: var(--ink-faint);
|
|
211
|
+
}
|
|
212
|
+
.studio-stones {
|
|
213
|
+
display: flex;
|
|
214
|
+
flex-wrap: wrap;
|
|
215
|
+
gap: 9px;
|
|
216
|
+
align-items: center;
|
|
217
|
+
}
|
|
218
|
+
.studio-stone {
|
|
219
|
+
width: 11px;
|
|
220
|
+
height: 11px;
|
|
221
|
+
border-radius: 50%;
|
|
222
|
+
background: var(--ink-faint);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* Where the full queues live — the calm room is the front door; the tables it
|
|
226
|
+
replaces stay one link away for when the reader wants the whole list. */
|
|
227
|
+
.studio-links {
|
|
228
|
+
position: relative;
|
|
229
|
+
z-index: 1;
|
|
230
|
+
margin-top: clamp(26px, 4vh, 44px);
|
|
231
|
+
padding-top: 18px;
|
|
232
|
+
border-top: 1px solid var(--rule-soft);
|
|
233
|
+
display: flex;
|
|
234
|
+
flex-wrap: wrap;
|
|
235
|
+
gap: 8px 22px;
|
|
236
|
+
max-width: 720px;
|
|
237
|
+
font-size: 13.5px;
|
|
238
|
+
color: var(--ink-faint);
|
|
239
|
+
}
|
|
240
|
+
.studio-links a { color: var(--accent-ink); }
|
|
241
|
+
|
|
242
|
+
/* The empty room — a good sign, said plainly, not a dead panel. */
|
|
243
|
+
.studio-empty {
|
|
244
|
+
max-width: 720px;
|
|
245
|
+
background: var(--bg-card);
|
|
246
|
+
border-radius: var(--radius);
|
|
247
|
+
padding: clamp(24px, 3.4vw, 36px);
|
|
248
|
+
}
|
|
249
|
+
.studio-empty h2 {
|
|
250
|
+
margin: 0 0 8px;
|
|
251
|
+
font-weight: 300;
|
|
252
|
+
font-size: clamp(19px, 2.4vw, 24px);
|
|
253
|
+
letter-spacing: -.02em;
|
|
254
|
+
color: var(--ink);
|
|
255
|
+
}
|
|
256
|
+
.studio-empty p {
|
|
257
|
+
margin: 0;
|
|
258
|
+
font-size: 14.5px;
|
|
259
|
+
line-height: 1.55;
|
|
260
|
+
color: var(--ink-soft);
|
|
261
|
+
max-width: var(--measure);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Phone: the room keeps its air but the head and cards take the full width. */
|
|
265
|
+
@media (max-width: 640px) {
|
|
266
|
+
.studio { padding-top: 20px; }
|
|
267
|
+
.studio-card__actions { gap: 8px; }
|
|
268
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!-- data-artist="on" is the studio's MODE marker (shell.js applySocial's pattern,
|
|
3
|
+
set here statically so entering the room IS entering the mode, with no paint
|
|
4
|
+
flash). It is the surface half of goal 1000074's argument: the artist's craft
|
|
5
|
+
gets its own place, not one more table on the engineer's screen. -->
|
|
6
|
+
<html lang="en" data-artist="on">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="utf-8">
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
10
|
+
<title>Studio — {{worldName}}</title>
|
|
11
|
+
<meta name="description" content="Artist mode — a calm place to tend the words and art the project shows people, one piece at a time: the review that shipped, and the copy someone flagged.">
|
|
12
|
+
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
13
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
14
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
15
|
+
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
16
|
+
<link rel="stylesheet" href="/builders/studio.css">
|
|
17
|
+
</head>
|
|
18
|
+
<body data-page="studio" data-page-title="Studio">
|
|
19
|
+
|
|
20
|
+
<div class="app">
|
|
21
|
+
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
22
|
+
<div class="app-scrim" id="app-scrim" hidden></div>
|
|
23
|
+
<div class="app-body">
|
|
24
|
+
<header class="app-topbar" id="app-topbar"></header>
|
|
25
|
+
<div class="app-content">
|
|
26
|
+
|
|
27
|
+
<!-- The studio is a room, not a board, so it carries its OWN head voice
|
|
28
|
+
(a greeting in the display face) rather than the quiet .page-head--board
|
|
29
|
+
a working table uses. The static h1 is the no-JS / signed-out fallback;
|
|
30
|
+
studio.js rewrites it to "Good evening, <name>" once /me answers, and
|
|
31
|
+
fills the sub-line with what waits. It is the page's one <h1>. -->
|
|
32
|
+
<main id="hall-main" class="studio">
|
|
33
|
+
<div class="studio-atmos" aria-hidden="true"></div>
|
|
34
|
+
|
|
35
|
+
<header class="studio-head">
|
|
36
|
+
<p class="studio-eyebrow">artist mode</p>
|
|
37
|
+
<h1 class="studio-greet" id="studio-greet">The studio</h1>
|
|
38
|
+
<p class="studio-sub" id="studio-sub">A quiet place to tend the words and art the project shows people.</p>
|
|
39
|
+
</header>
|
|
40
|
+
|
|
41
|
+
<!-- The one thing to tend now. Rendered by studio.js; the markup here is
|
|
42
|
+
the loading state (and the no-JS fallback), never an empty panel. -->
|
|
43
|
+
<section class="studio-focus" id="studio-focus" aria-live="polite">
|
|
44
|
+
<div class="studio-empty">
|
|
45
|
+
<h2>Opening the studio…</h2>
|
|
46
|
+
<p>Gathering the reviews and flagged copy that wait for you.</p>
|
|
47
|
+
</div>
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
<!-- What else waits — the garden's remaining stones. Hidden until there
|
|
51
|
+
is more than the one in focus. -->
|
|
52
|
+
<div class="studio-rest" id="studio-rest" hidden></div>
|
|
53
|
+
|
|
54
|
+
<!-- The full tables the room replaces stay one link away, for when the
|
|
55
|
+
reader wants the whole list rather than one piece at a time. -->
|
|
56
|
+
<nav class="studio-links" aria-label="The full queues">
|
|
57
|
+
<a href="/builders/copy-desk">The copy desk — every flagged string</a>
|
|
58
|
+
<a href="/builders/work">The task board — all work</a>
|
|
59
|
+
</nav>
|
|
60
|
+
</main>
|
|
61
|
+
|
|
62
|
+
<footer class="app-foot" id="app-foot"></footer>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
68
|
+
|
|
69
|
+
<script src="/builders/shell.js?v=2026-07-02-shell1"></script>
|
|
70
|
+
<script src="/builders/palette.js" defer></script>
|
|
71
|
+
<script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
|
|
72
|
+
<script src="/builders/studio.js?v=2026-09-04-studio1"></script>
|
|
73
|
+
</body>
|
|
74
|
+
</html>
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// modules/hall-ui/public/studio.js — the Studio (artist mode), /builders/studio.
|
|
2
|
+
// R06 of goal 1000074 (the artist's loop), task 1003117.
|
|
3
|
+
//
|
|
4
|
+
// The criterion (artist-mode): "a place an artist wants to be, not a form. A
|
|
5
|
+
// focus environment on the Cloud Bongos screen — a lofi-girl-style zen garden of
|
|
6
|
+
// focus and creativity — rather than another table of rows."
|
|
7
|
+
//
|
|
8
|
+
// So this is NOT a queue table. It aggregates the artist's real work — the two
|
|
9
|
+
// surfaces the rest of goal 1000074 built — and presents it ONE PIECE AT A TIME,
|
|
10
|
+
// a stone to tend in a calm garden, with the rest waiting quietly:
|
|
11
|
+
// 1. REVIEW REQUESTS (R04, cascade.js) — a copy/visual ship files a two-step
|
|
12
|
+
// artist review at `backlog`, discipline 'artist', source 'cascade'. The
|
|
13
|
+
// two steps (coach the grader, then remake/tweak) live on the task record;
|
|
14
|
+
// this room names them and opens the door.
|
|
15
|
+
// 2. FLAGGED COPY (R02, copy-desk) — open flags over the words the product
|
|
16
|
+
// shows people. Best-effort: an instance without the copy-desk module has
|
|
17
|
+
// no queue, and the room degrades to reviews alone rather than erroring.
|
|
18
|
+
//
|
|
19
|
+
// It writes NOTHING and gates NOTHING (ADR 0162 / the goal's no-live-CMS rule):
|
|
20
|
+
// "open this" routes to where the craft is actually done (the task record, the
|
|
21
|
+
// copy desk), and "let it be for now" only advances the reader to the next
|
|
22
|
+
// piece. The full tables it replaces stay one link away in the footer.
|
|
23
|
+
|
|
24
|
+
(() => {
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const API = '/api/bongos';
|
|
28
|
+
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
29
|
+
const { escapeHtml, $ } = window.OTB;
|
|
30
|
+
|
|
31
|
+
let viewer = null;
|
|
32
|
+
let pieces = []; // the combined garden: [{ type:'review'|'flag', … }]
|
|
33
|
+
let idx = 0; // which stone is in focus
|
|
34
|
+
|
|
35
|
+
// Bound the reviews read. cascade.js files a review per qualifying ship and they
|
|
36
|
+
// sit at backlog until claimed or dismissed ("dismissable rather than nagging"),
|
|
37
|
+
// so this queue grows without bound over the project's life — the unbounded-read
|
|
38
|
+
// shape modules/hall-ui/CLAUDE.md flags as a repeat bug (the Work board's backlog,
|
|
39
|
+
// Home's next-up card, the Goals rollup all had it). 50 is far more than a focus
|
|
40
|
+
// queue tends at once; the footer links to the copy desk and board are the
|
|
41
|
+
// whole-list path. listTasks orders `priority NULLS LAST, created_at`, so the cap
|
|
42
|
+
// keeps the most-prioritised waiting reviews.
|
|
43
|
+
const REVIEW_LIMIT = 50;
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// helpers
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
function greeting() {
|
|
50
|
+
const h = new Date().getHours();
|
|
51
|
+
if (h < 5) return 'Still up';
|
|
52
|
+
if (h < 12) return 'Good morning';
|
|
53
|
+
if (h < 18) return 'Good afternoon';
|
|
54
|
+
return 'Good evening';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The shipped task a review is ABOUT — cascade.js stamps
|
|
58
|
+
// source_ref = 'cascade-artist-review-on-ship-<taskId>'.
|
|
59
|
+
function reviewShippedId(t) {
|
|
60
|
+
const m = /-on-ship-(\d+)/.exec(t.source_ref || '');
|
|
61
|
+
return m ? m[1] : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The product's own words, shown as evidence — untrusted source-tree text, so
|
|
65
|
+
// escaped, and clipped so one runaway string cannot fill the room.
|
|
66
|
+
function quote(text, max = 300) {
|
|
67
|
+
const s = String(text == null ? '' : text);
|
|
68
|
+
return escapeHtml(s.length > max ? `${s.slice(0, max)}…` : s);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Clip a plain field (a name, a reason) so one very long value cannot stretch
|
|
72
|
+
// the card — the mirror of quote()'s clip, for text that is not the evidence.
|
|
73
|
+
// Escaping still happens at render; this only bounds length.
|
|
74
|
+
function clip(text, max) {
|
|
75
|
+
const s = String(text == null ? '' : text);
|
|
76
|
+
return s.length > max ? `${s.slice(0, max)}…` : s;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// the focus card, one shape per kind of work
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
function reviewCard(p, n) {
|
|
84
|
+
const title = p.shippedId
|
|
85
|
+
? `The copy and visuals that shipped in task #${escapeHtml(p.shippedId)}`
|
|
86
|
+
: escapeHtml(p.title || 'A review of what shipped');
|
|
87
|
+
return `
|
|
88
|
+
<p class="studio-card__eyebrow">to tend now · ${escapeHtml(n)}</p>
|
|
89
|
+
<h2 class="studio-card__title">${title}</h2>
|
|
90
|
+
<p class="studio-card__context">It shipped and it is live. Your review coaches the grader on how it judged the work, then remakes or tweaks the words or art only if they need it — it never blocks what shipped.</p>
|
|
91
|
+
<ol class="studio-steps">
|
|
92
|
+
<li class="studio-step"><span class="studio-step__n">01</span><span>Coach the grader<small>Review how it judged this work, and teach it what good looked like.</small></span></li>
|
|
93
|
+
<li class="studio-step"><span class="studio-step__n">02</span><span>Remake or tweak<small>Only if it needs it — with the AI at your side, or on your own.</small></span></li>
|
|
94
|
+
</ol>
|
|
95
|
+
<div class="studio-card__actions">
|
|
96
|
+
<a class="btn-accent" href="/builders/task/${escapeHtml(String(p.taskId))}">Open this review</a>
|
|
97
|
+
<button type="button" class="btn-ghost" data-skip>Let it be for now</button>
|
|
98
|
+
</div>`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function flagCard(p, n) {
|
|
102
|
+
const who = p.who ? `${escapeHtml(p.who)} flagged this` : 'Someone flagged this';
|
|
103
|
+
const reason = p.reason ? ` — ${escapeHtml(p.reason)}` : '';
|
|
104
|
+
return `
|
|
105
|
+
<p class="studio-card__eyebrow">to tend now · ${escapeHtml(n)}</p>
|
|
106
|
+
<blockquote class="studio-card__quote">${quote(p.text)}</blockquote>
|
|
107
|
+
<p class="studio-card__context">${who}${reason}</p>
|
|
108
|
+
<div class="studio-card__actions">
|
|
109
|
+
<a class="btn-accent" href="/builders/copy-desk">Open in the copy desk</a>
|
|
110
|
+
<button type="button" class="btn-ghost" data-skip>Let it be for now</button>
|
|
111
|
+
</div>`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function emptyCard(done) {
|
|
115
|
+
const heading = done ? 'You have tended everything waiting' : 'Nothing waits right now';
|
|
116
|
+
const body = done
|
|
117
|
+
? 'That is the whole garden. Come back when work ships, or start again from the top.'
|
|
118
|
+
: 'A quiet moment — either good news or an unread queue. When work with copy or visuals ships, a review appears here for you.';
|
|
119
|
+
const restart = done
|
|
120
|
+
? '<div class="studio-card__actions studio-empty__actions"><button type="button" class="btn-ghost" data-restart>Start over</button></div>'
|
|
121
|
+
: '';
|
|
122
|
+
return `<div class="studio-empty"><h2>${heading}</h2><p>${body}</p>${restart}</div>`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// paint
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
function paintHead() {
|
|
130
|
+
const name = viewer && (viewer.display_name || viewer.github_login);
|
|
131
|
+
$('#studio-greet').textContent = name ? `${greeting()}, ${name}` : greeting();
|
|
132
|
+
const n = pieces.length;
|
|
133
|
+
$('#studio-sub').textContent = n
|
|
134
|
+
? `${n} ${n === 1 ? 'piece' : 'pieces'} of the product's voice ${n === 1 ? 'waits' : 'wait'} for you. Tend them one at a time — or let them be.`
|
|
135
|
+
: 'Nothing waits right now. Enjoy the quiet.';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function paint() {
|
|
139
|
+
const focus = $('#studio-focus');
|
|
140
|
+
const rest = $('#studio-rest');
|
|
141
|
+
|
|
142
|
+
if (!pieces.length) { focus.innerHTML = emptyCard(false); rest.hidden = true; return; }
|
|
143
|
+
if (idx >= pieces.length) { focus.innerHTML = emptyCard(true); rest.hidden = true; return; }
|
|
144
|
+
|
|
145
|
+
const p = pieces[idx];
|
|
146
|
+
focus.innerHTML = p.type === 'review' ? reviewCard(p, `${idx + 1} of ${pieces.length}`) : flagCard(p, `${idx + 1} of ${pieces.length}`);
|
|
147
|
+
|
|
148
|
+
// The rest of the garden: one stone per remaining piece, quiet and
|
|
149
|
+
// non-interactive — the room asks the reader to tend one thing, not to scan.
|
|
150
|
+
const remaining = pieces.length - (idx + 1);
|
|
151
|
+
if (remaining > 0) {
|
|
152
|
+
const shown = Math.min(remaining, 14);
|
|
153
|
+
const stones = new Array(shown).fill('<span class="studio-stone"></span>').join('');
|
|
154
|
+
const extra = remaining > shown ? ` +${remaining - shown}` : '';
|
|
155
|
+
rest.innerHTML = `<span class="studio-stones" aria-hidden="true">${stones}</span>`
|
|
156
|
+
+ `<span class="studio-rest__line">and ${remaining} more waiting${extra}</span>`;
|
|
157
|
+
rest.hidden = false;
|
|
158
|
+
} else {
|
|
159
|
+
rest.hidden = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// load
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
// The waiting review requests: backlog is where cascade.js files them and where
|
|
168
|
+
// they sit until claimed or dismissed ("dismissable rather than nagging"), so
|
|
169
|
+
// status=backlog is the bounded, correct read for what is waiting to be tended.
|
|
170
|
+
async function loadReviews() {
|
|
171
|
+
const res = await api.request('GET', `${API}/tasks?discipline=artist&status=backlog&limit=${REVIEW_LIMIT}`);
|
|
172
|
+
if (!res.ok) return [];
|
|
173
|
+
const tasks = (res.data && res.data.tasks) || [];
|
|
174
|
+
return tasks
|
|
175
|
+
.filter((t) => t.source === 'cascade' || /^artist review\b/i.test(t.title || ''))
|
|
176
|
+
.sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0))
|
|
177
|
+
.map((t) => ({ type: 'review', taskId: t.id, title: t.title, shippedId: reviewShippedId(t) }));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// The open flags. Best-effort: no copy-desk module → 404 → no flags, and the
|
|
181
|
+
// room is reviews alone rather than an error.
|
|
182
|
+
async function loadFlags() {
|
|
183
|
+
const res = await api.request('GET', `${API}/copy-desk/queue?status=open`);
|
|
184
|
+
if (!res.ok) return [];
|
|
185
|
+
// The server already filters to open flags (?status=open); no client re-filter.
|
|
186
|
+
const flags = (res.data && res.data.flags) || [];
|
|
187
|
+
return flags.map((f) => ({
|
|
188
|
+
type: 'flag',
|
|
189
|
+
text: f.scope === 'surface' ? f.surface : (f.current ? f.current.text : f.text_at_flag),
|
|
190
|
+
reason: clip(f.reason, 240),
|
|
191
|
+
who: clip(f.flagged_by_name || f.flagged_by_login || '', 60) || null,
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// wire + boot
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
document.addEventListener('click', (ev) => {
|
|
200
|
+
if (ev.target.closest('[data-skip]')) { idx += 1; paint(); window.scrollTo({ top: 0 }); return; }
|
|
201
|
+
if (ev.target.closest('[data-restart]')) { idx = 0; paint(); window.scrollTo({ top: 0 }); }
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
(async () => {
|
|
205
|
+
const me = await api.request('GET', `${API}/me`);
|
|
206
|
+
if (me.ok) viewer = me.data.builder || me.data;
|
|
207
|
+
// Reviews first (the richer craft), then flags — each newest-first within.
|
|
208
|
+
const [reviews, flags] = await Promise.all([loadReviews(), loadFlags()]);
|
|
209
|
+
pieces = [...reviews, ...flags];
|
|
210
|
+
paintHead();
|
|
211
|
+
paint();
|
|
212
|
+
})();
|
|
213
|
+
})();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The Studio (artist mode), R06 of goal 1000074 (task 1003117). A calm room that surfaces the artist's real work — backlog review requests (cascade, R04) and open copy flags (R02) — one piece at a time, never a table. Against the kit's own stub the page renders its EMPTY state (no /tasks or /copy-desk feed), which is a state the floors must hold in; against the hall-preview harness with the studio__reviews / copy-desk__queue fixtures it renders the focus card and the garden's stones.",
|
|
3
|
+
"page": "/builders/studio",
|
|
4
|
+
"surface": "hall-ui",
|
|
5
|
+
"stub": {
|
|
6
|
+
"prefix": "/builders/"
|
|
7
|
+
},
|
|
8
|
+
"modeQuery": false,
|
|
9
|
+
"ignoreRequests": [
|
|
10
|
+
"/api/(gds|bongos)/me$",
|
|
11
|
+
"/api/(gds|bongos)/me/",
|
|
12
|
+
"/api/(gds|bongos)/tasks",
|
|
13
|
+
"/api/(gds|bongos)/copy-desk/"
|
|
14
|
+
],
|
|
15
|
+
"ignoreConsole": [
|
|
16
|
+
"/api/(gds|bongos)/",
|
|
17
|
+
"\\b401\\b",
|
|
18
|
+
"\\b403\\b",
|
|
19
|
+
"\\b404\\b",
|
|
20
|
+
"\\b501\\b",
|
|
21
|
+
"Unauthorized",
|
|
22
|
+
"Forbidden",
|
|
23
|
+
"Not Found",
|
|
24
|
+
"load failed"
|
|
25
|
+
],
|
|
26
|
+
"states": {
|
|
27
|
+
"focus": {
|
|
28
|
+
"auth": true,
|
|
29
|
+
"actions": [
|
|
30
|
+
[
|
|
31
|
+
"wait",
|
|
32
|
+
700
|
|
33
|
+
]
|
|
34
|
+
],
|
|
35
|
+
"expect": {
|
|
36
|
+
"visible": [
|
|
37
|
+
"#studio-focus"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"out": {
|
|
42
|
+
"auth": false
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"reducedMotion": {
|
|
46
|
+
"auth": true,
|
|
47
|
+
"actions": [
|
|
48
|
+
[
|
|
49
|
+
"wait",
|
|
50
|
+
600
|
|
51
|
+
]
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -658,6 +658,15 @@ main:focus { outline: none; }
|
|
|
658
658
|
.nav-item__label { display: none; font-size: 14px; }
|
|
659
659
|
.nav-icon { width: 16px; height: 16px; flex-shrink: 0; }
|
|
660
660
|
|
|
661
|
+
/* task 1003117 (R06 of goal 1000074): the Studio entry is warmed for a builder
|
|
662
|
+
whose craft is artist — the "emphasized entry for artist-craft builders" half
|
|
663
|
+
of artist mode. Distinct from --active (the current page's leading accent bar):
|
|
664
|
+
this warms the GLYPH, so the emphasis reads in the collapsed icon-only rail
|
|
665
|
+
too. accent-ink is the text-safe accent in both modes. shell.js sets the class
|
|
666
|
+
from /me's preferred_disciplines; it is cosmetic (the page is open to all). */
|
|
667
|
+
.nav-item--craft .nav-icon,
|
|
668
|
+
.nav-item--craft .nav-item__label { color: var(--accent-ink); }
|
|
669
|
+
|
|
661
670
|
.app-sidebar__foot {
|
|
662
671
|
padding: 10px 0;
|
|
663
672
|
border-top: 1px solid var(--glass-edge);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.545",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.545",
|
|
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.19.
|
|
3
|
+
"version": "1.19.545",
|
|
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",
|