1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

frontend: Esc deselects entry; group font artifacts in rail

This commit is contained in:
TheGeneralist 2026-07-18 21:38:06 +02:00
parent 3407122303
commit a4de506495
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
8 changed files with 141 additions and 73 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,8 +4,8 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Archivr</title> <title>Archivr</title>
<script type="module" crossorigin src="/assets/index-CVmaVy6J.js"></script> <script type="module" crossorigin src="/assets/index-Db35_tmT.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-nbPbdgtG.css"> <link rel="stylesheet" crossorigin href="/assets/index-C0COQCCD.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -464,6 +464,23 @@ export default function App() {
setPreviewEntryUid(null) setPreviewEntryUid(null)
}, [selectedEntry]) }, [selectedEntry])
// Esc: deselect current entry/entries, unless a modal or input has focus.
useEffect(() => {
const handler = (e) => {
if (e.key !== 'Escape') return
if (captureDialogOpen || previewEntryUid) return
const tag = document.activeElement?.tagName
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return
if (selectedUids.size > 0) {
e.preventDefault()
setSelectedUids(new Set())
document.activeElement?.blur?.()
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [captureDialogOpen, previewEntryUid, selectedUids])
// Toggle body class so fixed AudioBar doesn't obscure scrollable content // Toggle body class so fixed AudioBar doesn't obscure scrollable content
useEffect(() => { useEffect(() => {
document.body.classList.toggle('has-audio-bar', !!currentAudio) document.body.classList.toggle('has-audio-bar', !!currentAudio)

View file

@ -23,6 +23,8 @@ export default function ContextRail({ archiveId, selectedEntry, selectedUids, se
const [rearchiveState, setRearchiveState] = useState('idle') // 'idle' | 'running' | 'done' | 'error' const [rearchiveState, setRearchiveState] = useState('idle') // 'idle' | 'running' | 'done' | 'error'
const [rearchiveError, setRearchiveError] = useState('') const [rearchiveError, setRearchiveError] = useState('')
const rearchivePollRef = useRef(null) const rearchivePollRef = useRef(null)
const [fontsOpen, setFontsOpen] = useState(false)
useEffect(() => { setFontsOpen(false) }, [detail?.summary?.entry_uid])
// Bulk-panel state // Bulk-panel state
const isBulk = selectedUids?.size >= 2 const isBulk = selectedUids?.size >= 2
@ -399,30 +401,63 @@ export default function ContextRail({ archiveId, selectedEntry, selectedUids, se
))} ))}
</div> </div>
{detail.artifacts.length > 0 && ( {detail.artifacts.length > 0 && (() => {
<div className="rail-section"> const indexed = detail.artifacts.map((a, i) => ({ ...a, _idx: i }))
<div className="rail-section-heading"> const fonts = indexed.filter(a => a.artifact_role === 'font')
Artifacts <span className="num">{detail.artifacts.length}</span> const others = indexed.filter(a => a.artifact_role !== 'font')
</div> const fontTotalBytes = fonts.reduce((s, a) => s + (a.byte_size || 0), 0)
<ul className="artifact-list"> const entryUid = detail.summary.entry_uid
{detail.artifacts.map((artifact, index) => ( const renderRow = (artifact) => (
<li key={index}> <li key={artifact._idx}>
<a <a
href={`/api/archives/${archiveId}/entries/${detail.summary.entry_uid}/artifacts/${index}`} href={`/api/archives/${archiveId}/entries/${entryUid}/artifacts/${artifact._idx}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="artifact-link" className="artifact-link"
> >
<span className="artifact-name">{artifact.artifact_role.replace(/_/g, ' ')}</span> <span className="artifact-name">
{artifact.artifact_role === 'font'
? artifact.relpath.split('/').pop()
: artifact.artifact_role.replace(/_/g, ' ')}
</span>
<span className="artifact-size"> <span className="artifact-size">
{artifact.byte_size != null ? formatBytes(artifact.byte_size) : '—'} {artifact.byte_size != null ? formatBytes(artifact.byte_size) : '—'}
</span> </span>
</a> </a>
</li> </li>
))} )
return (
<div className="rail-section">
<div className="rail-section-heading">
Artifacts <span className="num">{detail.artifacts.length}</span>
</div>
<ul className="artifact-list">
{others.map(renderRow)}
{fonts.length > 0 && (
<li className="artifact-group">
<button
type="button"
className="artifact-group-header artifact-link"
aria-expanded={fontsOpen}
onClick={() => setFontsOpen(o => !o)}
>
<span className="artifact-name">
<span aria-hidden="true" className={`artifact-group-chevron${fontsOpen ? ' open' : ''}`}></span>
{` fonts (${fonts.length})`}
</span>
<span className="artifact-size">{formatBytes(fontTotalBytes)}</span>
</button>
{fontsOpen && (
<ul className="artifact-list artifact-group-body">
{fonts.map(renderRow)}
</ul>
)}
</li>
)}
</ul> </ul>
</div> </div>
)} )
})()}
</> </>
)} )}

View file

@ -515,6 +515,22 @@ select {
.artifact-name { font-size: 13px; color: var(--link); min-width: 0; word-break: break-word; } .artifact-name { font-size: 13px; color: var(--link); min-width: 0; word-break: break-word; }
.artifact-link:hover .artifact-name { text-decoration: underline; } .artifact-link:hover .artifact-name { text-decoration: underline; }
.artifact-size { font-size: 11.5px; color: var(--muted-2); flex-shrink: 0; font-variant-numeric: tabular-nums; } .artifact-size { font-size: 11.5px; color: var(--muted-2); flex-shrink: 0; font-variant-numeric: tabular-nums; }
.artifact-group { list-style: none; }
.artifact-group-header {
width: 100%; background: none; border: none; border-bottom: 1px solid var(--line-soft);
cursor: pointer; text-align: left; color: inherit; font: inherit;
}
.artifact-group-header:hover .artifact-name { text-decoration: underline; }
.artifact-group-chevron {
display: inline-block;
font-style: normal;
transition: transform 0.15s ease;
transform: rotate(0deg);
transform-origin: 45% 50%;
margin-right: 3px;
}
.artifact-group-chevron.open { transform: rotate(90deg); }
.artifact-group-body { padding-left: 14px; }
/* tags */ /* tags */
.tags-wrap { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 12px; } .tags-wrap { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 12px; }