Phone runtime (2b): inventory phone on the board dials for real
The phone gains a session mode that reads the live directory (/phone) and dials (/dial); the inventory threads it through. On a campaign level the board shows an INVENTORY nav button that opens the tool rack, and connecting a call hands back to the campaign via /?resume=1 (a fast-path that resumes the current node instead of the splash). On Barricelli's note-board: open inventory -> phone -> dial 5550100 -> Glitch Hunter. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+10
-2
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { lazy, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { BookOpen, Building2, CalendarClock, Camera, Check, ChevronRight, CircleHelp, ClipboardCheck, FileText, FolderOpen, Hand, Image as ImageIcon, Images, Info, Link2, Minus, MousePointer2, Network, Newspaper, NotebookPen, Pencil, Plus, RotateCcw, Search, Trash2, Upload, UserRound, X, ZoomIn, ZoomOut } from 'lucide-react'
|
||||
import type { BriefConcept, CaseDocument, CaseReport, CaseReportSubmissionInput, CaseState, Connection, DocumentCaptureKind, DocumentSemanticAnalysis, EventExhibit, Evidence, EvidenceMatchRuleDefinition, Exhibit, ExhibitRelation, FolderExhibit, LevelBrief, LevelFlag, LevelGoal, OrganizationKind, PartyExhibit, PartyKind, SourceFileType, TimelineRange, TimelineView, UploadedCaseDocument } from './types'
|
||||
import { AdminPanel } from './admin'
|
||||
@@ -6,6 +6,9 @@ import { clampBoardZoom, containedIds, dateValue, discardExhibit, folderIsOpen,
|
||||
import { defaultConnectionLabel, documentCapture, documentCaptureRegistry, documentExhibits, documentWidget, evidenceExhibits, exhibitWidget, type ExhibitWidgetContext, type WidgetCommand } from './exhibitRegistry'
|
||||
import type { PlaythroughState } from './narrative'
|
||||
|
||||
// three.js stays out of the board bundle until the player opens the inventory.
|
||||
const Inventory = lazy(() => import('./inventory').then(m => ({ default: m.Inventory })))
|
||||
|
||||
const BOARD_W = 2400
|
||||
const BOARD_H = 1500
|
||||
const SOURCE_FILE_TYPES: { value: SourceFileType; label: string }[] = [
|
||||
@@ -71,6 +74,7 @@ export function App() {
|
||||
const [recentlyCreatedConnectionId, setRecentlyCreatedConnectionId] = useState<string | null>(null)
|
||||
const [threadDraft, setThreadDraft] = useState<Connection | null>(null)
|
||||
const [flagsOpen, setFlagsOpen] = useState(false)
|
||||
const [inventoryOpen, setInventoryOpen] = useState(false)
|
||||
const [matchRulesOpen, setMatchRulesOpen] = useState(false)
|
||||
const [arrivingExhibitIds, setArrivingExhibitIds] = useState<string[]>([])
|
||||
const [activePlaythroughId, setActivePlaythroughId] = useState<string | null>(null)
|
||||
@@ -537,6 +541,9 @@ export function App() {
|
||||
})
|
||||
const timelineView = caseState.views.find((view): view is TimelineView => view.type === 'timeline')
|
||||
return <main className="desktop">
|
||||
{inventoryOpen && activePlaythroughId && <Suspense fallback={null}>
|
||||
<Inventory session={{ playthroughId: activePlaythroughId, onConnect: () => window.location.assign('/?resume=1') }} onClose={() => setInventoryOpen(false)} />
|
||||
</Suspense>}
|
||||
<header className="menubar">
|
||||
<div className="brand"><span className="brand-mark">GU</span><span>OSINT BOARD <em>/ {requestedEditMode && caseState.editingAllowed ? 'LEVEL EDITOR' : 'CASE TERMINAL'}</em></span></div>
|
||||
<nav>
|
||||
@@ -544,6 +551,7 @@ export function App() {
|
||||
<button className={briefOpen ? 'active' : ''} aria-label="Case brief" onClick={() => briefOpen ? closeBrief() : setBriefOpen(true)}>CASE BRIEF{briefAttentionCount > 0 && <b className="brief-count">{briefAttentionCount}</b>}</button>
|
||||
{caseState.report && <button className={reportOpen ? 'active' : ''} aria-label="Case report" onClick={() => reportOpen ? setReportOpen(false) : void openCaseReport()}>CASE REPORT{reportAttentionCount > 0 && <b className="brief-count">{reportAttentionCount}</b>}</button>}
|
||||
<button onClick={() => setEditingTimeline(true)}>TIMELINE</button>
|
||||
{activePlaythroughId && <button className={inventoryOpen ? 'active' : ''} onClick={() => setInventoryOpen(true)}>INVENTORY</button>}
|
||||
<button onClick={() => setHelpOpen(true)}>HELP</button>
|
||||
{isAdmin && <div className="admin-menu" ref={adminMenuRef}>
|
||||
<button className={adminMenuOpen ? 'active' : ''} aria-haspopup="menu" aria-expanded={adminMenuOpen} onClick={() => setAdminMenuOpen(open => !open)}>ADMIN</button>
|
||||
@@ -1315,7 +1323,7 @@ function FileEditor({ document, canEditGates, onClose, onSave }: { document: Cas
|
||||
{canEditGates && <label className="field gate-field"><span>REVEAL FLAGS · ALL REQUIRED</span><input value={requiredFlags} placeholder="tip.received, archive.unlocked" pattern="[a-z0-9_.\-, ]*" onChange={event => setRequiredFlags(event.target.value)}/><small>Leave blank to show this document when the level first loads.</small></label>}
|
||||
<div className="metadata-heading"><div><b>ADDITIONAL METADATA</b><small>FREE-FORM KEY / VALUE FIELDS</small></div><button type="button" onClick={() => setMetadata(rows => [...rows, { id: uid('metadata'), key: '', value: '' }])}><Plus size={13}/> ADD FIELD</button></div>
|
||||
<div className="metadata-rows">{metadata.length === 0 && <p>NO ADDITIONAL METADATA</p>}{metadata.map(row => <div className="metadata-row" key={row.id}><input aria-label="Metadata key" placeholder="FIELD" value={row.key} onChange={event => setMetadata(rows => rows.map(candidate => candidate.id === row.id ? { ...candidate, key: event.target.value } : candidate))}/><input aria-label="Metadata value" placeholder="VALUE" value={row.value} onChange={event => setMetadata(rows => rows.map(candidate => candidate.id === row.id ? { ...candidate, value: event.target.value } : candidate))}/><button type="button" aria-label="Remove metadata field" onClick={() => setMetadata(rows => rows.filter(candidate => candidate.id !== row.id))}><Trash2 size={13}/></button></div>)}</div>
|
||||
<p className="folder-editor-note">This metadata belongs to the source file, not to any folder that contains it.</p>
|
||||
|
||||
<div className="folder-editor-actions"><button type="button" onClick={onClose}>CANCEL</button><button className="primary" type="submit">SAVE METADATA</button></div>
|
||||
</div>
|
||||
</form></div>
|
||||
|
||||
Reference in New Issue
Block a user