mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-21 18:55:36 +02:00
feat(ui): add Storybook design system refinements
This commit is contained in:
parent
46ad816c4c
commit
3ccfcce87b
15 changed files with 2839 additions and 666 deletions
|
|
@ -1,6 +1,14 @@
|
|||
import { useState, useEffect, useRef } from 'react'
|
||||
import { fetchEntryDetail, fetchEntryTags, assignTag, removeTag, listEntryCollections } from '../api'
|
||||
import { formatTimestamp, formatBytes, valueText } from '../utils'
|
||||
import { formatTimestamp, formatBytes, valueText, sourceIconSvg } from '../utils'
|
||||
|
||||
const VIS_LABEL = { 0: 'Private', 1: 'Public', 2: 'Users only', 3: 'Public' }
|
||||
|
||||
const ExternalIcon = () => (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M7 17 17 7M9 7h8v8"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet, tagNodes, onTagsRefresh }) {
|
||||
const [detail, setDetail] = useState(null)
|
||||
|
|
@ -53,54 +61,60 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet,
|
|||
const updated = await fetchEntryTags(archiveId, selectedEntry.entry_uid)
|
||||
setTags(updated)
|
||||
onTagsRefresh()
|
||||
} catch (e) {
|
||||
// silently ignore for now; could add error state
|
||||
} catch {
|
||||
// silently ignore
|
||||
}
|
||||
}
|
||||
|
||||
const metaRows = detail ? [
|
||||
['Added', formatTimestamp(detail.summary.archived_at)],
|
||||
['Source', detail.summary.source_kind],
|
||||
['Type', detail.summary.entity_kind],
|
||||
['Visibility', detail.summary.visibility],
|
||||
['Root', detail.structured_root_relpath],
|
||||
] : []
|
||||
|
||||
return (
|
||||
<aside className="context-rail">
|
||||
<div className="rail-title">Context</div>
|
||||
{!selectedEntry ? (
|
||||
<div className="rail-body">Select an entry.</div>
|
||||
) : !detail ? (
|
||||
<div className="rail-body">Loading…</div>
|
||||
) : (
|
||||
<div className="rail-body">
|
||||
<strong className="rail-entry-title">
|
||||
{valueText(detail.summary.title) || valueText(detail.summary.entry_uid)}
|
||||
</strong>
|
||||
<div className="rail-eyebrow">Context</div>
|
||||
|
||||
<div className="rail-section">
|
||||
{detail.summary.original_url && (
|
||||
<div className="rail-item">
|
||||
<span className="rail-label">Original URL</span>:{' '}
|
||||
<a
|
||||
href={detail.summary.original_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rail-url-link"
|
||||
>
|
||||
{detail.summary.original_url}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{[
|
||||
['Added', formatTimestamp(detail.summary.archived_at)],
|
||||
['Source', detail.summary.source_kind],
|
||||
['Type', detail.summary.entity_kind],
|
||||
['Visibility', detail.summary.visibility],
|
||||
['Structured root', detail.structured_root_relpath],
|
||||
].map(([label, value]) => (
|
||||
<div key={label} className="rail-item">
|
||||
<span className="rail-label">{label}</span>: {valueText(value)}
|
||||
{!selectedEntry ? (
|
||||
<p className="tags-empty">Select an entry.</p>
|
||||
) : !detail ? (
|
||||
<p className="tags-empty">Loading…</p>
|
||||
) : (
|
||||
<>
|
||||
<h2 className="rail-title">
|
||||
{valueText(detail.summary.title) || valueText(detail.summary.entry_uid)}
|
||||
</h2>
|
||||
|
||||
{detail.summary.original_url && (
|
||||
<a
|
||||
className="url-tile"
|
||||
href={detail.summary.original_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span className="ico" dangerouslySetInnerHTML={{ __html: sourceIconSvg(detail.summary.source_kind) }} />
|
||||
<span className="u-text">{detail.summary.original_url}</span>
|
||||
<span className="ext"><ExternalIcon /></span>
|
||||
</a>
|
||||
)}
|
||||
|
||||
<div className="meta-list">
|
||||
{metaRows.filter(([, v]) => v != null && v !== '').map(([label, value]) => (
|
||||
<div key={label} className="meta-item">
|
||||
<span className="meta-k">{label}</span>
|
||||
<span className={`meta-v${label === 'Root' ? ' mono' : ''}`}>{valueText(value)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{detail.artifacts.length > 0 ? (
|
||||
{detail.artifacts.length > 0 && (
|
||||
<div className="rail-section">
|
||||
<div className="rail-section-heading">Artifacts ({detail.artifacts.length})</div>
|
||||
<div className="rail-section-heading">
|
||||
Artifacts <span className="num">{detail.artifacts.length}</span>
|
||||
</div>
|
||||
<ul className="artifact-list">
|
||||
{detail.artifacts.map((artifact, index) => (
|
||||
<li key={index}>
|
||||
|
|
@ -110,64 +124,65 @@ export default function ContextRail({ archiveId, selectedEntry, onTagFilterSet,
|
|||
rel="noopener noreferrer"
|
||||
className="artifact-link"
|
||||
>
|
||||
{artifact.artifact_role.replace(/_/g, ' ')}
|
||||
{artifact.byte_size != null ? ` (${formatBytes(artifact.byte_size)})` : ''}
|
||||
<span className="artifact-name">{artifact.artifact_role.replace(/_/g, ' ')}</span>
|
||||
<span className="artifact-size">
|
||||
{artifact.byte_size != null ? formatBytes(artifact.byte_size) : '—'}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rail-item muted">No artifacts.</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedEntry && (
|
||||
<>
|
||||
<div className="entry-tags">
|
||||
<div className="rail-section">
|
||||
<div className="rail-section-heading">Tags</div>
|
||||
{tags.length === 0 ? (
|
||||
<span className="muted">No tags.</span>
|
||||
<p className="tags-empty">No tags yet.</p>
|
||||
) : (
|
||||
tags.map(tag => (
|
||||
<span key={tag.tag_uid} className="tag-pill" title={tag.full_path}>
|
||||
{tag.name}
|
||||
<button
|
||||
className="remove-tag"
|
||||
title={`Remove tag ${tag.full_path}`}
|
||||
onClick={() => handleRemoveTag(tag.tag_uid)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<div className="assign-tag-form">
|
||||
<input
|
||||
className="assign-tag-input"
|
||||
type="text"
|
||||
placeholder="/science/cs"
|
||||
autoComplete="off"
|
||||
value={assignInput}
|
||||
onChange={e => setAssignInput(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleAssignTag() }}
|
||||
/>
|
||||
<button className="assign-tag-btn" onClick={handleAssignTag}>Add tag</button>
|
||||
{assignError && (
|
||||
<div className="muted" style={{ fontSize: '0.85em', color: 'var(--accent)' }}>
|
||||
{assignError}
|
||||
<div className="tags-wrap">
|
||||
{tags.map(tag => (
|
||||
<span key={tag.tag_uid} className="tag-pill" title={tag.full_path}>
|
||||
{tag.name}
|
||||
<button
|
||||
className="remove"
|
||||
title={`Remove tag ${tag.full_path}`}
|
||||
onClick={() => handleRemoveTag(tag.tag_uid)}
|
||||
>×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{assignError && (
|
||||
<p style={{ color: 'var(--accent)', fontSize: 13, margin: '0 0 8px' }}>{assignError}</p>
|
||||
)}
|
||||
<div className="tag-input-wrap">
|
||||
<span className="hash">/</span>
|
||||
<input
|
||||
className="tag-input"
|
||||
type="text"
|
||||
placeholder="science/cs"
|
||||
autoComplete="off"
|
||||
value={assignInput}
|
||||
onChange={e => setAssignInput(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleAssignTag() }}
|
||||
/>
|
||||
<button className="tag-add-btn" onClick={handleAssignTag}>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
{selectedEntry && entryCollections.length > 0 && (
|
||||
|
||||
{entryCollections.length > 0 && (
|
||||
<div className="rail-section">
|
||||
<div className="rail-section-heading">Collections</div>
|
||||
{entryCollections.map(c => (
|
||||
<div key={c.collection_uid} className="rail-item">
|
||||
<span className="rail-label">{c.collection_uid}</span>:{' '}
|
||||
<span className="muted">
|
||||
{{ 0: 'Private', 1: 'Public', 2: 'Users only', 3: 'Public' }[c.visibility_bits] ?? `bits:${c.visibility_bits}`}
|
||||
<div key={c.collection_uid} className="coll-row">
|
||||
<span className="coll-name">{c.collection_uid}</span>
|
||||
<span className="vis-badge">
|
||||
{VIS_LABEL[c.visibility_bits] ?? `bits:${c.visibility_bits}`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue