GUOSINT BOARD / {requestedEditMode && caseState.editingAllowed ? 'LEVEL EDITOR' : 'CASE TERMINAL'}
@@ -608,7 +609,7 @@ export function App() {
-
+
diff --git a/src/inventory.tsx b/src/inventory.tsx
index 57666bc..52f70b1 100644
--- a/src/inventory.tsx
+++ b/src/inventory.tsx
@@ -85,13 +85,13 @@ function ToolRack({ index }: { index: number }) {
return
}
-export function Inventory({ onClose, session }: { onClose?: () => void; session?: PhoneSession }) {
+export function Inventory({ onClose, session, onTearToBoard }: { onClose?: () => void; session?: PhoneSession; onTearToBoard?: (text: string) => void }) {
const [index, setIndex] = useState(0)
const [active, setActive] = useState
- {active === 'phone' ? : }
+ {active === 'phone' ? : }
return
@@ -108,13 +108,30 @@ export function Inventory({ onClose, session }: { onClose?: () => void; session?
}
-// Placeholder notebook tool — a lined page you can scribble on (not yet persisted).
-function NotebookTool() {
- const [text, setText] = useState('')
+// Field notebook: the lines captured from NPCs during play. A page can be torn off
+// onto the board as a note (when a board tear handler is available).
+function NotebookTool({ playthroughId, onTear }: { playthroughId?: string; onTear?: (text: string) => void }) {
+ const [pages, setPages] = useState<{ id: string; text: string }[]>([])
+ useEffect(() => {
+ if (!playthroughId) return
+ fetch(`/api/playthroughs/${playthroughId}/notebook`).then(r => r.ok ? r.json() : []).then(setPages).catch(() => {})
+ }, [playthroughId])
+ const remove = async (id: string) => {
+ if (playthroughId) await fetch(`/api/playthroughs/${playthroughId}/notebook/${id}`, { method: 'DELETE' })
+ setPages(list => list.filter(page => page.id !== id))
+ }
+ const tear = (page: { id: string; text: string }) => { onTear?.(page.text); void remove(page.id) }
return FIELD NOTEBOOK
-
- { void advance() }} />
if (node.kind === 'dialogue' && node.utterances) return { void advance(terminalKey) }}
- onAward={utteranceId => { if (state) void fetch(`/api/playthroughs/${state.playthrough.id}/utterances/${utteranceId}/reach`, { method: 'POST' }) }} />
+ onAward={utteranceId => { if (state) void fetch(`/api/playthroughs/${state.playthrough.id}/utterances/${utteranceId}/reach`, { method: 'POST' }) }}
+ onCapture={(text, utteranceId) => { if (state) void fetch(`/api/playthroughs/${state.playthrough.id}/notebook`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text, utteranceId }) }) }} />
return
}
diff --git a/src/styles.css b/src/styles.css
index 54ebfc5..9d6218d 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -787,6 +787,16 @@ button:focus-visible { outline: 2px solid #e99a44; outline-offset: 2px; }
.inv-back:hover { border-color: #cdea6a; }
/* notebook tool (placeholder) */
.notebook { position: fixed; inset: 0; z-index: 500; display: grid; place-items: center; background: radial-gradient(120% 90% at 50% 30%, #1c1a12 0%, #0a0906 75%); }
-.notebook-page { width: min(560px, 88vw); height: min(72vh, 720px); background: repeating-linear-gradient(#f4ecd6 0 30px, #e3d8b8 30px 31px); box-shadow: 0 20px 60px #0009; padding: 26px 30px; display: flex; flex-direction: column; gap: 14px; }
+.notebook-page { width: min(560px, 88vw); height: min(72vh, 720px); background: repeating-linear-gradient(#f4ecd6 0 30px, #e3d8b8 30px 31px); box-shadow: 0 20px 60px #0009; padding: 26px 30px; display: flex; flex-direction: column; gap: 12px; overflow-y: auto; }
.notebook-head { font: 700 13px IBM Plex Mono, monospace; letter-spacing: 3px; color: #6a5a3a; }
-.notebook-page textarea { flex: 1; background: transparent; border: none; outline: none; resize: none; font: 16px/31px "Courier New", monospace; color: #3a3020; }
+.notebook-empty { font: 22px/1.3 "Reenie Beanie", cursive; color: #8a7a52; }
+.notebook-note { border-bottom: 1px dashed #c8b98f; padding-bottom: 8px; }
+.notebook-note p { margin: 0 0 6px; font: 27px/30px "Reenie Beanie", cursive; color: #26356b; }
+.notebook-note-actions { display: flex; gap: 8px; }
+.notebook-note-actions button { font: 11px IBM Plex Mono, monospace; letter-spacing: 1px; background: #e8dcbc; border: 1px solid #c0ad7f; color: #5a4a2a; padding: 3px 9px; cursor: pointer; }
+.notebook-note-actions button.ghost { background: transparent; border-color: transparent; color: #a08a5a; }
+.notebook-note-actions button:hover { border-color: #6a5a3a; }
+.dialogue-capture { margin-left: 12px; font: 10px IBM Plex Mono, monospace; letter-spacing: 1px; background: #0a211de0; border: 1px solid #6f8f85; color: #cdea6a; padding: 2px 8px; cursor: pointer; vertical-align: middle; }
+.dialogue-capture:hover { border-color: #cdea6a; color: #eafaa0; }
+/* A note torn to the board reads as handwriting too. */
+.evidence-card.note .card-content p { font-family: "Reenie Beanie", cursive; font-size: 19px; line-height: 1.05; color: #26356b; }
{current.speaker.name}{current.speaker.role && {current.speaker.role}}
+ {current.speaker.name}{current.speaker.role && {current.speaker.role}}
+ {onCapture && !inline && current.utterer === 'npc' && done && }
+
{fullText.slice(0, charCount)}{done ? '' : '▍'}
{showChoices ?{options.map(option => )}
diff --git a/src/play.tsx b/src/play.tsx
index c36ba6d..0699c5c 100644
--- a/src/play.tsx
+++ b/src/play.tsx
@@ -103,6 +103,7 @@ export function Play() {
if (node.kind === 'merit') return GU
{node.label}