privateboard 0.1.11 → 0.1.13
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/dist/cli.js +4260 -1660
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
- package/public/adjourn-overlay.css +8 -4
- package/public/agent-profile.css +345 -0
- package/public/agent-profile.js +222 -3
- package/public/app.js +4990 -149
- package/public/home.html +609 -1
- package/public/i18n.js +49 -385
- package/public/index.html +9549 -5314
- package/public/new-agent.js +148 -37
- package/public/quote-cta.css +7 -3
- package/public/quote-cta.js +21 -0
- package/public/report.html +109 -12
- package/public/room-settings.css +67 -0
- package/public/room-settings.js +72 -6
- package/public/themes.css +38 -0
- package/public/typing-sfx.js +87 -1
- package/public/user-settings.js +3 -1
- package/public/voice-replay.css +70 -1
- package/public/voice-replay.js +265 -2
package/public/new-agent.js
CHANGED
|
@@ -422,29 +422,107 @@
|
|
|
422
422
|
});
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
-
|
|
425
|
+
/** Module-scoped overrides set by `open(options)`. Cleared on
|
|
426
|
+
* close so the next default open() starts clean. */
|
|
427
|
+
let _submitOverride = null;
|
|
428
|
+
let _onCancelOverride = null;
|
|
429
|
+
let _classificationOverride = null;
|
|
430
|
+
let _footMetaOverride = null;
|
|
431
|
+
let _createLabelOverride = null;
|
|
432
|
+
|
|
433
|
+
/** Open the overlay. With no args, this is the default Signal
|
|
434
|
+
* manual-config flow (POST /api/agents on Create). With options,
|
|
435
|
+
* callers can pre-fill the form, intercept submit, and tweak
|
|
436
|
+
* the chrome — this lets the Full-persona save flow reuse the
|
|
437
|
+
* exact same overlay instead of forking the UI.
|
|
438
|
+
*
|
|
439
|
+
* options.prefill = { name, bio, instruction, modelV,
|
|
440
|
+
* avatarSeed, avatarPath }
|
|
441
|
+
* options.onSubmit = async (data, helpers) => {}
|
|
442
|
+
* data · { name, handle, bio, instruction, modelV,
|
|
443
|
+
* avatarPath, avatarSeed }
|
|
444
|
+
* helpers · { close }
|
|
445
|
+
* Throw / reject to keep the overlay open and surface an
|
|
446
|
+
* error toast (alert).
|
|
447
|
+
* options.onCancel · fired before close() when the user clicks
|
|
448
|
+
* cancel / X / backdrop · use to e.g. preserve build state.
|
|
449
|
+
* options.classificationLeft · text shown in the upper-left
|
|
450
|
+
* classification bar (default · "DIRECTOR · NEW")
|
|
451
|
+
* options.footMeta · text shown in the foot-meta line
|
|
452
|
+
* options.createLabel · text on the Create button (default ·
|
|
453
|
+
* "Create director")
|
|
454
|
+
*/
|
|
455
|
+
function open(options) {
|
|
426
456
|
if (!overlay) return;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
457
|
+
options = options || null;
|
|
458
|
+
const prefill = (options && options.prefill) || null;
|
|
459
|
+
|
|
460
|
+
// Reset form. When prefill is supplied, populate after reset.
|
|
461
|
+
modal.querySelector(".na-name-input").value = prefill && prefill.name ? prefill.name : "";
|
|
462
|
+
modal.querySelector(".na-desc-input").value = prefill && prefill.bio ? prefill.bio : "";
|
|
463
|
+
modal.querySelector(".na-instr-input").value = prefill && prefill.instruction ? prefill.instruction : "";
|
|
464
|
+
|
|
465
|
+
// Avatar · pre-seed when caller provides one (e.g. Full-mode
|
|
466
|
+
// build's stashed seed). Falls back to the placeholder otherwise.
|
|
467
|
+
if (prefill && prefill.avatarSeed) {
|
|
468
|
+
avatarState = { placeholder: false, seed: prefill.avatarSeed, roll: 1 };
|
|
469
|
+
} else {
|
|
470
|
+
avatarState = { placeholder: true, seed: null, roll: 0 };
|
|
471
|
+
}
|
|
432
472
|
paintAvatar();
|
|
433
473
|
refreshAll();
|
|
434
474
|
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
475
|
+
// Model · push prefill model into the shared agent-composer
|
|
476
|
+
// state so the chip + downstream readers (na-create POST etc.)
|
|
477
|
+
// pick it up. Cleared by the user via the chip dropdown.
|
|
478
|
+
if (prefill && prefill.modelV
|
|
479
|
+
&& window.app && typeof window.app.setAgentComposerModel === "function") {
|
|
480
|
+
try { window.app.setAgentComposerModel(prefill.modelV); } catch (_) { /* */ }
|
|
481
|
+
}
|
|
438
482
|
paintModelLabel();
|
|
439
483
|
|
|
484
|
+
// Apply chrome overrides.
|
|
485
|
+
_submitOverride = (options && typeof options.onSubmit === "function") ? options.onSubmit : null;
|
|
486
|
+
_onCancelOverride = (options && typeof options.onCancel === "function") ? options.onCancel : null;
|
|
487
|
+
_classificationOverride = (options && options.classificationLeft) ? String(options.classificationLeft) : null;
|
|
488
|
+
_footMetaOverride = (options && options.footMeta) ? String(options.footMeta) : null;
|
|
489
|
+
_createLabelOverride = (options && options.createLabel) ? String(options.createLabel) : null;
|
|
490
|
+
applyChromeOverrides();
|
|
491
|
+
|
|
440
492
|
overlay.classList.add("open");
|
|
441
493
|
overlay.setAttribute("aria-hidden", "false");
|
|
442
494
|
document.body.style.overflow = "hidden";
|
|
443
495
|
setTimeout(() => modal.querySelector(".na-name-input").focus(), 80);
|
|
444
496
|
applyNewAgentI18n();
|
|
497
|
+
// applyChromeOverrides runs AFTER applyNewAgentI18n on i18n
|
|
498
|
+
// events too · the i18n pass would otherwise reset our custom
|
|
499
|
+
// strings.
|
|
500
|
+
applyChromeOverrides();
|
|
445
501
|
refreshProviderStatus();
|
|
446
502
|
}
|
|
447
503
|
|
|
504
|
+
/** Mirror the override state into the actual DOM. Re-runs after
|
|
505
|
+
* i18n applies so user-facing labels survive locale changes. */
|
|
506
|
+
function applyChromeOverrides() {
|
|
507
|
+
if (!modal) return;
|
|
508
|
+
const classEl = modal.querySelector(".na-classification > span:first-child");
|
|
509
|
+
if (classEl && _classificationOverride) {
|
|
510
|
+
classEl.innerHTML = `<span class="dot">●</span> ${escape(_classificationOverride)}`;
|
|
511
|
+
}
|
|
512
|
+
const footEl = modal.querySelector(".na-foot-meta");
|
|
513
|
+
if (footEl && _footMetaOverride) {
|
|
514
|
+
footEl.textContent = _footMetaOverride;
|
|
515
|
+
footEl.classList.add("na-foot-meta-override");
|
|
516
|
+
} else if (footEl) {
|
|
517
|
+
footEl.classList.remove("na-foot-meta-override");
|
|
518
|
+
}
|
|
519
|
+
const createBtn = modal.querySelector(".na-create");
|
|
520
|
+
if (createBtn && _createLabelOverride) {
|
|
521
|
+
const labelSpan = createBtn.querySelector("span:not(.na-create-mark)");
|
|
522
|
+
if (labelSpan) labelSpan.textContent = _createLabelOverride;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
448
526
|
/** Read the user's current agent-model selection (shared with the
|
|
449
527
|
* AI composer) and paint it into the overlay's `cmp-dd-value` span.
|
|
450
528
|
* Falls back to a dash when app or its model resolver isn't ready. */
|
|
@@ -465,11 +543,25 @@
|
|
|
465
543
|
span.textContent = label;
|
|
466
544
|
}
|
|
467
545
|
|
|
468
|
-
function close() {
|
|
546
|
+
function close(opts) {
|
|
469
547
|
if (!overlay) return;
|
|
548
|
+
const fromCancel = !!(opts && opts.fromCancel);
|
|
549
|
+
// Notify the caller (e.g. Full-mode persona flow) that the user
|
|
550
|
+
// dismissed without saving · lets them preserve the build so it
|
|
551
|
+
// can be re-opened. Fired BEFORE we wipe the override state so
|
|
552
|
+
// the callback can still see options it set.
|
|
553
|
+
if (fromCancel && _onCancelOverride) {
|
|
554
|
+
try { _onCancelOverride(); } catch (_) { /* */ }
|
|
555
|
+
}
|
|
470
556
|
overlay.classList.remove("open");
|
|
471
557
|
overlay.setAttribute("aria-hidden", "true");
|
|
472
558
|
document.body.style.overflow = "";
|
|
559
|
+
// Reset overrides so the next default open() starts clean.
|
|
560
|
+
_submitOverride = null;
|
|
561
|
+
_onCancelOverride = null;
|
|
562
|
+
_classificationOverride = null;
|
|
563
|
+
_footMetaOverride = null;
|
|
564
|
+
_createLabelOverride = null;
|
|
473
565
|
}
|
|
474
566
|
|
|
475
567
|
function slugify(s) {
|
|
@@ -628,16 +720,18 @@
|
|
|
628
720
|
overlay = document.getElementById("new-agent-overlay");
|
|
629
721
|
modal = overlay.querySelector(".new-agent-modal");
|
|
630
722
|
|
|
631
|
-
// Close
|
|
632
|
-
|
|
633
|
-
|
|
723
|
+
// Close · all dismissal paths flag fromCancel:true so the
|
|
724
|
+
// override hook fires (Full-mode persona flow uses it to
|
|
725
|
+
// preserve the build state when the overlay is dismissed).
|
|
726
|
+
modal.querySelector(".na-close").addEventListener("click", () => close({ fromCancel: true }));
|
|
727
|
+
modal.querySelector(".na-cancel").addEventListener("click", () => close({ fromCancel: true }));
|
|
634
728
|
overlay.addEventListener("click", (e) => {
|
|
635
|
-
if (e.target === overlay) close();
|
|
729
|
+
if (e.target === overlay) close({ fromCancel: true });
|
|
636
730
|
});
|
|
637
731
|
document.addEventListener("keydown", (e) => {
|
|
638
732
|
if (e.key === "Escape" && overlay.classList.contains("open")) {
|
|
639
733
|
e.stopImmediatePropagation();
|
|
640
|
-
close();
|
|
734
|
+
close({ fromCancel: true });
|
|
641
735
|
}
|
|
642
736
|
});
|
|
643
737
|
|
|
@@ -702,39 +796,56 @@
|
|
|
702
796
|
create.innerHTML = `<span class="na-create-mark">◆</span><span>${escape(t("na_creating"))}</span>`;
|
|
703
797
|
|
|
704
798
|
try {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
await
|
|
799
|
+
if (_submitOverride) {
|
|
800
|
+
// Caller-supplied submit path · used by the Full-persona
|
|
801
|
+
// save flow to POST to /generate-persona/:jobId/save
|
|
802
|
+
// instead of the default /api/agents. The override
|
|
803
|
+
// owns success-state handling (refresh, close, etc.)
|
|
804
|
+
// but we still close on resolve as a safety.
|
|
805
|
+
await _submitOverride({
|
|
806
|
+
name, bio, instruction, modelV, avatarPath,
|
|
807
|
+
avatarSeed, avatarRoll,
|
|
808
|
+
}, { close, escape, t });
|
|
809
|
+
// Close `fromCancel:false` so the override's onCancel
|
|
810
|
+
// hook doesn't fire (we just saved successfully).
|
|
811
|
+
close();
|
|
812
|
+
} else {
|
|
813
|
+
const res = await fetch("/api/agents", {
|
|
814
|
+
method: "POST",
|
|
815
|
+
headers: { "content-type": "application/json" },
|
|
816
|
+
body: JSON.stringify({ name, bio, instruction, modelV, avatarPath }),
|
|
817
|
+
});
|
|
818
|
+
if (!res.ok) {
|
|
819
|
+
const err = await res.json().catch(() => ({}));
|
|
820
|
+
throw new Error(err.error || res.statusText);
|
|
821
|
+
}
|
|
822
|
+
const created = await res.json();
|
|
823
|
+
// Refresh app.agents so the sidebar + agentsById register the
|
|
824
|
+
// new director immediately. Falls back gracefully if app isn't
|
|
825
|
+
// booted (shouldn't happen in normal flows).
|
|
826
|
+
if (window.app && typeof window.app.refreshAgents === "function") {
|
|
827
|
+
await window.app.refreshAgents();
|
|
828
|
+
}
|
|
829
|
+
// Hand the new agent's id to anyone watching for the event.
|
|
830
|
+
try {
|
|
831
|
+
window.dispatchEvent(new CustomEvent("boardroom:agent-created", { detail: created }));
|
|
832
|
+
} catch (_) { /* */ }
|
|
833
|
+
close();
|
|
720
834
|
}
|
|
721
|
-
// Hand the new agent's id to anyone watching for the event.
|
|
722
|
-
try {
|
|
723
|
-
window.dispatchEvent(new CustomEvent("boardroom:agent-created", { detail: created }));
|
|
724
|
-
} catch (_) { /* */ }
|
|
725
|
-
close();
|
|
726
835
|
} catch (e) {
|
|
727
836
|
const msg = e && e.message ? e.message : String(e);
|
|
728
837
|
alert(t("na_create_fail", { msg }));
|
|
729
838
|
create.disabled = false;
|
|
730
839
|
create.innerHTML = `<span class="na-create-mark">◆</span><span data-i18n-na="na_create"></span>`;
|
|
731
840
|
applyNewAgentI18n();
|
|
841
|
+
applyChromeOverrides();
|
|
732
842
|
}
|
|
733
843
|
});
|
|
734
844
|
}
|
|
735
845
|
|
|
736
|
-
// Public API
|
|
737
|
-
|
|
846
|
+
// Public API · options pass-through lets the Full-persona save
|
|
847
|
+
// flow open the same overlay with prefill + custom submit.
|
|
848
|
+
window.openNewAgent = function (options) { if (!overlay) init(); open(options); };
|
|
738
849
|
window.closeNewAgent = close;
|
|
739
850
|
|
|
740
851
|
if (document.readyState === "loading") {
|
package/public/quote-cta.css
CHANGED
|
@@ -87,13 +87,15 @@
|
|
|
87
87
|
|
|
88
88
|
/* ─── Save toast ─────────────────────────────────────────────
|
|
89
89
|
Lightweight feedback after a successful POST /api/notes (or a
|
|
90
|
-
failure).
|
|
90
|
+
failure). Top-center anchored so it doesn't overlap with the
|
|
91
|
+
bottom bar's input controls + voice-replay panel — which were
|
|
92
|
+
covering the previous bottom-center placement. Lime for ok,
|
|
91
93
|
red-tinted for error. Click to dismiss early. */
|
|
92
94
|
.qcta-toast {
|
|
93
95
|
position: fixed;
|
|
94
|
-
|
|
96
|
+
top: 72px; /* clears the topbar / classification strip */
|
|
95
97
|
left: 50%;
|
|
96
|
-
transform: translate(-50%, 8px);
|
|
98
|
+
transform: translate(-50%, -8px); /* starts above resting, slides DOWN */
|
|
97
99
|
z-index: 1600;
|
|
98
100
|
background: var(--panel, #131312);
|
|
99
101
|
border: 0.5px solid var(--lime, #6FB572);
|
|
@@ -107,6 +109,8 @@
|
|
|
107
109
|
opacity: 0;
|
|
108
110
|
transition: opacity 0.16s ease-out, transform 0.16s ease-out;
|
|
109
111
|
white-space: nowrap;
|
|
112
|
+
/* Drop shadow flipped so the elevation hint reads as "tray
|
|
113
|
+
dropping from above" rather than "rising from below." */
|
|
110
114
|
box-shadow: 0 14px 30px -14px rgba(0, 0, 0, 0.55);
|
|
111
115
|
}
|
|
112
116
|
.qcta-toast.open {
|
package/public/quote-cta.js
CHANGED
|
@@ -494,8 +494,28 @@
|
|
|
494
494
|
// some paths but not all; using a self-contained one keeps this
|
|
495
495
|
// module independent. Lime for ok, red-tinted for error. Auto-
|
|
496
496
|
// dismisses after 1.8s; click to dismiss early.
|
|
497
|
+
//
|
|
498
|
+
// Horizontal anchor · the toast sits over the CHAT COLUMN, not
|
|
499
|
+
// the viewport center. Centering on the viewport pulls the
|
|
500
|
+
// toast right of the chat (the sidebar eats ~280px on the left)
|
|
501
|
+
// and reads as visually skewed. Recomputed every show so the
|
|
502
|
+
// toast tracks sidebar collapse / window resize.
|
|
497
503
|
let toastEl = null;
|
|
498
504
|
let toastTimer = null;
|
|
505
|
+
function positionToast() {
|
|
506
|
+
if (!toastEl) return;
|
|
507
|
+
const chat = document.querySelector(".chat-col") || document.querySelector('[data-main-view="room"]');
|
|
508
|
+
if (!chat) {
|
|
509
|
+
// Fallback · centre on viewport when the chat column isn't
|
|
510
|
+
// mounted (e.g. notes page · toast still useful but anchor
|
|
511
|
+
// missing). Same behaviour as before this fix.
|
|
512
|
+
toastEl.style.left = "50%";
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const r = chat.getBoundingClientRect();
|
|
516
|
+
const cx = r.left + r.width / 2;
|
|
517
|
+
toastEl.style.left = cx + "px";
|
|
518
|
+
}
|
|
499
519
|
function toast(msg, kind) {
|
|
500
520
|
if (!toastEl) {
|
|
501
521
|
toastEl = document.createElement("div");
|
|
@@ -506,6 +526,7 @@
|
|
|
506
526
|
toastEl.classList.remove("kind-ok", "kind-error");
|
|
507
527
|
toastEl.classList.add("kind-" + (kind === "error" ? "error" : "ok"));
|
|
508
528
|
toastEl.textContent = msg;
|
|
529
|
+
positionToast();
|
|
509
530
|
toastEl.classList.add("open");
|
|
510
531
|
if (toastTimer) clearTimeout(toastTimer);
|
|
511
532
|
toastTimer = setTimeout(() => {
|
package/public/report.html
CHANGED
|
@@ -131,6 +131,11 @@
|
|
|
131
131
|
U+FF00-FFEF;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/* Print-paper backstop · hidden on screen; shown only during the
|
|
135
|
+
print render. Inside @media print (below) it expands to a
|
|
136
|
+
position:fixed full-page background. */
|
|
137
|
+
[data-print-paper-fill] { display: none; }
|
|
138
|
+
|
|
134
139
|
@media print {
|
|
135
140
|
*, *::before, *::after {
|
|
136
141
|
print-color-adjust: exact !important;
|
|
@@ -194,14 +199,36 @@
|
|
|
194
199
|
|
|
195
200
|
/* Page-frame background · matches body's printed colour per spine
|
|
196
201
|
so any PDF engine that paints html beyond body's box (some
|
|
197
|
-
legacy print pipelines) stays coherent.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
legacy print pipelines) stays coherent. Default white; per-
|
|
203
|
+
spine selectors below override for the light-paper spines.
|
|
204
|
+
The earlier `:has()` selector silently failed on browsers
|
|
205
|
+
that don't support `:has()` in print mode (some Chrome
|
|
206
|
+
versions, headless / Save-as-PDF pipelines), leaving html
|
|
207
|
+
white and the dark spine PDF unreadable. We now mirror the
|
|
208
|
+
`data-spine` attribute onto `<html>` itself in `swapSpine`
|
|
209
|
+
(see JS below), so a plain attribute selector picks up the
|
|
210
|
+
light-paper colour reliably. */
|
|
201
211
|
html { background: #FFFFFF !important; }
|
|
202
|
-
html
|
|
203
|
-
html
|
|
204
|
-
html
|
|
212
|
+
html[data-spine="anthropic-essay"] { background: #F4F0E8 !important; }
|
|
213
|
+
html[data-spine="a16z-thesis"] { background: #F7F3E8 !important; }
|
|
214
|
+
html[data-spine="boardroom-dark"] { background: #FAF7F0 !important; }
|
|
215
|
+
|
|
216
|
+
/* Print-paper backstop · a `position: fixed; inset: 0` div with
|
|
217
|
+
`print-color-adjust: exact` reliably paints on every printed
|
|
218
|
+
page in Chrome, where `html { background }` and `body {
|
|
219
|
+
background }` are inconsistently honoured. JS sets the
|
|
220
|
+
background colour per spine in `swapSpine` so this paints the
|
|
221
|
+
cream / white paper colour edge-to-edge. z-index: -1 keeps it
|
|
222
|
+
behind all content. */
|
|
223
|
+
[data-print-paper-fill] {
|
|
224
|
+
display: block !important;
|
|
225
|
+
position: fixed !important;
|
|
226
|
+
inset: 0 !important;
|
|
227
|
+
z-index: -1 !important;
|
|
228
|
+
print-color-adjust: exact !important;
|
|
229
|
+
-webkit-print-color-adjust: exact !important;
|
|
230
|
+
pointer-events: none !important;
|
|
231
|
+
}
|
|
205
232
|
|
|
206
233
|
/* Container resets so .doc fills the printable area instead of
|
|
207
234
|
being clipped at its 880px on-screen max-width. */
|
|
@@ -2822,6 +2849,16 @@
|
|
|
2822
2849
|
</head>
|
|
2823
2850
|
<body>
|
|
2824
2851
|
|
|
2852
|
+
<!-- Print-only paper background · a position:fixed div that fills the
|
|
2853
|
+
entire page on every printed sheet. Chrome's print pipeline is
|
|
2854
|
+
historically unreliable about painting `html { background }` and
|
|
2855
|
+
`body { background }` (even with print-color-adjust: exact),
|
|
2856
|
+
causing dark-spine reports to save as PDFs with the wrong (white)
|
|
2857
|
+
bleed colour. A real DIV with print-color-adjust paints reliably
|
|
2858
|
+
on every page. JS sets the background colour per spine in
|
|
2859
|
+
`swapSpine`. Hidden on screen. -->
|
|
2860
|
+
<div class="print-paper-fill" data-print-paper-fill aria-hidden="true"></div>
|
|
2861
|
+
|
|
2825
2862
|
<div class="top-rule">
|
|
2826
2863
|
<span class="crumb">Boardroom <span class="accent">·</span> Insights</span>
|
|
2827
2864
|
<div class="top-actions">
|
|
@@ -2897,6 +2934,15 @@
|
|
|
2897
2934
|
const href = `report/spines/${safe}.css`;
|
|
2898
2935
|
if (link.getAttribute("href") !== href) link.setAttribute("href", href);
|
|
2899
2936
|
document.body.setAttribute("data-spine", safe);
|
|
2937
|
+
// Mirror the spine onto <html> too so the @media print rule
|
|
2938
|
+
// `html[data-spine="..."] { background: ... }` resolves
|
|
2939
|
+
// reliably in every Chrome / Save-as-PDF pipeline. The
|
|
2940
|
+
// earlier `:has()`-based selector silently failed in some
|
|
2941
|
+
// print pipelines, leaving html white and the dark-spine PDF
|
|
2942
|
+
// showing wrong text colours against an unintended white
|
|
2943
|
+
// page bleed. Setting data-spine on the root element bypasses
|
|
2944
|
+
// `:has()` entirely.
|
|
2945
|
+
document.documentElement.setAttribute("data-spine", safe);
|
|
2900
2946
|
// Mark the document as CJK when the brief is in Chinese (or any
|
|
2901
2947
|
// CJK script). Several spines — anthropic-essay, a16z-thesis,
|
|
2902
2948
|
// boardroom-dark in the bottom-line callout — use `font-style:
|
|
@@ -2909,10 +2955,27 @@
|
|
|
2909
2955
|
const sample = ((brief && brief.title) || "") + " " + ((brief && brief.bodyMd) || "");
|
|
2910
2956
|
const isCjk = /[㐀-鿿-ヿ가-]/.test(sample);
|
|
2911
2957
|
document.body.classList.toggle("is-cjk", isCjk);
|
|
2912
|
-
// Inject (or replace) the per-spine
|
|
2913
|
-
//
|
|
2914
|
-
//
|
|
2915
|
-
//
|
|
2958
|
+
// Inject (or replace) the per-spine print-paper rule. We paint
|
|
2959
|
+
// the cream / white paper colour at THREE levels because Chrome
|
|
2960
|
+
// / Save-as-PDF pipelines silently drop one or another:
|
|
2961
|
+
// · `@page { background }` — paints the @page bleed
|
|
2962
|
+
// (technically the canonical surface, but historically buggy
|
|
2963
|
+
// and ignored by some print pipelines)
|
|
2964
|
+
// · `html { background }` — paints the document canvas;
|
|
2965
|
+
// overridden by the static `html { background: #FFFFFF }`
|
|
2966
|
+
// default in @media print, but THIS injected rule wins on
|
|
2967
|
+
// specificity (it loads after the static stylesheet)
|
|
2968
|
+
// · `body { background }` — paints the body's content box;
|
|
2969
|
+
// the most reliable surface but only fills inside @page
|
|
2970
|
+
// margins
|
|
2971
|
+
// With all three forced to the same paper colour AND
|
|
2972
|
+
// print-color-adjust: exact !important, the saved PDF reliably
|
|
2973
|
+
// shows the spine's paper colour edge-to-edge regardless of
|
|
2974
|
+
// which surface Chrome ends up honouring on a given OS / Chrome
|
|
2975
|
+
// version. The earlier single-`@page`-rule injection was the
|
|
2976
|
+
// root cause of the "dark report → white PDF" bug — Chrome
|
|
2977
|
+
// dropped the @page background and the static `html {
|
|
2978
|
+
// background: #FFFFFF }` default won.
|
|
2916
2979
|
const paper = SPINE_PAPER[safe] || "#FFFFFF";
|
|
2917
2980
|
let pageStyle = document.getElementById("spine-page-bg");
|
|
2918
2981
|
if (!pageStyle) {
|
|
@@ -2920,7 +2983,24 @@
|
|
|
2920
2983
|
pageStyle.id = "spine-page-bg";
|
|
2921
2984
|
document.head.appendChild(pageStyle);
|
|
2922
2985
|
}
|
|
2923
|
-
pageStyle.textContent =
|
|
2986
|
+
pageStyle.textContent = `
|
|
2987
|
+
@media print {
|
|
2988
|
+
@page { background: ${paper}; }
|
|
2989
|
+
html, body {
|
|
2990
|
+
background: ${paper} !important;
|
|
2991
|
+
print-color-adjust: exact !important;
|
|
2992
|
+
-webkit-print-color-adjust: exact !important;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
`;
|
|
2996
|
+
// Print-paper backstop · the position:fixed div in <body> needs
|
|
2997
|
+
// its background colour set directly (inline style) so it paints
|
|
2998
|
+
// the right paper colour on every printed page. This is the
|
|
2999
|
+
// FALLBACK surface — Chrome's print pipeline drops `html` and
|
|
3000
|
+
// `body` backgrounds inconsistently, but always paints a real
|
|
3001
|
+
// div with print-color-adjust: exact.
|
|
3002
|
+
const fill = document.querySelector("[data-print-paper-fill]");
|
|
3003
|
+
if (fill) fill.style.background = paper;
|
|
2924
3004
|
}
|
|
2925
3005
|
|
|
2926
3006
|
function escape(s) {
|
|
@@ -3400,6 +3480,23 @@
|
|
|
3400
3480
|
// bullets, ordered lists, paragraphs, blockquotes, fenced code blocks
|
|
3401
3481
|
// (incl. ```mermaid for diagrams), and pipe tables.
|
|
3402
3482
|
function renderMarkdown(md) {
|
|
3483
|
+
// Pre-pre-pass · normalise fenced-block openers that the LLM
|
|
3484
|
+
// sometimes glues onto the tail of a paragraph (`...end of prose.```views-compared`).
|
|
3485
|
+
// The fence-recognition regex below requires the opener to be on
|
|
3486
|
+
// its OWN line (`/^```.../`), so a mid-line opener falls through
|
|
3487
|
+
// and the fenced body renders as raw text full of `{` / `}` / `:`
|
|
3488
|
+
// / quote symbols — exactly the "符号导致内容混乱" report we
|
|
3489
|
+
// get when the brief writer writes the closing prose without a
|
|
3490
|
+
// newline before the dispatch fence. Insert a blank line in
|
|
3491
|
+
// front of every fence opener that isn't already at the start
|
|
3492
|
+
// of a line. Keep paired closing fences alone (they're always
|
|
3493
|
+
// preceded by content inside the block — that's fine).
|
|
3494
|
+
// Match openers only · `[\w-]+` requires a non-empty lang tag so
|
|
3495
|
+
// bare closing fences (`\`\`\`\n`) don't get rewritten too. Without
|
|
3496
|
+
// that distinction, every closing fence also picks up an extra
|
|
3497
|
+
// blank line and the body slice includes a trailing empty line.
|
|
3498
|
+
md = String(md || "").replace(/([^\n])(\n?)```([\w-]+)\s*\n/g,
|
|
3499
|
+
(_m, before, _newline, lang) => `${before}\n\n\`\`\`${lang}\n`);
|
|
3403
3500
|
// Pre-pass · pull out fenced ```code blocks before splitting on blank
|
|
3404
3501
|
// lines, since a code block can legally contain blank lines.
|
|
3405
3502
|
const placeholders = [];
|
package/public/room-settings.css
CHANGED
|
@@ -472,6 +472,73 @@
|
|
|
472
472
|
color: var(--lime, #6FB572);
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
/* Vote-trigger radio group · two cells per row (Auto / Manual)
|
|
476
|
+
with the radio glyph + name pinned to the top line and a single
|
|
477
|
+
compact description line beneath. The previous stacked-column
|
|
478
|
+
layout wasted vertical space for two short binary options; the
|
|
479
|
+
2-up grid sits the choices side-by-side at content height so the
|
|
480
|
+
panel stays dense. The section's own .rs-config-row-hint above
|
|
481
|
+
carries the "when the chair opens the round-end vote" framing,
|
|
482
|
+
so each cell only needs its own short rationale. */
|
|
483
|
+
.rs-vote-trigger-grp {
|
|
484
|
+
display: grid;
|
|
485
|
+
grid-template-columns: 1fr 1fr;
|
|
486
|
+
gap: 6px;
|
|
487
|
+
}
|
|
488
|
+
.rs-radio-row {
|
|
489
|
+
display: grid;
|
|
490
|
+
grid-template-columns: 13px 1fr;
|
|
491
|
+
align-items: center;
|
|
492
|
+
column-gap: 8px;
|
|
493
|
+
row-gap: 2px;
|
|
494
|
+
padding: 7px 9px;
|
|
495
|
+
background: var(--bg, #0A0A0A);
|
|
496
|
+
border: 0.5px solid var(--line-bright, #2A2A26);
|
|
497
|
+
cursor: pointer;
|
|
498
|
+
transition: border-color 0.12s, background 0.12s;
|
|
499
|
+
}
|
|
500
|
+
.rs-radio-row:hover { border-color: var(--text-faint, #3A382F); }
|
|
501
|
+
.rs-radio-row input[type="radio"] {
|
|
502
|
+
grid-row: 1;
|
|
503
|
+
grid-column: 1;
|
|
504
|
+
margin: 0;
|
|
505
|
+
accent-color: var(--lime, #6FB572);
|
|
506
|
+
cursor: pointer;
|
|
507
|
+
width: 13px;
|
|
508
|
+
height: 13px;
|
|
509
|
+
flex-shrink: 0;
|
|
510
|
+
}
|
|
511
|
+
.rs-radio-label {
|
|
512
|
+
grid-row: 1 / span 2;
|
|
513
|
+
grid-column: 2;
|
|
514
|
+
display: flex;
|
|
515
|
+
flex-direction: column;
|
|
516
|
+
gap: 1px;
|
|
517
|
+
min-width: 0;
|
|
518
|
+
}
|
|
519
|
+
.rs-radio-name {
|
|
520
|
+
font-family: var(--font-human);
|
|
521
|
+
font-size: 12px;
|
|
522
|
+
font-weight: 600;
|
|
523
|
+
color: var(--text-soft, #8E8B83);
|
|
524
|
+
letter-spacing: -0.005em;
|
|
525
|
+
line-height: 1.2;
|
|
526
|
+
}
|
|
527
|
+
.rs-radio-desc {
|
|
528
|
+
font-family: var(--font-human);
|
|
529
|
+
font-size: 11px;
|
|
530
|
+
color: var(--text-faint, #5C5A4D);
|
|
531
|
+
letter-spacing: -0.003em;
|
|
532
|
+
line-height: 1.35;
|
|
533
|
+
}
|
|
534
|
+
.rs-radio-row.active {
|
|
535
|
+
border-color: var(--lime, #6FB572);
|
|
536
|
+
background: var(--panel-2, #1A1A18);
|
|
537
|
+
}
|
|
538
|
+
.rs-radio-row.active .rs-radio-name {
|
|
539
|
+
color: var(--lime, #6FB572);
|
|
540
|
+
}
|
|
541
|
+
|
|
475
542
|
/* Section label · matches the new composer's mono micro-type kicker.
|
|
476
543
|
No leading lime bullet — the inline count chip carries enough
|
|
477
544
|
visual weight, and dropping the bullet lets the labels sit flush
|