@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.
@@ -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.543",
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.543",
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.543",
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",