Merge branch 'main' of ssh://ramanujan.glitch.university:2222/glitch-university/gupi-osint-board

This commit is contained in:
2026-08-23 23:40:12 +02:00
2 changed files with 12 additions and 8 deletions
+2
View File
@@ -1063,6 +1063,7 @@ function Board({ state, selected, locatorDocumentId, linkFrom, recentlyCreatedEx
<svg className="folder-bands" width={BOARD_W} height={BOARD_H}>
{containmentRelations.map(relation => { const folder = byId.get(relation.fromExhibitId), document = byId.get(relation.toExhibitId); if (folder?.type !== 'folder' || document?.type !== 'document') return null; const origin = { x: folder.x + folder.width / 2, y: folder.y + 78 }; return <line className={folder.isOpen ? 'open' : 'closed'} key={relation.id} x1={origin.x} y1={origin.y} x2={folder.isOpen ? document.x + document.width / 2 : origin.x} y2={folder.isOpen ? document.y + document.height / 2 : origin.y}/> })}
</svg>
<div className="exhibit-layer">
{evidenceExhibits(state.exhibits).filter(exhibit => !exhibit.hidden).map((ev, i) => { const containedDocuments = containedIds(state, ev.id).flatMap(id => { const document = byId.get(id); return document?.type === 'document' ? [document] : [] }); const locatedDocumentId = ev.type === 'folder' && !ev.isOpen && containedDocuments.some(document => document.id === locatorDocumentId) ? locatorDocumentId : undefined; const definition = exhibitWidget(ev.type); const Widget = definition.Component; const containsArrival = ev.type === 'folder' && containedDocuments.some(document => arrivingExhibitIds.includes(document.id)); const notePresentation=ev.type === 'note' ? ev.presentation === 'lined_sheet' ? 'lined-sheet' : 'luggage-tag' : ''; return <article key={ev.id} tabIndex={ev.type === 'folder' ? 0 : undefined} aria-expanded={ev.type === 'folder' ? ev.isOpen : undefined} title={ev.type === 'folder' ? 'Double-click or hold to open or close this folder' : undefined} data-document-locator-target={locatedDocumentId} data-temporal-id={`widget:${ev.id}`} className={`evidence-card ${definition.visualType} ${notePresentation} ${selected === ev.id ? 'selected' : ''} ${locatedDocumentId ? 'document-located' : ''} ${linkFrom === ev.id ? 'linking' : ''} ${linkFrom && linkFrom !== ev.id ? 'thread-target' : ''} ${recentlyCreatedExhibitId === ev.id || arrivingExhibitIds.includes(ev.id) || containsArrival ? 'arriving' : ''}`} style={{ left: ev.x, top: ev.y, width: ev.width, height: ev.height, rotate: `${ev.rotation + (i % 3 - 1) * .45}deg`, zIndex: ev.zIndex }}
onPointerDown={e => { e.stopPropagation(); if (linkFrom && e.button === 0) return; if (ev.type === 'folder') startFolderLongPress(e, ev.id); if (tool === 'hand' || e.button === 1) { e.preventDefault(); pointerDown(e) } else if (e.button === 0) pointerDown(e, { kind: 'widget', id: ev.id }) }}
onPointerMove={e => { e.stopPropagation(); pointerMove(e) }} onPointerUp={e => { e.stopPropagation(); finishDrag(e) }} onPointerCancel={e => { e.stopPropagation(); finishDrag(e) }}
@@ -1080,6 +1081,7 @@ function Board({ state, selected, locatorDocumentId, linkFrom, recentlyCreatedEx
<div className="source-file-footer">{document.captureKind === 'clipping' && <span className={`clip-provenance ${missingProvenance.length ? 'incomplete' : 'complete'}`}>{missingProvenance.length ? `ADD ${missingProvenance.join(' + ')}` : 'SOURCE RECORDED'}</span>}<time>{document.publishedAt?.slice(0, 10) || 'UNDATED'}</time>
<div className="source-file-actions"><button onClick={event => { event.stopPropagation(); onOpenSource(document.id) }}><BookOpen size={12}/> OPEN</button><button onClick={event => { event.stopPropagation(); onEditFile(document.id) }}><Pencil size={12}/> METADATA</button></div></div>
</article> })}
</div>
</div>
</div>
}
+10 -8
View File
@@ -60,10 +60,10 @@ button:focus-visible { outline: 2px solid #e99a44; outline-offset: 2px; }
.board-trash span { display: grid; gap: 3px; }.board-trash b { font: 600 9px IBM Plex Mono; letter-spacing: .1em; }.board-trash small { font: 6px IBM Plex Mono; letter-spacing: .05em; white-space: nowrap; }
.board-trash.drag-ready { opacity: 1; color: #d69568; border-color: #b06e4f; background: #1c2b26ee; transform: translateY(-2px); }
.board-trash.active { color: #ffe2cf; border-color: #df5c48; background: #6d2823ed; box-shadow: 0 0 0 4px #c4473738, 6px 8px 0 #02090799; transform: scale(1.05); }
.board { transform-origin: 0 0; position: absolute; background: linear-gradient(110deg, #0c211d33, #122b251f); }
.board { transform-origin: 0 0; position: absolute; isolation: isolate; background: linear-gradient(110deg, #0c211d33, #122b251f); }
.board-stamp { position: absolute; left: 1480px; top: 240px; color: #6e807a2e; border: 2px solid #6e807a20; padding: 9px 13px; transform: rotate(-4deg); font: 600 11px IBM Plex Mono; }
.board-stamp span { display: block; text-align: center; font-size: 8px; margin-top: 4px; }
.connections { position: absolute; inset: 0; overflow: visible; pointer-events: none; }
.connections { position: absolute; z-index: 20; inset: 0; overflow: visible; pointer-events: none; }
.connections path { stroke: #8f2828; stroke-width: 3; fill: none; filter: drop-shadow(1px 2px 0 #020907aa); }
.connections circle { fill: #b33a32; stroke: #581916; stroke-width: 2; }
.connections .thread-preview path { stroke: #bc3b35; stroke-width: 2.5; stroke-dasharray: 8 6; opacity: .82; }
@@ -73,9 +73,9 @@ button:focus-visible { outline: 2px solid #e99a44; outline-offset: 2px; }
.luggage-tag.luggage-tag { background: linear-gradient(100deg, #aa8755, #c7a773 52%, #a9834f); border: 1px solid #d3b681; color: #33291c; clip-path: polygon(13px 0, calc(100% - 13px) 0, 100% 14px, 100% 100%, 0 100%, 0 14px); transform-origin: 50% 8px; transition: transform .22s ease, filter .22s ease; box-shadow: 5px 7px 0 #020b0980, inset 0 0 18px #60452233; }
.luggage-tag.luggage-tag::before { content: ''; position: absolute; z-index: 3; top: 6px; left: 50%; width: 10px; height: 10px; translate: -50% 0; border-radius: 50%; background: #12211d; border: 2px solid #70583b; box-shadow: 0 0 0 2px #c5a66f, inset 1px 1px 2px #000; }
.luggage-tag.luggage-tag::after { content: ''; position: absolute; left: 9px; right: 9px; bottom: -1px; height: 4px; background: #745a37; clip-path: polygon(0 0, 5% 50%, 12% 0, 20% 70%, 28% 0, 40% 60%, 49% 0, 61% 70%, 73% 0, 83% 60%, 91% 0, 100% 50%, 100% 100%, 0 100%); }
.thread-tag { position: absolute; z-index: 7; border: 0; cursor: grab; touch-action: none; transition: left .1s ease-out, top .1s ease-out, transform .22s ease, filter .22s ease, opacity .15s ease; }
.thread-tag { position: absolute; z-index: 30; border: 0; cursor: grab; touch-action: none; transition: left .1s ease-out, top .1s ease-out, transform .22s ease, filter .22s ease, opacity .15s ease; }
.board-viewport.threading .thread-tag { pointer-events: none; opacity: .72; }
.thread-tag.dragging { z-index: 15; cursor: grabbing; transition: transform .12s ease, filter .12s ease; filter: drop-shadow(0 0 7px #d99a58aa); }
.thread-tag.dragging { z-index: 31; cursor: grabbing; transition: transform .12s ease, filter .12s ease; filter: drop-shadow(0 0 7px #d99a58aa); }
.thread-tag-position { position: absolute; z-index: 8; left: 50%; top: -22px; translate: -50% 0; min-width: 34px; padding: 3px 5px; border: 1px solid #9a683d; background: #102a24; color: #e6a45e; font: 7px IBM Plex Mono; box-shadow: 2px 3px #02090799; }
.thread-tag.untagged { width: 13px; height: 13px; padding: 0; transform: translate(-50%, -50%); border: 2px solid #611d1c; border-radius: 50%; background: #a63531; box-shadow: 1px 2px #020907aa; }
.thread-tag.untagged i { display: none; }
@@ -89,15 +89,17 @@ button:focus-visible { outline: 2px solid #e99a44; outline-offset: 2px; }
.thread-tag-content { display: grid; align-content: start; gap: 7px; height: 108px; padding-top: 10px; overflow: hidden; text-align: left; transition: transform .22s ease; }
.thread-tag-content small { padding-bottom: 4px; border-bottom: 1px solid #7e6542; color: #5b472d; font: 600 6px IBM Plex Mono; letter-spacing: .1em; }
.thread-tag-content b { display: -webkit-box; overflow: hidden; -webkit-line-clamp: 5; -webkit-box-orient: vertical; color: #33291c; font: 12px/1.25 Special Elite; }
.thread-lead-ins { position: absolute; z-index: 16; inset: 0; overflow: visible; pointer-events: none; }
.thread-lead-ins { position: absolute; z-index: 40; inset: 0; overflow: visible; pointer-events: none; }
.thread-lead-ins path { fill: none; stroke: #8f2828; stroke-width: 3; stroke-linecap: round; filter: drop-shadow(1px 2px 0 #020907aa); }
.event-support-lines { position: absolute; inset: 0; overflow: visible; pointer-events: none; }
.event-support-lines { position: absolute; z-index: 1; inset: 0; overflow: visible; pointer-events: none; }
.event-support-lines line { stroke: #d3a05c; stroke-width: 2; stroke-dasharray: 7 6; opacity: .58; filter: drop-shadow(1px 1px 0 #020907); }
.party-association-lines { position: absolute; inset: 0; overflow: visible; pointer-events: none; }
.party-association-lines { position: absolute; z-index: 1; inset: 0; overflow: visible; pointer-events: none; }
.party-association-lines line { stroke: #6fa694; stroke-width: 2; stroke-dasharray: 3 5; opacity: .52; filter: drop-shadow(1px 1px 0 #020907); }
.folder-bands { position: absolute; inset: 0; overflow: visible; pointer-events: none; }
.folder-bands { position: absolute; z-index: 1; inset: 0; overflow: visible; pointer-events: none; }
.folder-bands line { stroke: #b63232; stroke-width: 7; opacity: .2; filter: drop-shadow(1px 1px 0 #050a08); transition: x2 .42s cubic-bezier(.2,.75,.2,1), y2 .42s cubic-bezier(.2,.75,.2,1), opacity .24s ease; }
.folder-bands line.closed { opacity: 0; }
.exhibit-layer { position: absolute; z-index: 10; inset: 0; pointer-events: none; }
.exhibit-layer > .evidence-card, .exhibit-layer > .source-file-widget { pointer-events: auto; }
.evidence-card { position: absolute; min-height: 138px; color: #1b2925; background: #d8d8ca; border: 1px solid #edece0; padding: 13px 15px 12px; box-shadow: 7px 9px 0 #020b0980, 0 0 0 1px #45524d; cursor: move; user-select: none; }
.evidence-card::after { content: ''; position: absolute; left: 9px; right: 9px; bottom: -6px; height: 5px; background: #80847b; clip-path: polygon(0 0, 5% 50%, 12% 0, 20% 70%, 28% 0, 40% 60%, 49% 0, 61% 70%, 73% 0, 83% 60%, 91% 0, 100% 50%, 100% 100%, 0 100%); }
.evidence-card.selected { outline: 2px solid #e49a4a; outline-offset: 5px; }