@bongos/core 1.21.75 → 1.21.76
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 +65 -45
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +50 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +45 -42
- package/docs/copy-registry.json +76 -49
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +16 -8
- package/docs/page-readings.json +1542 -1522
- package/modules/hall-ui/public/craft-mode.js +3 -1
- package/modules/hall-ui/public/page-notes.css +150 -0
- package/modules/hall-ui/public/page-notes.js +30 -5
- package/modules/hall-ui/public/studio.css +15 -0
- package/modules/hall-ui/public/studio.html +6 -0
- package/modules/hall-ui/public/studio.js +38 -4
- package/modules/hall-ui/public/style.css +1 -116
- package/modules/page-notes/db.js +19 -0
- package/modules/page-notes/migrations/page_notes_002_surface_index.sql +12 -0
- package/modules/page-notes/notes.js +25 -2
- package/modules/page-notes/routes/page-notes.js +9 -0
- package/modules/public-landing/public/account.html +2 -0
- package/modules/public-landing/public/assets/artist-review.js +107 -0
- package/modules/public-landing/public/contact.html +2 -0
- package/modules/public-landing/public/guild.html +2 -0
- package/modules/public-landing/public/index.html +2 -0
- package/modules/public-landing/public/invitations.html +2 -0
- package/modules/public-landing/public/privacy.html +2 -0
- package/modules/public-landing/public/projects.html +2 -0
- package/modules/public-landing/public/terms.html +2 -0
- package/modules/ui-design/kit/serve.js +32 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/artist_review.mjs +231 -0
- package/tests/hall_studio_home.mjs +39 -1
- package/tests/landing_legal_pages.mjs +2 -1
- package/tests/page_notes_db.mjs +28 -1
- package/tests/page_notes_panel.mjs +9 -3
|
@@ -206,7 +206,9 @@
|
|
|
206
206
|
// routes behind a tool hold its walls (ADR 0016).
|
|
207
207
|
var TOOLS = {
|
|
208
208
|
artistry: [
|
|
209
|
-
|
|
209
|
+
// Named "Artist review" since task 1004499: the public site's button
|
|
210
|
+
// (public-landing assets/artist-review.js) opens this same panel.
|
|
211
|
+
{ id: 'palette', icon: 'artistry', short: 'Review', label: 'Artist review: write feedback on this page', script: 'page-notes.js', api: 'OTBPageNotes' },
|
|
210
212
|
// Task 1004497: this page's words, edited where they stand as a proposed wording change.
|
|
211
213
|
{ id: 'paintbrush', icon: 'brush', short: 'Edit words', label: 'Edit this page\'s words', script: 'paintbrush.js', api: 'OTBPaintbrush' },
|
|
212
214
|
],
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/* page-notes.css — the page feedback doc's sheet (task 1004496), moved out of
|
|
2
|
+
style.css by task 1004499 so the PUBLIC site's Artist review button can wear
|
|
3
|
+
the same panel: style.css imports it for the hall, and the public pages'
|
|
4
|
+
artist-review.js links it at /modules/hall-ui/public/page-notes.css. Every
|
|
5
|
+
token the public sheets may lack carries its fallback in place. The
|
|
6
|
+
lightbulb (lightbulb.js) rides these classes too. */
|
|
7
|
+
|
|
8
|
+
/* The fallbacks live as custom properties, never in a declaration value, so
|
|
9
|
+
the hall's colour-literal guard (tests/hall_audit.mjs) still holds: in the
|
|
10
|
+
hall each one resolves to its token, on the public site to its fallback. */
|
|
11
|
+
.pn, .arv-btn {
|
|
12
|
+
--pn-scrim: var(--scrim, rgba(0, 0, 0, .55));
|
|
13
|
+
--pn-shadow-pop: var(--shadow-pop, 0 24px 60px rgba(0, 0, 0, .35));
|
|
14
|
+
--pn-danger: var(--danger, #e5484d);
|
|
15
|
+
--pn-accent-on: var(--accent-on, #1a0b04);
|
|
16
|
+
--pn-glass: var(--glass, rgba(255, 255, 255, .08));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* The page feedback doc: a sheet from the right over a scrim. */
|
|
20
|
+
html.pn-open { overflow: hidden; }
|
|
21
|
+
.pn {
|
|
22
|
+
position: fixed;
|
|
23
|
+
inset: 0;
|
|
24
|
+
z-index: var(--z-modal, 1000);
|
|
25
|
+
display: flex;
|
|
26
|
+
justify-content: flex-end;
|
|
27
|
+
background: var(--pn-scrim);
|
|
28
|
+
}
|
|
29
|
+
.pn__panel {
|
|
30
|
+
position: relative;
|
|
31
|
+
width: min(560px, 100%);
|
|
32
|
+
height: 100%;
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
background: var(--bg-card);
|
|
37
|
+
color: var(--ink);
|
|
38
|
+
box-shadow: 0 0 0 1px var(--glass-edge), var(--pn-shadow-pop);
|
|
39
|
+
}
|
|
40
|
+
.pn__head { position: relative; padding: 22px 64px 14px 24px; border-bottom: 1px solid var(--rule-soft); }
|
|
41
|
+
.pn__eyebrow {
|
|
42
|
+
margin: 0 0 6px;
|
|
43
|
+
font-family: var(--label-face);
|
|
44
|
+
font-size: 11px;
|
|
45
|
+
letter-spacing: .12em;
|
|
46
|
+
text-transform: uppercase;
|
|
47
|
+
color: var(--craft, var(--ink-faint));
|
|
48
|
+
}
|
|
49
|
+
.pn__title { margin: 0; font-family: var(--font-display); font-weight: 400; font-size: 22px; letter-spacing: -0.02em; }
|
|
50
|
+
.pn__close {
|
|
51
|
+
position: absolute; top: 14px; right: 14px;
|
|
52
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
53
|
+
width: 40px; height: 40px; padding: 0; border: 0; border-radius: var(--radius-pill);
|
|
54
|
+
background: none; color: var(--ink-soft); cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
.pn__close svg { width: 18px; height: 18px; }
|
|
57
|
+
.pn__close:hover { color: var(--ink); background: var(--glass); }
|
|
58
|
+
.pn__body { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 24px 28px; display: flex; flex-direction: column; gap: 12px; }
|
|
59
|
+
.pn__label { font-size: 13px; color: var(--ink-soft); }
|
|
60
|
+
.pn__text {
|
|
61
|
+
width: 100%;
|
|
62
|
+
min-height: 260px;
|
|
63
|
+
box-sizing: border-box;
|
|
64
|
+
padding: 14px 16px;
|
|
65
|
+
border: 1px solid var(--rule);
|
|
66
|
+
border-radius: var(--radius-sm);
|
|
67
|
+
background: var(--bg);
|
|
68
|
+
color: var(--ink);
|
|
69
|
+
font: inherit;
|
|
70
|
+
font-size: 15px;
|
|
71
|
+
line-height: 1.55;
|
|
72
|
+
resize: vertical;
|
|
73
|
+
}
|
|
74
|
+
.pn__text:focus { outline: 2px solid var(--focus); outline-offset: 1px; }
|
|
75
|
+
.pn__tools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }
|
|
76
|
+
.pn__add {
|
|
77
|
+
position: relative;
|
|
78
|
+
display: inline-flex; align-items: center;
|
|
79
|
+
min-height: 34px; padding: 0 14px;
|
|
80
|
+
border-radius: var(--radius-pill);
|
|
81
|
+
box-shadow: inset 0 0 0 1px var(--rule);
|
|
82
|
+
font-size: 13.5px; color: var(--ink); cursor: pointer;
|
|
83
|
+
}
|
|
84
|
+
.pn__add:hover { background: var(--glass); }
|
|
85
|
+
.pn__add:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
86
|
+
.pn__add input { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; }
|
|
87
|
+
.pn__quiet { margin: 0; font-size: 13px; color: var(--ink-faint); }
|
|
88
|
+
.pn__saved { margin-left: auto; }
|
|
89
|
+
.pn__thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: 8px; }
|
|
90
|
+
.pn__thumb { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 8px; background: var(--bg-deep); }
|
|
91
|
+
.pn__error { margin: 0; font-size: 13.5px; color: var(--pn-danger); }
|
|
92
|
+
.pn__error:empty { display: none; }
|
|
93
|
+
.pn__foot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; padding-top: 4px; }
|
|
94
|
+
.pn__hint { margin: 0; font-size: 13px; line-height: 1.45; color: var(--ink-soft); }
|
|
95
|
+
.pn__foot .pn__hint { flex: 1 1 240px; }
|
|
96
|
+
.pn__submit {
|
|
97
|
+
min-height: 40px; padding: 0 20px; border: 0; border-radius: var(--radius-pill);
|
|
98
|
+
background: var(--accent); color: var(--pn-accent-on); font: inherit; font-weight: 500; cursor: pointer;
|
|
99
|
+
}
|
|
100
|
+
.pn__submit:hover { background: var(--accent-lift, var(--accent)); }
|
|
101
|
+
.pn__submit:disabled { opacity: .55; cursor: default; }
|
|
102
|
+
.pn__submit:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
103
|
+
.pn__list { margin-top: 14px; padding-top: 16px; border-top: 1px solid var(--rule-soft); }
|
|
104
|
+
.pn__list-h {
|
|
105
|
+
margin: 0 0 8px;
|
|
106
|
+
font-family: var(--label-face); font-size: var(--label-size, 11.5px); letter-spacing: var(--label-space, .1em);
|
|
107
|
+
text-transform: var(--label-case, uppercase); color: var(--ink-faint); font-weight: 400;
|
|
108
|
+
}
|
|
109
|
+
.pn__rows, .pn__ideas { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
110
|
+
.pn__row {
|
|
111
|
+
display: grid; gap: 2px; width: 100%; padding: 10px 12px; border: 0; border-radius: var(--radius-sm);
|
|
112
|
+
background: none; color: var(--ink); font: inherit; text-align: left; cursor: pointer;
|
|
113
|
+
}
|
|
114
|
+
a.pn__row { text-decoration: none; } /* the lightbulb's page ideas are links */
|
|
115
|
+
.pn__row:hover { background: var(--glass); }
|
|
116
|
+
.pn__row:focus-visible, .pn__back:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
|
|
117
|
+
.pn__row-meta { font-size: 12px; color: var(--ink-faint); }
|
|
118
|
+
.pn__row-text { font-size: 14px; color: var(--ink-soft); }
|
|
119
|
+
.pn__ideas a { display: inline-block; padding: 6px 0; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
|
|
120
|
+
.pn__back {
|
|
121
|
+
align-self: flex-start; min-height: 32px; padding: 0 12px; margin-left: -12px; border: 0;
|
|
122
|
+
border-radius: var(--radius-pill); background: none; color: var(--ink-soft); font: inherit; font-size: 13.5px; cursor: pointer;
|
|
123
|
+
}
|
|
124
|
+
.pn__back:hover { color: var(--ink); background: var(--glass); }
|
|
125
|
+
.pn__done { margin: 0; font-size: 16px; }
|
|
126
|
+
.pn-doc { font-size: 15px; line-height: 1.6; overflow-wrap: anywhere; }
|
|
127
|
+
.pn-doc p, .pn-doc ul { margin: 0 0 12px; }
|
|
128
|
+
.pn-doc__h { margin: 16px 0 6px; font-size: 16px; font-weight: 500; }
|
|
129
|
+
.pn-doc__img { display: block; max-width: 100%; height: auto; margin: 8px 0; border-radius: 8px; }
|
|
130
|
+
@media (max-width: 640px) {
|
|
131
|
+
.pn__head { padding: 18px 60px 12px 16px; }
|
|
132
|
+
.pn__body { padding: 14px 16px 24px; }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* The public site's Artist review button (task 1004499, artist-review.js): a
|
|
136
|
+
pill in the hub bar, beside the identity chip. */
|
|
137
|
+
.arv-btn {
|
|
138
|
+
display: inline-flex; align-items: center; gap: 7px;
|
|
139
|
+
min-height: 36px; padding: 0 14px 0 11px;
|
|
140
|
+
border: 0; border-radius: var(--radius-pill, 999px);
|
|
141
|
+
background: none; color: var(--ink); font: inherit; font-size: 14px; cursor: pointer;
|
|
142
|
+
box-shadow: inset 0 0 0 1px var(--rule);
|
|
143
|
+
}
|
|
144
|
+
.arv-btn svg { width: 17px; height: 17px; flex: none; }
|
|
145
|
+
.arv-btn:hover { background: var(--pn-glass); }
|
|
146
|
+
.arv-btn:focus-visible { outline: 2px solid var(--focus, var(--accent)); outline-offset: 2px; }
|
|
147
|
+
@media (max-width: 640px) {
|
|
148
|
+
.arv-btn { width: 36px; padding: 0; justify-content: center; }
|
|
149
|
+
.arv-btn__label { display: none; }
|
|
150
|
+
}
|
|
@@ -26,12 +26,35 @@
|
|
|
26
26
|
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
+
// Where the panel is open. The hall needs no setting; the public site's
|
|
30
|
+
// Artist review button (task 1004499, artist-review.js) calls
|
|
31
|
+
// configure({ surface: 'site', hall: <the hall's origin> }) before it opens.
|
|
32
|
+
var HALL_ORIGIN_RE = /^https?:\/\/[A-Za-z0-9.:-]+$/;
|
|
33
|
+
var cfg = { surface: 'builders', hall: '' };
|
|
34
|
+
function configure(o) {
|
|
35
|
+
o = o || {};
|
|
36
|
+
cfg.surface = o.surface === 'site' ? 'site' : 'builders';
|
|
37
|
+
cfg.hall = HALL_ORIGIN_RE.test(String(o.hall || '')) ? String(o.hall).replace(/\/+$/, '') : '';
|
|
38
|
+
}
|
|
39
|
+
function onSite() { return cfg.surface === 'site'; }
|
|
40
|
+
// The page a doc is about. In the hall, the page inventory id builders:<data-page>.
|
|
41
|
+
// On the public site, site:<first path segment>, so / is site:home and
|
|
42
|
+
// /projects is site:projects. Those docs are read in the Studio.
|
|
29
43
|
function page() {
|
|
30
44
|
var b = document.body || { dataset: {} };
|
|
31
|
-
var slug = String((b.dataset && b.dataset.page) || 'home').toLowerCase().replace(/[^a-z0-9_-]/g, '-').slice(0, 64) || 'home';
|
|
32
|
-
var title = (b.dataset && b.dataset.pageTitle) || String(document.title || '').split(/\s[—-]\s/)[0] || slug;
|
|
33
45
|
var path = String((window.location && window.location.pathname) || '/').replace(/[^A-Za-z0-9/_.-]/g, '').slice(0, 200) || '/';
|
|
34
|
-
|
|
46
|
+
var raw = onSite() ? (path.split('/')[1] || 'home').replace(/\.html$/, '') : ((b.dataset && b.dataset.page) || 'home');
|
|
47
|
+
var slug = String(raw).toLowerCase().replace(/[^a-z0-9_-]/g, '-').replace(/^[^a-z0-9]+/, '').slice(0, 64) || 'home';
|
|
48
|
+
// A public page's title leads with the product name ("Cloud Bongos — projects"),
|
|
49
|
+
// so it is named by its path instead: Home, Projects, Account.
|
|
50
|
+
var title = onSite() ? slug.charAt(0).toUpperCase() + slug.slice(1).replace(/[-_]+/g, ' ')
|
|
51
|
+
: (b.dataset && b.dataset.pageTitle) || String(document.title || '').split(/\s[—·|-]\s/)[0] || slug;
|
|
52
|
+
return { id: (onSite() ? 'site:' : 'builders:') + slug, path: path, title: title.slice(0, 120) };
|
|
53
|
+
}
|
|
54
|
+
// An idea's own page. The hall reads it at /#/idea/N; from the public site the
|
|
55
|
+
// hall is another host on the hub, the origin configure() was given.
|
|
56
|
+
function ideaHref(id) {
|
|
57
|
+
return onSite() ? cfg.hall + '/builders/idea/' + id : '/#/idea/' + id;
|
|
35
58
|
}
|
|
36
59
|
function api(method, path, body) {
|
|
37
60
|
var opts = { method: method, credentials: 'same-origin', headers: {} };
|
|
@@ -81,7 +104,7 @@
|
|
|
81
104
|
function shellHtml(p) {
|
|
82
105
|
return '<div class="pn__panel" role="dialog" aria-modal="true" aria-labelledby="pn-title">' +
|
|
83
106
|
'<div class="pn__head">' +
|
|
84
|
-
'<p class="pn__eyebrow">
|
|
107
|
+
'<p class="pn__eyebrow">Artist review · ' + esc(p.title) + '</p>' +
|
|
85
108
|
'<h2 class="pn__title" id="pn-title">What should this page be?</h2>' +
|
|
86
109
|
'<button class="pn__close" type="button" data-pn-close aria-label="Close">' +
|
|
87
110
|
'<svg viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M5 5l8 8M13 5l-8 8"/></svg></button>' +
|
|
@@ -123,7 +146,7 @@
|
|
|
123
146
|
function ideasHtml(ideas) {
|
|
124
147
|
if (!ideas || !ideas.length) return '';
|
|
125
148
|
return '<ul class="pn__ideas">' + ideas.map(function (i) {
|
|
126
|
-
return '<li><a href="
|
|
149
|
+
return '<li><a href="' + esc(ideaHref(i.id)) + '">' + esc(i.title) + '</a></li>';
|
|
127
150
|
}).join('') + '</ul>';
|
|
128
151
|
}
|
|
129
152
|
function readHtml(doc) {
|
|
@@ -314,6 +337,8 @@
|
|
|
314
337
|
open: function () { if (mountSheet()) openDraft(); },
|
|
315
338
|
openNote: function (id) { if (mountSheet()) refreshList().then(function () { return showNote(id); }); },
|
|
316
339
|
close: close,
|
|
340
|
+
configure: configure,
|
|
341
|
+
ideaHref: ideaHref,
|
|
317
342
|
renderBody: renderBody,
|
|
318
343
|
page: page,
|
|
319
344
|
};
|
|
@@ -501,6 +501,21 @@ html.st-open { overflow: hidden; }
|
|
|
501
501
|
.st-surface__fill { height: 4px; border-radius: 999px; background: var(--st-accent); }
|
|
502
502
|
.st-asked { margin: 0; font-size: 14px; line-height: 1.5; color: var(--st-ink-faint); }
|
|
503
503
|
|
|
504
|
+
/* From the live site (task 1004499): one row per review, the page then who and
|
|
505
|
+
when, then its opening words. Each row opens the doc in the palette's panel. */
|
|
506
|
+
.st-sites { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; }
|
|
507
|
+
.st-site {
|
|
508
|
+
display: flex; flex-direction: column; gap: 4px;
|
|
509
|
+
margin: 0 -12px; padding: 10px 12px; border-radius: 12px;
|
|
510
|
+
color: var(--st-ink); text-decoration: none;
|
|
511
|
+
}
|
|
512
|
+
.st-site:hover { background: var(--st-glass); }
|
|
513
|
+
.st-site:focus-visible { outline: 2px solid var(--st-accent); outline-offset: 1px; }
|
|
514
|
+
.st-site__head { display: flex; align-items: baseline; gap: 4px 12px; flex-wrap: wrap; }
|
|
515
|
+
.st-site__page { font-size: 17px; font-weight: 300; }
|
|
516
|
+
.st-site__meta { font-size: 13px; color: var(--st-ink-dim); }
|
|
517
|
+
.st-site__text { font-size: 14px; line-height: 1.45; color: var(--st-ink-soft); overflow-wrap: anywhere; }
|
|
518
|
+
|
|
504
519
|
/* ---------------------------------------------------------------------------
|
|
505
520
|
The phone. The room keeps its night: the window shrinks to the top right
|
|
506
521
|
(scaled whole, so its moon and lights keep their places), the desk sits on a
|
|
@@ -125,6 +125,12 @@
|
|
|
125
125
|
<span class="st-action__label">Artist Review Status</span>
|
|
126
126
|
<span class="st-action__detail" id="st-status-detail">how far each surface has come</span>
|
|
127
127
|
</button>
|
|
128
|
+
<!-- Task 1004499: the reviews builders wrote on the public site with
|
|
129
|
+
its Artist review button. Each opens here, in the palette's panel. -->
|
|
130
|
+
<button type="button" class="st-action" id="st-site" aria-haspopup="dialog">
|
|
131
|
+
<span class="st-action__label">From the live site</span>
|
|
132
|
+
<span class="st-action__detail" id="st-site-detail">reviews written on the live site</span>
|
|
133
|
+
</button>
|
|
128
134
|
<!-- The retired review queue (task 1004325 / TW14), a quiet door that
|
|
129
135
|
shows only while reviews filed before Tweak Mode still wait. -->
|
|
130
136
|
<button type="button" class="st-older" id="st-older" aria-haspopup="dialog" hidden>
|
|
@@ -666,9 +666,20 @@
|
|
|
666
666
|
setText('st-status-detail', lib.statusDetail(home.pages));
|
|
667
667
|
}
|
|
668
668
|
|
|
669
|
-
//
|
|
669
|
+
// The public site's reviews (task 1004499): docs written with its Artist
|
|
670
|
+
// review button, newest first. Each row is the doc's own link, which opens it
|
|
671
|
+
// in the palette's panel right here (craft-mode.js reads ?note=).
|
|
672
|
+
async function loadSite() {
|
|
673
|
+
const res = await api.request('GET', `${API}/page-notes/site`);
|
|
674
|
+
if (!res.ok || !res.data) { home.siteFailed = true; return; }
|
|
675
|
+
home.site = res.data.notes || [];
|
|
676
|
+
const n = home.site.length;
|
|
677
|
+
setText('st-site-detail', n ? `${n === 20 ? 'the newest 20' : n} ${n === 1 ? 'review' : 'reviews'} from the live site` : 'no reviews from the live site yet');
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ---- the small glass panels -----------------------------------------------
|
|
670
681
|
|
|
671
|
-
const TITLES = Object.freeze({ flow: 'Tweak/CopyWrite Flow', status: 'Artist Review Status' });
|
|
682
|
+
const TITLES = Object.freeze({ flow: 'Tweak/CopyWrite Flow', status: 'Artist Review Status', site: 'From the live site' });
|
|
672
683
|
|
|
673
684
|
function flowHtml() {
|
|
674
685
|
if (!home.next) {
|
|
@@ -712,10 +723,31 @@
|
|
|
712
723
|
return `<ul class="st-surfaces">${rows}</ul>${asked ? `<p class="st-asked">${escapeHtml(asked)}</p>` : ''}`;
|
|
713
724
|
}
|
|
714
725
|
|
|
726
|
+
function siteHtml() {
|
|
727
|
+
if (!home.site) {
|
|
728
|
+
return `<p class="st-panel__line">${home.siteFailed ? 'The reviews could not be read just now. Try again in a moment.' : 'Reading the reviews…'}</p>`;
|
|
729
|
+
}
|
|
730
|
+
if (!home.site.length) {
|
|
731
|
+
return '<p class="st-panel__line">None yet. A signed-in builder on the live site sees a Review button in its top bar; what they write lands here.</p>';
|
|
732
|
+
}
|
|
733
|
+
const day = (iso) => { const d = iso ? new Date(iso) : null; return d && !isNaN(d) ? d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : ''; };
|
|
734
|
+
const rows = home.site.map((r) => {
|
|
735
|
+
const ideas = r.status === 'filed' ? `${r.idea_count} ${r.idea_count === 1 ? 'idea' : 'ideas'}` : 'splitting';
|
|
736
|
+
const whole = String(r.excerpt || '').replace(/!\[[^\]\n]*\]\([^)\n]*\)/g, '[image]').replace(/\s+/g, ' ').trim();
|
|
737
|
+
const text = whole.length > 140 ? `${whole.slice(0, 139).replace(/\s+\S*$/, '')}…` : whole;
|
|
738
|
+
return `
|
|
739
|
+
<li><a class="st-site" href="${escapeHtml(r.link)}">
|
|
740
|
+
<span class="st-site__head"><span class="st-site__page">${escapeHtml(r.page_title || r.page_path || r.page_id)}</span><span class="st-site__meta">${escapeHtml(r.author_name)} · ${escapeHtml(day(r.submitted_at))} · ${escapeHtml(ideas)}</span></span>
|
|
741
|
+
<span class="st-site__text">${escapeHtml(text)}</span>
|
|
742
|
+
</a></li>`;
|
|
743
|
+
}).join('');
|
|
744
|
+
return `<ul class="st-sites">${rows}</ul>`;
|
|
745
|
+
}
|
|
746
|
+
|
|
715
747
|
function paintPanel() {
|
|
716
748
|
const body = byId('st-panel-body');
|
|
717
749
|
if (!body || !home.panel) return;
|
|
718
|
-
body.innerHTML = home.panel === 'flow' ? flowHtml() : statusHtml();
|
|
750
|
+
body.innerHTML = home.panel === 'flow' ? flowHtml() : home.panel === 'site' ? siteHtml() : statusHtml();
|
|
719
751
|
}
|
|
720
752
|
|
|
721
753
|
function openPanel(kind, opener) {
|
|
@@ -902,6 +934,8 @@
|
|
|
902
934
|
if (flow) flow.addEventListener('click', () => openPanel('flow', flow));
|
|
903
935
|
const status = byId('st-status');
|
|
904
936
|
if (status) status.addEventListener('click', () => openPanel('status', status));
|
|
937
|
+
const site = byId('st-site');
|
|
938
|
+
if (site) site.addEventListener('click', () => openPanel('site', site));
|
|
905
939
|
const older = byId('st-older');
|
|
906
940
|
if (older) older.addEventListener('click', () => openReviews(older));
|
|
907
941
|
|
|
@@ -962,7 +996,7 @@
|
|
|
962
996
|
if (!hasDom || !lib) return;
|
|
963
997
|
wireHome();
|
|
964
998
|
if (!signedIn) return;
|
|
965
|
-
await Promise.all([loadTally(), loadNext(), loadPages()]);
|
|
999
|
+
await Promise.all([loadTally(), loadNext(), loadPages(), loadSite()]);
|
|
966
1000
|
if (home.panel) paintPanel();
|
|
967
1001
|
}
|
|
968
1002
|
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
here. It lives on the shell rules below and on a page's own body, because a
|
|
57
57
|
body declaration beats an inherited :root value at every width and a token
|
|
58
58
|
tier that carries a breakpoint silently defeats its own media query. */
|
|
59
|
+
@import url("page-notes.css");
|
|
59
60
|
|
|
60
61
|
:root {
|
|
61
62
|
color-scheme: light;
|
|
@@ -1569,122 +1570,6 @@ html[data-theme="light"][data-craft="ideating"] .app-sidebar {
|
|
|
1569
1570
|
.topbar-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1570
1571
|
}
|
|
1571
1572
|
|
|
1572
|
-
/* The page feedback doc: a sheet from the right over a scrim. */
|
|
1573
|
-
html.pn-open { overflow: hidden; }
|
|
1574
|
-
.pn {
|
|
1575
|
-
position: fixed;
|
|
1576
|
-
inset: 0;
|
|
1577
|
-
z-index: var(--z-modal);
|
|
1578
|
-
display: flex;
|
|
1579
|
-
justify-content: flex-end;
|
|
1580
|
-
background: var(--scrim);
|
|
1581
|
-
}
|
|
1582
|
-
.pn__panel {
|
|
1583
|
-
position: relative;
|
|
1584
|
-
width: min(560px, 100%);
|
|
1585
|
-
height: 100%;
|
|
1586
|
-
box-sizing: border-box;
|
|
1587
|
-
display: flex;
|
|
1588
|
-
flex-direction: column;
|
|
1589
|
-
background: var(--bg-card);
|
|
1590
|
-
color: var(--ink);
|
|
1591
|
-
box-shadow: 0 0 0 1px var(--glass-edge), var(--shadow-pop);
|
|
1592
|
-
}
|
|
1593
|
-
.pn__head { position: relative; padding: 22px 64px 14px 24px; border-bottom: 1px solid var(--rule-soft); }
|
|
1594
|
-
.pn__eyebrow {
|
|
1595
|
-
margin: 0 0 6px;
|
|
1596
|
-
font-family: var(--label-face);
|
|
1597
|
-
font-size: 11px;
|
|
1598
|
-
letter-spacing: .12em;
|
|
1599
|
-
text-transform: uppercase;
|
|
1600
|
-
color: var(--craft, var(--ink-faint));
|
|
1601
|
-
}
|
|
1602
|
-
.pn__title { margin: 0; font-family: var(--font-display); font-weight: 400; font-size: 22px; letter-spacing: -0.02em; }
|
|
1603
|
-
.pn__close {
|
|
1604
|
-
position: absolute; top: 14px; right: 14px;
|
|
1605
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
1606
|
-
width: 40px; height: 40px; padding: 0; border: 0; border-radius: var(--radius-pill);
|
|
1607
|
-
background: none; color: var(--ink-soft); cursor: pointer;
|
|
1608
|
-
}
|
|
1609
|
-
.pn__close svg { width: 18px; height: 18px; }
|
|
1610
|
-
.pn__close:hover { color: var(--ink); background: var(--glass); }
|
|
1611
|
-
.pn__body { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 24px 28px; display: flex; flex-direction: column; gap: 12px; }
|
|
1612
|
-
.pn__label { font-size: 13px; color: var(--ink-soft); }
|
|
1613
|
-
.pn__text {
|
|
1614
|
-
width: 100%;
|
|
1615
|
-
min-height: 260px;
|
|
1616
|
-
box-sizing: border-box;
|
|
1617
|
-
padding: 14px 16px;
|
|
1618
|
-
border: 1px solid var(--rule);
|
|
1619
|
-
border-radius: var(--radius-sm);
|
|
1620
|
-
background: var(--bg);
|
|
1621
|
-
color: var(--ink);
|
|
1622
|
-
font: inherit;
|
|
1623
|
-
font-size: 15px;
|
|
1624
|
-
line-height: 1.55;
|
|
1625
|
-
resize: vertical;
|
|
1626
|
-
}
|
|
1627
|
-
.pn__text:focus { outline: 2px solid var(--focus); outline-offset: 1px; }
|
|
1628
|
-
.pn__tools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }
|
|
1629
|
-
.pn__add {
|
|
1630
|
-
position: relative;
|
|
1631
|
-
display: inline-flex; align-items: center;
|
|
1632
|
-
min-height: 34px; padding: 0 14px;
|
|
1633
|
-
border-radius: var(--radius-pill);
|
|
1634
|
-
box-shadow: inset 0 0 0 1px var(--rule);
|
|
1635
|
-
font-size: 13.5px; color: var(--ink); cursor: pointer;
|
|
1636
|
-
}
|
|
1637
|
-
.pn__add:hover { background: var(--glass); }
|
|
1638
|
-
.pn__add:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
1639
|
-
.pn__add input { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; }
|
|
1640
|
-
.pn__quiet { margin: 0; font-size: 13px; color: var(--ink-faint); }
|
|
1641
|
-
.pn__saved { margin-left: auto; }
|
|
1642
|
-
.pn__thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: 8px; }
|
|
1643
|
-
.pn__thumb { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 8px; background: var(--bg-deep); }
|
|
1644
|
-
.pn__error { margin: 0; font-size: 13.5px; color: var(--danger); }
|
|
1645
|
-
.pn__error:empty { display: none; }
|
|
1646
|
-
.pn__foot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; padding-top: 4px; }
|
|
1647
|
-
.pn__hint { margin: 0; font-size: 13px; line-height: 1.45; color: var(--ink-soft); }
|
|
1648
|
-
.pn__foot .pn__hint { flex: 1 1 240px; }
|
|
1649
|
-
.pn__submit {
|
|
1650
|
-
min-height: 40px; padding: 0 20px; border: 0; border-radius: var(--radius-pill);
|
|
1651
|
-
background: var(--accent); color: var(--accent-on); font: inherit; font-weight: 500; cursor: pointer;
|
|
1652
|
-
}
|
|
1653
|
-
.pn__submit:hover { background: var(--accent-lift); }
|
|
1654
|
-
.pn__submit:disabled { opacity: .55; cursor: default; }
|
|
1655
|
-
.pn__submit:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
1656
|
-
.pn__list { margin-top: 14px; padding-top: 16px; border-top: 1px solid var(--rule-soft); }
|
|
1657
|
-
.pn__list-h {
|
|
1658
|
-
margin: 0 0 8px;
|
|
1659
|
-
font-family: var(--label-face); font-size: var(--label-size); letter-spacing: var(--label-space);
|
|
1660
|
-
text-transform: var(--label-case); color: var(--ink-faint); font-weight: 400;
|
|
1661
|
-
}
|
|
1662
|
-
.pn__rows, .pn__ideas { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
1663
|
-
.pn__row {
|
|
1664
|
-
display: grid; gap: 2px; width: 100%; padding: 10px 12px; border: 0; border-radius: var(--radius-sm);
|
|
1665
|
-
background: none; color: var(--ink); font: inherit; text-align: left; cursor: pointer;
|
|
1666
|
-
}
|
|
1667
|
-
a.pn__row { text-decoration: none; } /* the lightbulb's page ideas are links */
|
|
1668
|
-
.pn__row:hover { background: var(--glass); }
|
|
1669
|
-
.pn__row:focus-visible, .pn__back:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
|
|
1670
|
-
.pn__row-meta { font-size: 12px; color: var(--ink-faint); }
|
|
1671
|
-
.pn__row-text { font-size: 14px; color: var(--ink-soft); }
|
|
1672
|
-
.pn__ideas a { display: inline-block; padding: 6px 0; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
|
|
1673
|
-
.pn__back {
|
|
1674
|
-
align-self: flex-start; min-height: 32px; padding: 0 12px; margin-left: -12px; border: 0;
|
|
1675
|
-
border-radius: var(--radius-pill); background: none; color: var(--ink-soft); font: inherit; font-size: 13.5px; cursor: pointer;
|
|
1676
|
-
}
|
|
1677
|
-
.pn__back:hover { color: var(--ink); background: var(--glass); }
|
|
1678
|
-
.pn__done { margin: 0; font-size: 16px; }
|
|
1679
|
-
.pn-doc { font-size: 15px; line-height: 1.6; overflow-wrap: anywhere; }
|
|
1680
|
-
.pn-doc p, .pn-doc ul { margin: 0 0 12px; }
|
|
1681
|
-
.pn-doc__h { margin: 16px 0 6px; font-size: 16px; font-weight: 500; }
|
|
1682
|
-
.pn-doc__img { display: block; max-width: 100%; height: auto; margin: 8px 0; border-radius: 8px; }
|
|
1683
|
-
@media (max-width: 640px) {
|
|
1684
|
-
.pn__head { padding: 18px 60px 12px 16px; }
|
|
1685
|
-
.pn__body { padding: 14px 16px 24px; }
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
1573
|
/* THE PAINTBRUSH (task 1004497, paintbrush.js): the page's own words, edited
|
|
1689
1574
|
where they stand. Not modal: the page stays live under it, so the bar docks
|
|
1690
1575
|
at the foot and the words carry the mode's colour as a dashed underline. A
|
package/modules/page-notes/db.js
CHANGED
|
@@ -52,6 +52,24 @@ async function listForPage(pageId, { limit = 20 } = {}, executor) {
|
|
|
52
52
|
return r.rows;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Every submitted doc on one surface, newest first (task 1004499: the Studio's
|
|
56
|
+
// list of the public site's reviews). The surface is a fixed literal from
|
|
57
|
+
// notes.js, never request input; LIKE's own wildcards cannot appear in it.
|
|
58
|
+
async function listForSurface(surface, { limit = 20 } = {}, executor) {
|
|
59
|
+
const q = exec(executor);
|
|
60
|
+
const r = await q(
|
|
61
|
+
`SELECT n.id, n.page_id, n.page_path, n.page_title, n.status, n.submitted_at, n.idea_ids,
|
|
62
|
+
left(n.body_md, 400) AS excerpt,
|
|
63
|
+
COALESCE(NULLIF(b.display_name, ''), b.github_login) AS author_name
|
|
64
|
+
FROM page_notes_docs n JOIN builders b ON b.id = n.author_id
|
|
65
|
+
WHERE n.page_id LIKE $1 AND n.status <> 'draft'
|
|
66
|
+
ORDER BY n.submitted_at DESC NULLS LAST, n.id DESC
|
|
67
|
+
LIMIT $2`,
|
|
68
|
+
[`${surface}:%`, limit],
|
|
69
|
+
);
|
|
70
|
+
return r.rows;
|
|
71
|
+
}
|
|
72
|
+
|
|
55
73
|
async function saveBody(id, authorId, bodyMd, executor) {
|
|
56
74
|
const q = exec(executor);
|
|
57
75
|
const r = await q(
|
|
@@ -170,6 +188,7 @@ module.exports = {
|
|
|
170
188
|
openDraft,
|
|
171
189
|
getNote,
|
|
172
190
|
listForPage,
|
|
191
|
+
listForSurface,
|
|
173
192
|
saveBody,
|
|
174
193
|
countImages,
|
|
175
194
|
addImage,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- page_notes_002_surface_index.sql — the Studio's list of the public site's
|
|
2
|
+
-- reviews (task 1004499) reads every submitted doc on ONE surface, newest first:
|
|
3
|
+
-- `page_id LIKE 'site:%'`. page_notes_by_page (001) is ordered by the database's
|
|
4
|
+
-- collation, which a prefix LIKE cannot seek on; text_pattern_ops orders by
|
|
5
|
+
-- bytes, so the prefix is an index range and the list stays a seek as the table
|
|
6
|
+
-- grows. Partial on submitted docs, like page_notes_by_page: drafts are never
|
|
7
|
+
-- listed.
|
|
8
|
+
CREATE INDEX IF NOT EXISTS page_notes_by_surface
|
|
9
|
+
ON page_notes_docs (page_id text_pattern_ops, submitted_at DESC) WHERE status <> 'draft';
|
|
10
|
+
|
|
11
|
+
INSERT INTO schema_migrations (version) VALUES ('page_notes_002_surface_index')
|
|
12
|
+
ON CONFLICT DO NOTHING;
|
|
@@ -148,12 +148,31 @@ function wholeDocIdea({ pageTitle, pageId, bodyMd }) {
|
|
|
148
148
|
return { title, body: str(bodyMd).replace(IMAGE_REF_RE, (_m, alt) => `[image${alt ? ': ' + alt : ''}]`).trim().slice(0, MAX_IDEA_BODY) };
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
//
|
|
151
|
+
// The public site's pages (task 1004499, the Artist review button): their docs
|
|
152
|
+
// are page ids on the 'site:' surface. Their page_path is a path on the PUBLIC
|
|
153
|
+
// host, which a link inside the hall cannot reach (the hall is its own host on
|
|
154
|
+
// the hub), so a site doc is read in the Studio, which lists them.
|
|
155
|
+
const SITE_SURFACE = 'site';
|
|
156
|
+
const STUDIO_PATH = '/builders/studio';
|
|
157
|
+
function isSiteNote(note) { return str(note && note.page_id).startsWith(`${SITE_SURFACE}:`); }
|
|
158
|
+
|
|
159
|
+
// Where a doc is read: its page, opened on the doc (a site doc: the Studio).
|
|
152
160
|
function noteLink(note) {
|
|
153
|
-
const path = str(note.page_path) || '/';
|
|
161
|
+
const path = isSiteNote(note) ? STUDIO_PATH : (str(note.page_path) || '/');
|
|
154
162
|
return `${path}${path.includes('?') ? '&' : '?'}note=${note.id}`;
|
|
155
163
|
}
|
|
156
164
|
|
|
165
|
+
// One row of the Studio's "From the live site" list (GET /page-notes/site),
|
|
166
|
+
// from a listForSurface row. The one mapper, so the Studio's test renders what
|
|
167
|
+
// the route answers.
|
|
168
|
+
function siteListItem(r) {
|
|
169
|
+
return {
|
|
170
|
+
id: String(r.id), page_id: r.page_id, page_path: r.page_path, page_title: r.page_title, status: r.status,
|
|
171
|
+
submitted_at: r.submitted_at, idea_count: (r.idea_ids || []).length, author_name: r.author_name,
|
|
172
|
+
excerpt: r.excerpt, link: noteLink(r),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
157
176
|
// The idea body: the split's words, then the way back to the full doc.
|
|
158
177
|
function ideaBody(idea, note, authorName) {
|
|
159
178
|
const who = str(authorName) || 'An artist';
|
|
@@ -216,5 +235,9 @@ module.exports = {
|
|
|
216
235
|
parseSplitReply,
|
|
217
236
|
wholeDocIdea,
|
|
218
237
|
noteLink,
|
|
238
|
+
SITE_SURFACE,
|
|
239
|
+
STUDIO_PATH,
|
|
240
|
+
isSiteNote,
|
|
241
|
+
siteListItem,
|
|
219
242
|
ideaBody,
|
|
220
243
|
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// POST /page-notes open (or resume) my draft for a page
|
|
5
5
|
// GET /page-notes?page_id= my draft for the page + the page's reference docs
|
|
6
|
+
// GET /page-notes/site the public site's reviews, for the Studio (task 1004499)
|
|
6
7
|
// GET /page-notes/:id one doc, its images and the ideas it became
|
|
7
8
|
// PATCH /page-notes/:id save my draft's text
|
|
8
9
|
// POST /page-notes/:id/images add a reference image to my draft (base64 JSON)
|
|
@@ -156,6 +157,14 @@ module.exports = function buildPageNotesRouter() {
|
|
|
156
157
|
});
|
|
157
158
|
}));
|
|
158
159
|
|
|
160
|
+
// The public site's reviews (task 1004499), for the Studio's list. Mounted
|
|
161
|
+
// before /page-notes/:id so 'site' is never read as a doc id.
|
|
162
|
+
router.get('/page-notes/site', api.requireBuilder, api.asyncHandler('GET /page-notes/site', async (req, res) => {
|
|
163
|
+
const rows = await db.listForSurface(N.SITE_SURFACE, { limit: 20 });
|
|
164
|
+
res.set('Cache-Control', 'no-store');
|
|
165
|
+
res.json({ notes: rows.map(N.siteListItem) });
|
|
166
|
+
}));
|
|
167
|
+
|
|
159
168
|
router.get('/page-notes/images/:id', api.requireBuilder, api.asyncHandler('GET /page-notes/images/:id', async (req, res) => {
|
|
160
169
|
const id = String(req.params.id || '');
|
|
161
170
|
if (!ID_RE.test(id)) return res.fail('invalid_image_id', 400);
|