@bongos/core 1.19.1067 → 1.19.1069
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 +73 -58
- package/docs/copy-inventory.md +74 -53
- package/docs/copy-registry.json +310 -101
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +10 -2
- package/docs/page-readings.json +566 -536
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +3 -3
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +23 -32
- package/modules/hall-ui/public/studio-lib.js +207 -0
- package/modules/hall-ui/public/studio.css +559 -170
- package/modules/hall-ui/public/studio.html +143 -49
- package/modules/hall-ui/public/studio.js +442 -33
- package/modules/hall-ui/public/studio.states.json +164 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/studio.md +47 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/hall-preview/server.js +5 -0
- package/src/module-api.js +1 -1
- package/tests/hall_audit.mjs +4 -0
- package/tests/hall_nav.mjs +25 -1
- package/tests/hall_page_gate_map.mjs +2 -0
- package/tests/hall_studio_home.mjs +429 -0
- package/tests/hall_studio_world.mjs +7 -1
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
|
@@ -1,33 +1,48 @@
|
|
|
1
|
-
// modules/hall-ui/public/studio.js — the
|
|
2
|
-
// R06 of goal 1000074 (the artist's loop), task 1003117.
|
|
1
|
+
// modules/hall-ui/public/studio.js — the studio, /builders/studio.
|
|
3
2
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// THE STUDIO HOME (task 1003971 / BV2.TW16, goal 1000095; ADR 0341 D2, D5, D8
|
|
4
|
+
// and D11). The artist's lofi home in Tweak Mode, built 1:1 on the master
|
|
5
|
+
// design (docs/design/mocks/tweak-mode/Main.dc.html): the night room, a
|
|
6
|
+
// greeting whose words are editable where they stand, the tally top right
|
|
7
|
+
// (GET /copy-desk/tally), Resume back into the page the artist holds (GET
|
|
8
|
+
// /copy-desk/next's resume, into /builders/tweak-editor?page=<id>), and three
|
|
9
|
+
// quick actions that open glass panels in place: the Tweak/CopyWrite Flow (GET
|
|
10
|
+
// /copy-desk/next: the recommendation, take it, pick another), the Approval
|
|
11
|
+
// queue (approval-queue.js, mounted below) and Artist Review Status (GET
|
|
12
|
+
// /copy-desk/pages: three surface lines, "N of M tweaked"). The words and rules
|
|
13
|
+
// are studio-lib.js.
|
|
7
14
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
15
|
+
// THE STUDIO'S OWN WORDS are a tweak of THIS page, builders:studio (ADR 0341
|
|
16
|
+
// D2), never a live edit. A first click on them offers to take the studio page
|
|
17
|
+
// (the claim route, D5); once the artist holds it, the lines the studio's own
|
|
18
|
+
// reading lists are typed over in place and autosaved to the round's draft
|
|
19
|
+
// (PUT /copy-desk/pages/builders:studio/draft, the whole draft each time, the
|
|
20
|
+
// tweak editor's rule), and the page itself keeps its words until the round is
|
|
21
|
+
// submitted, applied, approved and shipped like any other. Submit is the
|
|
22
|
+
// editor's: the strip under the words links there.
|
|
23
|
+
//
|
|
24
|
+
// THE OLDER REVIEWS (R06 of goal 1000074, task 1003117). Below the home sits
|
|
25
|
+
// the garden this page used to be, behind a quiet door that shows only while
|
|
26
|
+
// reviews filed before Tweak Mode still wait. It presents the artist's older
|
|
27
|
+
// work ONE PIECE AT A TIME, a stone to tend, with the rest waiting quietly:
|
|
11
28
|
// 1. REVIEW REQUESTS (R04, cascade.js) — RETIRED, draining. A copy/visual
|
|
12
29
|
// ship used to file a two-step artist review at `backlog`, discipline
|
|
13
30
|
// 'artist', source 'cascade'. Tweak Mode removed that rule (task 1004325 /
|
|
14
31
|
// BV2.TW14, ADR 0341 D9), so no new reviews arrive; the ones already filed
|
|
15
32
|
// still show here so they can be answered (they may still hold a deploy
|
|
16
|
-
// under the strict artist gate, ADR 0241).
|
|
17
|
-
// Tweak Mode home, and this queue goes with it once it is empty.
|
|
33
|
+
// under the strict artist gate, ADR 0241). The door goes once none wait.
|
|
18
34
|
// 2. FLAGGED COPY (R02, copy-desk) — open flags over the words the product
|
|
19
|
-
// shows people
|
|
20
|
-
//
|
|
35
|
+
// shows people, tended beside the reviews while the door is open.
|
|
36
|
+
// Best-effort: an instance without the copy-desk module has no queue.
|
|
21
37
|
//
|
|
22
38
|
// It gates NOTHING (ADR 0162 / ADR 0241): nothing here holds a ship or a deploy.
|
|
23
39
|
// And it writes NOTHING to any live string or asset — the goal's no-live-CMS
|
|
24
|
-
// rule, unchanged. What it DOES write
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// tests/hall_studio_world.mjs.
|
|
40
|
+
// rule, unchanged. What it DOES write is named in WRITE_ROUTES below, pinned in
|
|
41
|
+
// tests/hall_studio_world.mjs: the artist's own VERDICT on their own older
|
|
42
|
+
// reviews (since task 1003829), the Approval queue's two decisions (TW12), and
|
|
43
|
+
// the page claim and draft save that make a page, the studio included, the
|
|
44
|
+
// artist's to rewrite (TW16). Every change to the product still lands as a
|
|
45
|
+
// claimed, graded, shipped task.
|
|
31
46
|
//
|
|
32
47
|
// THE PICTURE (task 1003829). A review names the shipped task it is about, and
|
|
33
48
|
// that task may carry a ship-time visual. The room reads it for the card in
|
|
@@ -95,6 +110,14 @@
|
|
|
95
110
|
// travels as the route's own path (the tweak editor's take.path shape).
|
|
96
111
|
approve: (pageId) => `${API}/copy-desk/pages/${pageId}/approve`,
|
|
97
112
|
sendBack: (pageId) => `${API}/copy-desk/pages/${pageId}/send-back`,
|
|
113
|
+
// The home's two (task 1003971 / BV2.TW16, ADR 0341 D5 and D11). claim
|
|
114
|
+
// takes a page's tweak round for the artist: "take it" in the Tweak/
|
|
115
|
+
// CopyWrite Flow, and "take the studio page" before the studio's own words
|
|
116
|
+
// are typed over. draft saves the WHOLE draft of the studio page the artist
|
|
117
|
+
// holds (the holder only; the route decides). Neither writes a live string:
|
|
118
|
+
// a round's words reach its page only as a shipped, approved tweak.
|
|
119
|
+
claim: (pageId) => `${API}/copy-desk/pages/${pageId}/claim`,
|
|
120
|
+
draft: (pageId) => `${API}/copy-desk/pages/${pageId}/draft`,
|
|
98
121
|
});
|
|
99
122
|
|
|
100
123
|
// The verdict floor, read off /me. Local like goals-render.js's copy — four
|
|
@@ -111,14 +134,6 @@
|
|
|
111
134
|
// helpers
|
|
112
135
|
// ---------------------------------------------------------------------------
|
|
113
136
|
|
|
114
|
-
function greeting() {
|
|
115
|
-
const h = new Date().getHours();
|
|
116
|
-
if (h < 5) return 'Still up';
|
|
117
|
-
if (h < 12) return 'Good morning';
|
|
118
|
-
if (h < 18) return 'Good afternoon';
|
|
119
|
-
return 'Good evening';
|
|
120
|
-
}
|
|
121
|
-
|
|
122
137
|
// The shipped task a review is ABOUT — cascade.js stamps
|
|
123
138
|
// source_ref = 'cascade-artist-review-on-ship-<taskId>'.
|
|
124
139
|
function reviewShippedId(t) {
|
|
@@ -255,13 +270,18 @@
|
|
|
255
270
|
// paint
|
|
256
271
|
// ---------------------------------------------------------------------------
|
|
257
272
|
|
|
273
|
+
// The quiet door to the older reviews: shown only while a review filed before
|
|
274
|
+
// Tweak Mode still waits (TW14), since that is the one reason the garden stays.
|
|
258
275
|
function paintHead() {
|
|
259
|
-
const
|
|
260
|
-
$('#
|
|
261
|
-
|
|
262
|
-
$('#
|
|
263
|
-
|
|
264
|
-
|
|
276
|
+
const reviews = pieces.filter((p) => p.type === 'review').length;
|
|
277
|
+
const door = $('#st-older');
|
|
278
|
+
if (door) door.hidden = reviews === 0;
|
|
279
|
+
const label = $('#st-older-label');
|
|
280
|
+
if (label) {
|
|
281
|
+
label.textContent = reviews === 1
|
|
282
|
+
? '1 review from before Tweak Mode still waits'
|
|
283
|
+
: `${reviews} reviews from before Tweak Mode still wait`;
|
|
284
|
+
}
|
|
265
285
|
}
|
|
266
286
|
|
|
267
287
|
function paint() {
|
|
@@ -558,10 +578,399 @@
|
|
|
558
578
|
approvals.refresh();
|
|
559
579
|
}
|
|
560
580
|
|
|
581
|
+
// ===========================================================================
|
|
582
|
+
// THE HOME (task 1003971 / BV2.TW16): the tally, Resume, the three quick
|
|
583
|
+
// actions and their panels, and the studio's own words. It needs a real DOM
|
|
584
|
+
// and the two pure libs; without either (the vm tests of the garden, a
|
|
585
|
+
// stale cache) it draws nothing and the garden still works.
|
|
586
|
+
// ===========================================================================
|
|
587
|
+
const lib = window.OTBStudioLib;
|
|
588
|
+
const editorLib = window.OTBTweakEditor;
|
|
589
|
+
const hasDom = typeof document.getElementById === 'function';
|
|
590
|
+
const byId = (id) => (hasDom ? document.getElementById(id) : null);
|
|
591
|
+
const STUDIO = lib ? lib.STUDIO_PAGE : 'builders:studio';
|
|
592
|
+
const SAVE_DEBOUNCE_MS = 700;
|
|
593
|
+
const editorHref = (pageId) => `/builders/tweak-editor?page=${pageId}`;
|
|
594
|
+
|
|
595
|
+
const home = {
|
|
596
|
+
next: null, nextFailed: false,
|
|
597
|
+
pages: null, pagesFailed: false,
|
|
598
|
+
pick: null, picking: false,
|
|
599
|
+
panel: null, opener: null,
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// The one claim call site, for a page id the inventory's shape allows (the
|
|
603
|
+
// server validates it against the inventory again).
|
|
604
|
+
async function claimPage(pageId) {
|
|
605
|
+
if (!lib || !lib.PAGE_ID_RE.test(String(pageId || ''))) return { ok: false, data: {} };
|
|
606
|
+
return api.request('POST', WRITE_ROUTES.claim(pageId), {});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function refusalOf(res) {
|
|
610
|
+
const d = (window.OTB.errDetails && window.OTB.errDetails(res.data)) || {};
|
|
611
|
+
const code = (window.OTB.errCode && window.OTB.errCode(res.data)) || (res.data && res.data.error && res.data.error.code) || '';
|
|
612
|
+
return lib.claimWords(code, d);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function setText(id, text) { const el = byId(id); if (el) el.textContent = text; }
|
|
616
|
+
|
|
617
|
+
// ---- the tally, Resume, the details ---------------------------------------
|
|
618
|
+
|
|
619
|
+
async function loadTally() {
|
|
620
|
+
const res = await api.request('GET', `${API}/copy-desk/tally`);
|
|
621
|
+
const parts = res.ok ? lib.tallyParts(res.data) : null;
|
|
622
|
+
const pill = byId('st-tally');
|
|
623
|
+
if (!pill || !parts) return;
|
|
624
|
+
setText('st-tally-credits', parts.credits);
|
|
625
|
+
setText('st-tally-pages', parts.pages);
|
|
626
|
+
setText('st-tally-week', parts.week);
|
|
627
|
+
pill.hidden = false;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function loadNext() {
|
|
631
|
+
const res = await api.request('GET', `${API}/copy-desk/next`);
|
|
632
|
+
if (!res.ok || !res.data) { home.nextFailed = true; return; }
|
|
633
|
+
home.next = res.data;
|
|
634
|
+
setText('st-flow-detail', lib.flowDetail(home.next));
|
|
635
|
+
const resume = home.next.resume;
|
|
636
|
+
if (!resume || !lib.PAGE_ID_RE.test(String(resume.page_id || ''))) return;
|
|
637
|
+
// One more read, for "12 of 38 lines": the editor's own counter.
|
|
638
|
+
const ed = await api.request('GET', `${API}/copy-desk/pages/${resume.page_id}/draft`);
|
|
639
|
+
const counter = ed.ok && ed.data ? ed.data.counter : null;
|
|
640
|
+
const link = byId('st-resume');
|
|
641
|
+
if (link) {
|
|
642
|
+
link.href = editorHref(resume.page_id);
|
|
643
|
+
link.hidden = false;
|
|
644
|
+
}
|
|
645
|
+
setText('st-resume-detail', lib.resumeDetail(resume, counter));
|
|
646
|
+
paintSubLine(lib.subParts(resume, counter));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// The greeting's sub-line, written here (not in studio-lib.js) so the copy
|
|
650
|
+
// registry places it at this assignment: it is one of the studio's own lines,
|
|
651
|
+
// and an in-place tweak of it names this string.
|
|
652
|
+
function paintSubLine(p) {
|
|
653
|
+
const sub = byId('studio-sub');
|
|
654
|
+
if (!sub || !p || wedit.editing) return;
|
|
655
|
+
if (p.counted) {
|
|
656
|
+
sub.textContent = `${p.title}, on the ${p.surface} page, is where you left it. ${p.changed} of its ${p.total} lines are yours so far.`;
|
|
657
|
+
} else {
|
|
658
|
+
sub.textContent = `${p.title}, on the ${p.surface} page, is where you left it.`;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function loadPages() {
|
|
663
|
+
const res = await api.request('GET', `${API}/copy-desk/pages`);
|
|
664
|
+
if (!res.ok || !res.data) { home.pagesFailed = true; return; }
|
|
665
|
+
home.pages = res.data;
|
|
666
|
+
setText('st-status-detail', lib.statusDetail(home.pages));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// ---- the two small glass panels -------------------------------------------
|
|
670
|
+
|
|
671
|
+
const TITLES = Object.freeze({ flow: 'Tweak/CopyWrite Flow', status: 'Artist Review Status' });
|
|
672
|
+
|
|
673
|
+
function flowHtml() {
|
|
674
|
+
if (!home.next) {
|
|
675
|
+
return `<p class="st-panel__line">${home.nextFailed ? 'The next page could not be read just now. Try again in a moment.' : 'Finding the next page…'}</p>`;
|
|
676
|
+
}
|
|
677
|
+
const all = [home.next.recommended, ...(home.next.alternatives || [])].filter(Boolean);
|
|
678
|
+
const cur = (home.pick && all.find((c) => c.page_id === home.pick)) || all[0];
|
|
679
|
+
if (!cur) return '<p class="st-panel__line">Every page is taken or on its way to the site right now. Come back soon.</p>';
|
|
680
|
+
const others = all.filter((c) => c !== cur);
|
|
681
|
+
const pick = others.length
|
|
682
|
+
? `<button type="button" class="st-pick" data-pick aria-expanded="${home.picking ? 'true' : 'false'}" aria-controls="st-picks">pick another</button>`
|
|
683
|
+
: '';
|
|
684
|
+
const chips = others.map((o) => `<button type="button" class="st-chip" data-pick-id="${escapeHtml(o.page_id)}">${escapeHtml(lib.pickLabel(o))}</button>`).join('');
|
|
685
|
+
return `
|
|
686
|
+
<div class="st-next">
|
|
687
|
+
<span class="st-next__page">${escapeHtml(lib.titleOf(cur))}</span>
|
|
688
|
+
<span class="st-next__why">${escapeHtml(cur.why_line || cur.why || '')}</span>
|
|
689
|
+
</div>
|
|
690
|
+
<div class="st-next__acts">
|
|
691
|
+
<button type="button" class="st-take" data-take="${escapeHtml(cur.page_id)}">take it</button>
|
|
692
|
+
${pick}
|
|
693
|
+
</div>
|
|
694
|
+
<p class="st-panel__err" id="st-take-err" role="alert" hidden></p>
|
|
695
|
+
${others.length ? `<div class="st-picks" id="st-picks"${home.picking ? '' : ' hidden'}>${chips}</div>` : ''}`;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function statusHtml() {
|
|
699
|
+
if (!home.pages) {
|
|
700
|
+
return `<p class="st-panel__line">${home.pagesFailed ? 'The pages could not be read just now. Try again in a moment.' : 'Reading the pages…'}</p>`;
|
|
701
|
+
}
|
|
702
|
+
const rows = lib.surfaceRows(home.pages, lib.landingHost(byId('hall-main') && byId('hall-main').getAttribute('data-apex'), window.location && window.location.host)).map((s) => `
|
|
703
|
+
<li class="st-surface">
|
|
704
|
+
<div class="st-surface__head">
|
|
705
|
+
<span class="st-surface__name">${escapeHtml(s.name)}</span>
|
|
706
|
+
<span class="st-surface__where">${escapeHtml(s.where)}</span>
|
|
707
|
+
<span class="st-surface__count">${escapeHtml(s.count)}</span>
|
|
708
|
+
</div>
|
|
709
|
+
<div class="st-surface__track"><div class="st-surface__fill" style="width: ${Number(s.pct)}%"></div></div>
|
|
710
|
+
</li>`).join('');
|
|
711
|
+
const asked = lib.askedLine(home.next && home.next.asked_pages);
|
|
712
|
+
return `<ul class="st-surfaces">${rows}</ul>${asked ? `<p class="st-asked">${escapeHtml(asked)}</p>` : ''}`;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function paintPanel() {
|
|
716
|
+
const body = byId('st-panel-body');
|
|
717
|
+
if (!body || !home.panel) return;
|
|
718
|
+
body.innerHTML = home.panel === 'flow' ? flowHtml() : statusHtml();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function openPanel(kind, opener) {
|
|
722
|
+
const overlay = byId('st-panel');
|
|
723
|
+
if (!overlay || !TITLES[kind]) return;
|
|
724
|
+
home.panel = kind;
|
|
725
|
+
home.opener = opener || null;
|
|
726
|
+
home.picking = false;
|
|
727
|
+
setText('st-panel-title', TITLES[kind]);
|
|
728
|
+
paintPanel();
|
|
729
|
+
overlay.hidden = false;
|
|
730
|
+
document.documentElement.classList.add('st-open');
|
|
731
|
+
const title = byId('st-panel-title');
|
|
732
|
+
if (title) title.focus();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function closePanel() {
|
|
736
|
+
const overlay = byId('st-panel');
|
|
737
|
+
if (!overlay || overlay.hidden) return;
|
|
738
|
+
overlay.hidden = true;
|
|
739
|
+
home.panel = null;
|
|
740
|
+
document.documentElement.classList.remove('st-open');
|
|
741
|
+
if (home.opener && home.opener.focus) home.opener.focus();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function takePage(pageId, button) {
|
|
745
|
+
if (button) button.disabled = true;
|
|
746
|
+
const res = await claimPage(pageId);
|
|
747
|
+
if (res.ok) { window.location.assign(editorHref(pageId)); return; }
|
|
748
|
+
if (button) button.disabled = false;
|
|
749
|
+
const err = byId('st-take-err');
|
|
750
|
+
if (err) { err.textContent = refusalOf(res); err.hidden = false; }
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ---- the older reviews' dialog --------------------------------------------
|
|
754
|
+
|
|
755
|
+
function openReviews(opener) {
|
|
756
|
+
const box = byId('st-reviews');
|
|
757
|
+
if (!box) return;
|
|
758
|
+
home.opener = opener || null;
|
|
759
|
+
box.hidden = false;
|
|
760
|
+
document.documentElement.classList.add('st-open');
|
|
761
|
+
const title = byId('st-reviews-title');
|
|
762
|
+
if (title) title.focus();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function closeReviews() {
|
|
766
|
+
const box = byId('st-reviews');
|
|
767
|
+
if (!box || box.hidden) return;
|
|
768
|
+
box.hidden = true;
|
|
769
|
+
document.documentElement.classList.remove('st-open');
|
|
770
|
+
const door = byId('st-older');
|
|
771
|
+
const back = home.opener && !home.opener.hidden ? home.opener : (door && !door.hidden ? door : byId('st-flow'));
|
|
772
|
+
if (back && back.focus) back.focus();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// ---- the studio's own words, typed over in place ---------------------------
|
|
776
|
+
|
|
777
|
+
const wedit = {
|
|
778
|
+
editing: false, busy: false,
|
|
779
|
+
lines: [], readingHash: null, values: {}, keys: new Map(),
|
|
780
|
+
save: 'saved', timer: null, inflight: null, again: false, lastSent: null, error: '',
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
function wordEls() {
|
|
784
|
+
const box = byId('st-words');
|
|
785
|
+
return box ? Array.from(box.querySelectorAll('[data-words]')) : [];
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function paintEditing() {
|
|
789
|
+
const strip = byId('st-editing');
|
|
790
|
+
if (!strip || !wedit.editing) return;
|
|
791
|
+
const changed = editorLib.changedCount(wedit.lines, wedit.values);
|
|
792
|
+
const counter = editorLib.counterText(changed, wedit.lines.length, wedit.save);
|
|
793
|
+
const unread = wedit.keys.size === 0;
|
|
794
|
+
strip.classList.toggle('st-editing--failed', wedit.save === 'failed');
|
|
795
|
+
strip.innerHTML = unread
|
|
796
|
+
? `The studio page is yours, but its reading does not list these words yet. <a href="${editorHref(STUDIO)}">Rewrite it in the editor</a>`
|
|
797
|
+
: `The studio page is yours · ${escapeHtml(counter)}${wedit.error ? ` · ${escapeHtml(wedit.error)}` : ''} · <a href="${editorHref(STUDIO)}">submit it in the editor</a>`;
|
|
798
|
+
strip.hidden = false;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function enterEditing(ed, focusEl) {
|
|
802
|
+
const reading = (ed && ed.reading) || {};
|
|
803
|
+
wedit.lines = reading.lines || [];
|
|
804
|
+
wedit.readingHash = reading.reading_hash || null;
|
|
805
|
+
wedit.values = editorLib.initialValues(wedit.lines, ed && ed.draft);
|
|
806
|
+
wedit.lastSent = JSON.stringify(editorLib.draftBody(wedit.readingHash, wedit.lines, wedit.values));
|
|
807
|
+
wedit.keys = new Map();
|
|
808
|
+
for (const el of wordEls()) {
|
|
809
|
+
const line = lib.matchLine(el.textContent, wedit.lines);
|
|
810
|
+
if (!line) { el.setAttribute('data-unread', ''); continue; }
|
|
811
|
+
wedit.keys.set(el, line.key);
|
|
812
|
+
el.textContent = wedit.values[line.key];
|
|
813
|
+
el.setAttribute('contenteditable', 'true');
|
|
814
|
+
el.setAttribute('spellcheck', 'false');
|
|
815
|
+
el.setAttribute('aria-label', `The studio's words: ${line.text}`);
|
|
816
|
+
}
|
|
817
|
+
wedit.editing = true;
|
|
818
|
+
const hint = byId('st-hint');
|
|
819
|
+
if (hint) hint.hidden = true;
|
|
820
|
+
const offer = byId('st-offer');
|
|
821
|
+
if (offer) offer.hidden = true;
|
|
822
|
+
paintEditing();
|
|
823
|
+
const target = (focusEl && wedit.keys.has(focusEl)) ? focusEl : wordEls().find((el) => wedit.keys.has(el));
|
|
824
|
+
if (target) target.focus();
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async function readStudio() {
|
|
828
|
+
const res = await api.request('GET', `${API}/copy-desk/pages/${STUDIO}/draft`);
|
|
829
|
+
return res.ok && res.data && res.data.reading ? res.data : null;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// A first click on the words: straight into editing when the artist already
|
|
833
|
+
// holds the studio page, the offer to take it otherwise.
|
|
834
|
+
async function beginWords(focusEl) {
|
|
835
|
+
if (wedit.editing || wedit.busy || !lib || !editorLib) return;
|
|
836
|
+
wedit.busy = true;
|
|
837
|
+
const ed = await readStudio();
|
|
838
|
+
wedit.busy = false;
|
|
839
|
+
if (ed && ed.round && ed.round.held_by_you) { enterEditing(ed, focusEl); return; }
|
|
840
|
+
wedit.focusAfter = focusEl || null;
|
|
841
|
+
const offer = byId('st-offer');
|
|
842
|
+
const err = byId('st-offer-err');
|
|
843
|
+
if (err) err.hidden = true;
|
|
844
|
+
if (offer) offer.hidden = false;
|
|
845
|
+
const take = byId('st-offer-take');
|
|
846
|
+
if (take) take.focus();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
async function takeStudio() {
|
|
850
|
+
const take = byId('st-offer-take');
|
|
851
|
+
const err = byId('st-offer-err');
|
|
852
|
+
if (take) take.disabled = true;
|
|
853
|
+
const res = await claimPage(STUDIO);
|
|
854
|
+
if (!res.ok) {
|
|
855
|
+
if (take) take.disabled = false;
|
|
856
|
+
if (err) { err.textContent = refusalOf(res); err.hidden = false; }
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const ed = await readStudio();
|
|
860
|
+
if (take) take.disabled = false;
|
|
861
|
+
if (!ed) {
|
|
862
|
+
if (err) { err.textContent = 'The studio page is yours, but its words could not be read just now. Open it in the editor.'; err.hidden = false; }
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
enterEditing(ed, wedit.focusAfter);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function scheduleSave() {
|
|
869
|
+
clearTimeout(wedit.timer);
|
|
870
|
+
wedit.timer = setTimeout(saveWords, SAVE_DEBOUNCE_MS);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// The WHOLE draft, every changed line of the studio page (the ones typed here
|
|
874
|
+
// and any the editor saved), one save in flight at a time.
|
|
875
|
+
async function saveWords() {
|
|
876
|
+
clearTimeout(wedit.timer);
|
|
877
|
+
if (!wedit.editing) return;
|
|
878
|
+
if (wedit.inflight) { wedit.again = true; return; }
|
|
879
|
+
const body = editorLib.draftBody(wedit.readingHash, wedit.lines, wedit.values);
|
|
880
|
+
const sent = JSON.stringify(body);
|
|
881
|
+
if (sent === wedit.lastSent) return;
|
|
882
|
+
wedit.save = 'saving';
|
|
883
|
+
wedit.error = '';
|
|
884
|
+
paintEditing();
|
|
885
|
+
wedit.inflight = api.request('PUT', WRITE_ROUTES.draft(STUDIO), body);
|
|
886
|
+
const res = await wedit.inflight;
|
|
887
|
+
wedit.inflight = null;
|
|
888
|
+
if (res.ok) {
|
|
889
|
+
wedit.lastSent = sent;
|
|
890
|
+
wedit.save = 'saved';
|
|
891
|
+
} else {
|
|
892
|
+
wedit.save = 'failed';
|
|
893
|
+
const d = (window.OTB.errDetails && window.OTB.errDetails(res.data)) || {};
|
|
894
|
+
wedit.error = d.what_this_means || errMessage(res.data) || 'the words could not be saved';
|
|
895
|
+
}
|
|
896
|
+
paintEditing();
|
|
897
|
+
if (wedit.again) { wedit.again = false; saveWords(); }
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function wireHome() {
|
|
901
|
+
const flow = byId('st-flow');
|
|
902
|
+
if (flow) flow.addEventListener('click', () => openPanel('flow', flow));
|
|
903
|
+
const status = byId('st-status');
|
|
904
|
+
if (status) status.addEventListener('click', () => openPanel('status', status));
|
|
905
|
+
const older = byId('st-older');
|
|
906
|
+
if (older) older.addEventListener('click', () => openReviews(older));
|
|
907
|
+
|
|
908
|
+
const overlay = byId('st-panel');
|
|
909
|
+
if (overlay) {
|
|
910
|
+
overlay.addEventListener('click', (ev) => {
|
|
911
|
+
if (ev.target === overlay || ev.target.closest('[data-close-panel]')) { closePanel(); return; }
|
|
912
|
+
const take = ev.target.closest('[data-take]');
|
|
913
|
+
if (take) { takePage(take.getAttribute('data-take'), take); return; }
|
|
914
|
+
if (ev.target.closest('[data-pick]')) { home.picking = !home.picking; paintPanel(); const b = overlay.querySelector('[data-pick]'); if (b) b.focus(); return; }
|
|
915
|
+
const chip = ev.target.closest('[data-pick-id]');
|
|
916
|
+
if (chip) { home.pick = chip.getAttribute('data-pick-id'); home.picking = false; paintPanel(); const t = overlay.querySelector('[data-take]'); if (t) t.focus(); }
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
const reviews = byId('st-reviews');
|
|
920
|
+
if (reviews) {
|
|
921
|
+
reviews.addEventListener('click', (ev) => {
|
|
922
|
+
if (ev.target === reviews || ev.target.closest('[data-close-reviews]')) closeReviews();
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
document.addEventListener('keydown', (ev) => {
|
|
926
|
+
if (ev.key !== 'Escape') return;
|
|
927
|
+
if (home.panel) { closePanel(); return; }
|
|
928
|
+
const box = byId('st-reviews');
|
|
929
|
+
if (box && !box.hidden) closeReviews();
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
const hint = byId('st-hint');
|
|
933
|
+
if (hint) hint.addEventListener('click', () => beginWords(null));
|
|
934
|
+
const no = byId('st-offer-no');
|
|
935
|
+
if (no) no.addEventListener('click', () => { const o = byId('st-offer'); if (o) o.hidden = true; if (hint) hint.focus(); });
|
|
936
|
+
const take = byId('st-offer-take');
|
|
937
|
+
if (take) take.addEventListener('click', takeStudio);
|
|
938
|
+
for (const el of wordEls()) {
|
|
939
|
+
el.addEventListener('click', () => { if (!wedit.editing) beginWords(el); });
|
|
940
|
+
el.addEventListener('keydown', (ev) => {
|
|
941
|
+
if (ev.key === 'Enter' && wedit.editing) { ev.preventDefault(); el.blur(); }
|
|
942
|
+
});
|
|
943
|
+
el.addEventListener('paste', (ev) => {
|
|
944
|
+
if (!wedit.editing) return;
|
|
945
|
+
ev.preventDefault();
|
|
946
|
+
const text = ((ev.clipboardData && ev.clipboardData.getData('text/plain')) || '').replace(/\s+/g, ' ');
|
|
947
|
+
document.execCommand('insertText', false, text);
|
|
948
|
+
});
|
|
949
|
+
el.addEventListener('input', () => {
|
|
950
|
+
const key = wedit.keys.get(el);
|
|
951
|
+
if (!key) return;
|
|
952
|
+
wedit.values[key] = el.textContent;
|
|
953
|
+
paintEditing();
|
|
954
|
+
scheduleSave();
|
|
955
|
+
});
|
|
956
|
+
el.addEventListener('blur', () => { if (wedit.keys.has(el)) saveWords(); });
|
|
957
|
+
}
|
|
958
|
+
window.addEventListener('pagehide', () => { if (wedit.editing) saveWords(); });
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
async function bootHome(signedIn) {
|
|
962
|
+
if (!hasDom || !lib) return;
|
|
963
|
+
wireHome();
|
|
964
|
+
if (!signedIn) return;
|
|
965
|
+
await Promise.all([loadTally(), loadNext(), loadPages()]);
|
|
966
|
+
if (home.panel) paintPanel();
|
|
967
|
+
}
|
|
968
|
+
|
|
561
969
|
(async () => {
|
|
562
970
|
mountApprovals();
|
|
563
971
|
const me = await api.request('GET', `${API}/me`);
|
|
564
972
|
if (me.ok) viewer = me.data.builder || me.data;
|
|
973
|
+
bootHome(me.ok);
|
|
565
974
|
// Reviews first (the richer craft), then flags — each newest-first within.
|
|
566
975
|
const [reviews, flags] = await Promise.all([loadReviews(), loadFlags()]);
|
|
567
976
|
pieces = [...reviews, ...flags];
|