diff --git a/e2e/board.smoke.spec.ts b/e2e/board.smoke.spec.ts
index 2cfbbca..0e5bbc3 100644
--- a/e2e/board.smoke.spec.ts
+++ b/e2e/board.smoke.spec.ts
@@ -51,11 +51,15 @@ test('move, folder expansion, empty-board pan, desktop wheel zoom, mobile pinch,
await page.getByRole('button', { name: 'EVIDENCE', exact: true }).click()
const documentRow = page.locator('[data-document-row-id="22222222-2222-4222-8222-222222222222"]')
- await documentRow.click()
+ await documentRow.locator('.doc-row-main').click()
await expect(documentRow).toHaveClass(/\bselected\b/)
await expect(page.locator('[data-temporal-id="widget:11111111-1111-4111-8111-111111111111"]')).toHaveClass(/\bdocument-located\b/)
await expect(page.locator('.document-locator-beam .document-locator-ray')).toBeVisible()
- await documentRow.dblclick()
+ const transformBeforeLocate = await page.locator('.board').getAttribute('style')
+ await waitForSave(page, () => page.getByRole('button', { name: 'Locate Dated source image on board', exact: true }).click())
+ await expect.poll(() => page.locator('.board').getAttribute('style')).not.toEqual(transformBeforeLocate)
+ await expect(documentRow).toHaveClass(/\bselected\b/)
+ await documentRow.locator('.doc-row-main').dblclick()
await expect(page.getByRole('button', { name: 'Close document', exact: true })).toBeVisible()
await page.getByRole('button', { name: 'Close document', exact: true }).click()
await page.getByRole('button', { name: 'Close documents', exact: true }).click()
diff --git a/src/App.tsx b/src/App.tsx
index bbce2b7..373095f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -249,6 +249,23 @@ export function App() {
: s.viewport }))
}
+ const focusDocument = (id: string) => {
+ if (!caseState) return
+ const document = caseState.exhibits.find((exhibit): exhibit is CaseDocument => exhibit.id === id && exhibit.type === 'document')
+ if (!document) return
+ const membership = caseState.relations.find(relation => relation.type === 'contains' && relation.toExhibitId === document.id)
+ const folder = membership ? caseState.exhibits.find((exhibit): exhibit is FolderExhibit => exhibit.id === membership.fromExhibitId && exhibit.type === 'folder') : undefined
+ const visibleTarget = folder && !folder.isOpen ? folder : document
+ const bounds = boardRef.current?.getBoundingClientRect()
+ setSelected(document.id)
+ update(state => {
+ if (!bounds) return state
+ const focusViewport = { ...state.viewport, zoom: Math.max(state.viewport.zoom, .9) }
+ return { ...state, viewport: viewportCenteredOnExhibit(focusViewport, visibleTarget, { width: bounds.width, height: bounds.height }) }
+ })
+ setStatus(`LOCATED EXHIBIT ${document.displayNumber || ''}`.trim())
+ }
+
const extract = (doc: CaseDocument, regionId: string) => {
if (!caseState) return
const region = doc.regions.find(r => r.id === regionId)!
@@ -668,13 +685,15 @@ export function App() {
<> { if (e.target.files) void uploadFiles(e.target.files); e.target.value = '' }} />>
- {filteredDocuments.map((doc, index) =>
)}
+ {filteredDocuments.map((doc, index) =>
+
+
+
)}
{normalizedDocumentQuery && filteredDocuments.length === 0 &&
NO MATCHING DOCUMENTSSearches titles, contents, extracts, and metadata.
}
- ARCHIVE MOUNTED {requestedEditMode && caseState.editingAllowed ? 'AUTHORING' : 'READ ONLY'}
{ if (e.dataTransfer.types.includes('Files')) { e.preventDefault(); setDraggingFiles(true) } }} onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy' }} onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDraggingFiles(false) }} onDrop={e => { e.preventDefault(); if (e.dataTransfer.files.length) void uploadFiles(e.dataTransfer.files) }}>
diff --git a/src/styles.css b/src/styles.css
index c8a4c0f..2b0acc4 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -34,19 +34,19 @@ button:focus-visible { outline: 2px solid #e99a44; outline-offset: 2px; }
.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 { position: relative; width: 100%; min-height: 76px; border-bottom: 1px solid #243d36; background: transparent; display: grid; grid-template-columns: minmax(0, 1fr) 38px; text-align: left; align-items: stretch; }
.doc-row:hover { background: #18342e; }
-.doc-row.selected { position: relative; background: #304534; box-shadow: inset 4px 0 #f1cf55, inset 0 0 20px #d8b94722; }
+.doc-row.selected { background: #304534; box-shadow: inset 4px 0 #f1cf55, inset 0 0 20px #d8b94722; }
.doc-row.selected::after { content: 'LOCATED'; position: absolute; top: 7px; right: 9px; color: #f0ce57; font: 600 6px IBM Plex Mono; letter-spacing: .12em; }
.doc-row.selected .doc-icon { border-color: #e7c750; box-shadow: 0 0 12px #efd44f66, 3px 3px #081a16; }
+.doc-row-main { min-width: 0; min-height: 76px; padding: 11px 7px 11px 13px; display: grid; grid-template-columns: 45px minmax(0, 1fr); align-items: center; gap: 10px; border: 0; background: transparent; color: inherit; text-align: left; cursor: pointer; }
+.doc-row-locate { display: grid; place-items: center; margin: 25px 5px 8px 0; padding: 0; border: 1px solid transparent; background: transparent; color: #6f8f85; cursor: pointer; }
+.doc-row-locate:hover { border-color: #9a683d; background: #102722; color: #e0a25d; }
.doc-row strong { display: block; color: #d8dfda; font-size: 12px; margin-bottom: 6px; }
.doc-row span { display: block; font: 9px IBM Plex Mono; color: #708b83; }
-.doc-row > svg { color: #527068; }
.doc-icon { height: 49px; border: 1px solid #61736d; display: grid; place-items: center; position: relative; color: #ccd2cd; background: #263c36; box-shadow: 3px 3px #081a16; }
.doc-icon b { position: absolute; bottom: 2px; right: 2px; background: #d58e42; color: #14231f; font: 600 7px IBM Plex Mono; padding: 1px 3px; }
.tint-1 { background: #38352d; } .tint-2 { background: #263c3f; }
-.panel-foot { height: 42px; border-top: 1px solid #354b45; padding: 0 16px; display: flex; align-items: center; gap: 8px; font: 9px IBM Plex Mono; color: #718d84; }
-.panel-foot span { margin-left: auto; color: #b27b43; }
.board-shell { flex: 1; min-width: 0; position: relative; background: #0b1d19; }
.case-heading { position:absolute;top:20px;left:27px;z-index:2;color:#dde2de;pointer-events:none; }
.case-heading h1 { margin:0;font:500 24px Special Elite,serif;letter-spacing:.02em; }