diff --git a/e2e/board.smoke.spec.ts b/e2e/board.smoke.spec.ts index 768e368..6d6f883 100644 --- a/e2e/board.smoke.spec.ts +++ b/e2e/board.smoke.spec.ts @@ -27,6 +27,7 @@ async function waitForSave(page: Page, action: () => Promise) { test('move, folder expansion, hand pan, desktop wheel zoom, mobile pinch, and reload persistence', async ({ page }) => { await page.goto('/?level=e2e-level&edit=1') await expect(page.getByRole('heading', { name: 'Browser Safety Test' })).toBeVisible() + await expect(page.locator('.documents-panel')).toHaveClass(/\bclosed\b/) await expect(page.locator('.brief-panel')).toBeVisible() await expect(page.getByRole('button', { name: 'Case brief', exact: true })).toContainText('2') await page.getByRole('button', { name: 'BEGIN INVESTIGATION', exact: true }).click() diff --git a/e2e/mystery.acceptance.spec.ts b/e2e/mystery.acceptance.spec.ts index 0b76dad..9e91888 100644 --- a/e2e/mystery.acceptance.spec.ts +++ b/e2e/mystery.acceptance.spec.ts @@ -29,11 +29,20 @@ async function createEvent(page: Page, title: string, narrative: string, occurre } test('a cloned Glass Harbor level can be solved without modifying its template', async ({ page, request }) => { + test.setTimeout(90_000) const levels = await (await request.get('/api/levels')).json() as { id: string }[] const playable = levels.find(level => level.id.startsWith('glass-harbor-case-')) if (!playable) throw new Error('Glass Harbor playable clone was not seeded') await page.goto(`/?level=${playable.id}`) await expect(page.getByRole('heading', { name: 'The Glass Harbor Diversion' })).toBeVisible() + await expect(page.locator('.documents-panel')).toHaveClass(/\bclosed\b/) + await page.locator('.open-files').click() + await page.getByRole('searchbox', { name: 'Search inside documents', exact: true }).fill('Elias') + await expect(page.locator('.doc-row')).toHaveCount(1) + await expect(page.locator('.doc-row')).toContainText('Carrier Dispatch Manifest · H&F 14') + await page.getByRole('button', { name: 'Clear document search', exact: true }).click() + await expect(page.locator('.doc-row')).toHaveCount(8) + await page.getByRole('button', { name: 'Close documents', exact: true }).click() await expect(page.locator('.evidence-card.folder')).toHaveCount(3) await expect(page.locator('.timeline .marker')).toHaveCount(8) await expect(page.locator('.timeline-label button')).toHaveText('1987-10-01 — 1987-10-31') diff --git a/src/App.tsx b/src/App.tsx index 298cda5..9b50f6b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,11 @@ const SOURCE_FILE_TYPES: { value: SourceFileType; label: string }[] = [ function uid(_prefix: string) { return crypto.randomUUID() } function briefAcknowledgementKey(levelId: string) { return `gupi-osint-board:brief-acknowledged:${levelId}` } +function documentSearchText(document: CaseDocument) { + return [document.title, document.kind, document.date, document.publishedAt, document.fileName, document.mimeType, + ...document.body, ...document.regions.flatMap(region => [region.label, region.excerpt, region.date]), + ...Object.entries(document.metadata).flatMap(([key, value]) => [key, value])].filter(Boolean).join('\n').toLocaleLowerCase() +} function connectionPoint(item: Evidence) { return exhibitWidget(item.type).connectionPoint(item) } @@ -24,7 +29,8 @@ export function App() { const [openDoc, setOpenDoc] = useState(null) const [selected, setSelected] = useState(null) const [linkFrom, setLinkFrom] = useState(null) - const [docsOpen, setDocsOpen] = useState(true) + const [docsOpen, setDocsOpen] = useState(false) + const [documentQuery, setDocumentQuery] = useState('') const [helpOpen, setHelpOpen] = useState(false) const [status, setStatus] = useState('CONNECTING TO ARCHIVE…') const [clock, setClock] = useState('') @@ -274,6 +280,8 @@ export function App() { if (!caseState) return
GU

GLITCH UNIVERSITY NETWORK TERMINAL

{status}
const documentById = new Map(caseState.documents.map(document => [document.id, document])) + const normalizedDocumentQuery = documentQuery.trim().toLocaleLowerCase() + const filteredDocuments = normalizedDocumentQuery ? caseState.documents.filter(document => documentSearchText(document).includes(normalizedDocumentQuery)) : caseState.documents const unresolvedConceptCount = caseState.brief.concepts.filter(concept => !concept.resolvedPartyExhibitId).length const containedDocumentIds = new Set(caseState.relations.filter(relation => relation.type === 'contains').map(relation => relation.toWidgetId)) const temporalItems: TemporalItem[] = [ @@ -298,14 +306,15 @@ export function App() {
diff --git a/src/styles.css b/src/styles.css index 7bfefc6..6de4850 100644 --- a/src/styles.css +++ b/src/styles.css @@ -25,9 +25,14 @@ button:focus-visible { outline: 2px solid #e99a44; outline-offset: 2px; } .panel-heading h2 { font: 600 17px IBM Plex Mono; letter-spacing: .04em; margin: 6px 0 0; } .panel-heading sup { color: #d89043; font-size: 10px; } .panel-heading button { background: none; border: 0; color: #6f8b83; cursor: pointer; } -.search { margin: 0 15px 12px; height: 36px; border: 1px solid #314a44; display: flex; align-items: center; gap: 9px; padding: 0 12px; color: #607d75; font-size: 11px; } +.search { margin: 0 15px 12px; height: 36px; border: 1px solid #314a44; display: flex; align-items: center; gap: 9px; padding: 0 10px 0 12px; color: #607d75; font-size: 11px; } +.search:focus-within { border-color: #9a683d; box-shadow: inset 0 0 0 1px #6f4b2f; color: #c18b55; } +.search input { min-width: 0; flex: 1; border: 0; outline: 0; background: transparent; color: #d6ded9; font: 10px IBM Plex Mono; } +.search input::placeholder { color: #607d75; }.search input::-webkit-search-cancel-button { display: none; } +.search button { display: grid; place-items: center; padding: 2px; border: 0; background: transparent; color: #8a9d97; cursor: pointer; } .import-document { margin: 0 15px 12px; height: 34px; border: 1px dashed #a26d3d; background: #19322c; color: #d79754; display: flex; justify-content: center; align-items: center; gap: 8px; font: 600 9px IBM Plex Mono; letter-spacing: .08em; cursor: pointer; }.import-document:hover { background: #244039; border-style: solid; }.file-input { display: none; } .doc-list { overflow: auto; border-top: 1px solid #243d36; } +.no-document-results { min-height: 150px; padding: 32px 20px; display: grid; place-items: center; align-content: center; gap: 9px; text-align: center; color: #617d75; }.no-document-results b { color: #91a59f; font: 600 9px IBM Plex Mono; letter-spacing: .08em; }.no-document-results span { font: 8px/1.5 IBM Plex Mono; } .doc-row { width: 100%; min-height: 76px; border: 0; border-bottom: 1px solid #243d36; background: transparent; padding: 11px 13px; display: grid; grid-template-columns: 45px 1fr 18px; text-align: left; align-items: center; gap: 10px; cursor: pointer; } .doc-row:hover { background: #18342e; } .doc-row strong { display: block; color: #d8dfda; font-size: 12px; margin-bottom: 6px; }