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

Compare commits

..

No commits in common. "a4de5064956a32925a71406bc4345337adbc747b" and "cd463d28109e6e826a18f2ba38072dd75e684ff9" have entirely different histories.

11 changed files with 133 additions and 331 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 name="viewport" content="width=device-width, initial-scale=1" />
<title>Archivr</title>
<script type="module" crossorigin src="/assets/index-Db35_tmT.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C0COQCCD.css">
<script type="module" crossorigin src="/assets/index-B7dq_VAM.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-nbPbdgtG.css">
</head>
<body>
<div id="root"></div>

View file

@ -464,23 +464,6 @@ export default function App() {
setPreviewEntryUid(null)
}, [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
useEffect(() => {
document.body.classList.toggle('has-audio-bar', !!currentAudio)

View file

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

View file

@ -9,28 +9,8 @@ export default function PreviewPage({ archiveId, entryUid }) {
const [detail, setDetail] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
// Set to true by ArticleRenderer when the entry turns out to be an X article,
// triggering the X dark palette on the page shell.
const [xArticle, setXArticle] = useState(false)
// When displaying an X article, make the document scroll (not an inner div)
// and apply color-scheme:dark so native scrollbars match the dark palette.
useEffect(() => {
if (!xArticle) return
const html = document.documentElement
const body = document.body
const prevScheme = html.style.colorScheme
const prevBg = body.style.background
html.style.colorScheme = 'dark'
body.style.background = '#000'
return () => {
html.style.colorScheme = prevScheme
body.style.background = prevBg
}
}, [xArticle])
useEffect(() => {
setXArticle(false)
fetchEntryDetail(archiveId, entryUid)
.then(d => { setDetail(d); setLoading(false) })
.catch(e => { setError(e?.message || 'Failed to load entry'); setLoading(false) })
@ -44,33 +24,28 @@ export default function PreviewPage({ archiveId, entryUid }) {
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
background: xArticle ? '#000' : 'var(--paper)',
background: 'var(--paper)',
fontFamily: 'var(--sans)',
}}>
{/* Topbar — sticky + blur when showing an X article, matching a-topbar in the renderer */}
{/* Minimal topbar */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: xArticle ? '8px 12px' : '10px 16px',
borderBottom: xArticle ? 'none' : '1px solid var(--line)',
padding: '10px 16px',
borderBottom: '1px solid var(--line)',
flexShrink: 0,
position: xArticle ? 'sticky' : 'relative',
top: 0,
zIndex: 20,
background: xArticle ? 'rgba(0,0,0,0.82)' : 'var(--paper-2)',
backdropFilter: xArticle ? 'blur(12px)' : 'none',
WebkitBackdropFilter: xArticle ? 'blur(12px)' : 'none',
background: 'var(--paper-2)',
}}>
<a
href="/"
style={{ color: xArticle ? '#1d9bf0' : 'var(--accent)', textDecoration: 'none', fontSize: '13px', flexShrink: 0 }}
style={{ color: 'var(--accent)', textDecoration: 'none', fontSize: '13px', flexShrink: 0 }}
> Archive</a>
<span style={{
flex: 1,
fontSize: '14px',
fontWeight: 600,
color: xArticle ? '#e7e9ea' : 'var(--ink)',
color: 'var(--ink)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
@ -80,15 +55,13 @@ export default function PreviewPage({ archiveId, entryUid }) {
href={originalUrl}
target="_blank"
rel="noopener noreferrer"
style={{ color: xArticle ? '#71767b' : 'var(--muted)', textDecoration: 'none', fontSize: '13px', flexShrink: 0 }}
style={{ color: 'var(--muted)', textDecoration: 'none', fontSize: '13px', flexShrink: 0 }}
>Original </a>
)}
</div>
{/* Content */}
<div style={xArticle
? { flex: 1 }
: { flex: 1, minHeight: 0, overflow: 'auto', display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: 1, minHeight: 0, overflow: 'auto', display: 'flex', flexDirection: 'column' }}>
{loading && (
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--muted)', fontSize: '14px' }}>
Loading
@ -104,8 +77,6 @@ export default function PreviewPage({ archiveId, entryUid }) {
archiveId={archiveId}
entry={detail.summary}
detail={detail}
fullPage
onXArticle={setXArticle}
/>
)}
</div>

View file

@ -7,7 +7,7 @@ const VIDEO_EXTS = new Set(['mp4', 'webm', 'mov', 'mkv', 'avi', 'm4v', 'ogv']);
const AUDIO_EXTS = new Set(['mp3', 'ogg', 'm4a', 'opus', 'wav', 'flac', 'aac']);
const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg', 'bmp']);
export default function PreviewPanel({ archiveId, entry, detail, fullPage, onXArticle }) {
export default function PreviewPanel({ archiveId, entry, detail }) {
if (!entry) {
return (
@ -35,20 +35,16 @@ export default function PreviewPanel({ archiveId, entry, detail, fullPage, onXAr
// 1. Tweet / tweet thread
if (entityKind === 'tweet' || entityKind === 'tweet_thread') {
const tweetPreview = (
<TweetPreview
archiveId={archiveId}
entryUid={entryUid}
artifacts={artifacts}
entityKind={entityKind}
fullPage={fullPage}
onXArticle={onXArticle}
/>
return (
<div className="preview-tweet-wrap">
<TweetPreview
archiveId={archiveId}
entryUid={entryUid}
artifacts={artifacts}
entityKind={entityKind}
/>
</div>
);
// In fullPage mode the parent (PreviewPage) is the scroll container;
// skip preview-tweet-wrap to avoid nested overflow/scroll.
if (fullPage) return tweetPreview;
return <div className="preview-tweet-wrap">{tweetPreview}</div>;
}
// 2. Find primary_media artifact

View file

@ -415,84 +415,6 @@ const S = {
},
};
// X dark palette (mirroring x-article-renderer)
const X = {
bg: '#000000',
border: '#2f3336',
text: '#e7e9ea',
dim: '#71767b',
accent: '#1d9bf0',
codeBg: '#16181c',
};
// Full-page X article styles (mirrors x-article-renderer CSS)
// Used only when ArticleRenderer is rendered from PreviewPage (fullPage=true).
// Colors are hard-coded to X's dark palette; layout matches the standalone renderer.
const SF = {
// Article container fills the scrollable page area, dark bg
article: {
background: X.bg,
color: X.text,
minHeight: '100%',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
},
// Inner content constrained to 598px, matches #article-content in renderer
articleInner: {
maxWidth: '598px',
margin: '0 auto',
paddingBottom: '80px',
},
aCover: { width: '100%', display: 'block', objectFit: 'cover', maxHeight: '420px' },
aMeta: { padding: '20px 16px 0' },
aTweetTitle: {
fontSize: '34px', fontWeight: '800', letterSpacing: '-0.5px',
color: X.text, lineHeight: '44px', marginBottom: '16px',
},
aAuthorRow: { display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' },
aAvatar: { width: '40px', height: '40px', borderRadius: '50%', objectFit: 'cover', flexShrink: 0, display: 'block' },
aAvatarPh: { width: '40px', height: '40px', borderRadius: '50%', background: X.border, flexShrink: 0 },
aAuthorName: { fontSize: '15px', fontWeight: '700', color: X.text, lineHeight: '1.3' },
aAuthorSub: { fontSize: '15px', color: X.dim, lineHeight: '1.3' },
aDivider: { border: 'none', borderTop: `1px solid ${X.border}`, margin: '0' },
aBody: { padding: '4px 16px 0' },
// Block styles
bH1: {
fontSize: '26px', fontWeight: '800', letterSpacing: '-0.4px',
color: X.text, lineHeight: '36px', margin: '24px 0 10px',
},
bH2: {
fontSize: '26px', fontWeight: '800', letterSpacing: '-0.4px',
color: X.text, lineHeight: '36px', margin: '24px 0 10px',
},
bP: { fontSize: '17px', color: X.text, lineHeight: '1.5', marginBottom: '16px', marginTop: '0' },
bSpacer: { height: '6px', display: 'block' },
bQuote: { borderLeft: `3px solid ${X.border}`, padding: '2px 14px', margin: '14px 0', color: X.dim, fontSize: '17px', lineHeight: '1.5' },
bHr: { border: 'none', borderTop: `1px solid ${X.border}`, margin: '28px 0' },
bImg: { width: '100%', display: 'block', borderRadius: '12px', margin: '16px 0' },
bUl: { margin: '10px 0 16px', paddingLeft: '28px' },
bOl: { margin: '10px 0 16px', paddingLeft: '28px' },
bLi: { fontSize: '17px', color: X.text, lineHeight: '1.5', marginBottom: '6px' },
bTweet: {
display: 'flex', alignItems: 'center', gap: '12px',
border: `1px solid ${X.border}`, borderRadius: '12px',
padding: '14px 16px', margin: '14px 0',
color: X.dim, fontSize: '15px', textDecoration: 'none',
},
bMdPre: { borderRadius: '12px', margin: '12px 0', overflow: 'auto', background: '#1e2029' },
bMdCode: {
fontFamily: "ui-monospace, 'Cascadia Code', 'Fira Code', Menlo, Consolas, monospace",
fontSize: '13px', lineHeight: '1.65',
padding: '18px 20px', display: 'block',
color: X.text, background: 'transparent',
},
iCode: {
fontFamily: "ui-monospace, 'Cascadia Code', Menlo, Consolas, monospace",
fontSize: '0.875em', background: X.codeBg,
padding: '2px 6px', borderRadius: '4px', color: '#e6edf3',
},
link: { color: X.accent, textDecoration: 'none' },
};
// Artifact URL helpers
// Build relpath artifact API URL map from the artifacts array so archived
// copies are preferred over CDN URLs (avatars, media, cover images).
@ -668,7 +590,7 @@ function renderTweetTextJSX(fullText, entities, skipSpans = []) {
// Splits block text at style-range, URL, and mention boundaries, returning
// an array of React nodes with the appropriate wrappers applied.
function renderInlineJSX(text, styleRanges, urls, mentions, st = S) {
function renderInlineJSX(text, styleRanges, urls, mentions) {
if (!text) return null;
styleRanges = styleRanges || [];
urls = urls || [];
@ -688,7 +610,7 @@ function renderInlineJSX(text, styleRanges, urls, mentions, st = S) {
if (ann) anns.push(ann);
}
if (anns.length === 0) return linkifyText(text, st.link);
if (anns.length === 0) return linkifyText(text, S.link);
const pts = new Set([0, text.length]);
for (const a of anns) {
@ -715,7 +637,7 @@ function renderInlineJSX(text, styleRanges, urls, mentions, st = S) {
// Apply inline styles innermost first (matching Draft.js precedence).
if (active.some(a => a.kind === 'style' && a.style === 'Code'))
content = <code style={st.iCode}>{content}</code>;
content = <code style={S.iCode}>{content}</code>;
if (active.some(a => a.kind === 'style' && a.style === 'Bold'))
content = <strong>{content}</strong>;
if (active.some(a => a.kind === 'style' && a.style === 'Italic'))
@ -732,7 +654,7 @@ function renderInlineJSX(text, styleRanges, urls, mentions, st = S) {
// human-readable display URL; otherwise keep the styled anchor text.
const isTco = /^https?:\/\/t\.co\//i.test(content);
content = (
<a href={url.href} target="_blank" rel="noopener noreferrer" style={st.link}>
<a href={url.href} target="_blank" rel="noopener noreferrer" style={S.link}>
{isTco ? url.display : content}
</a>
);
@ -745,14 +667,14 @@ function renderInlineJSX(text, styleRanges, urls, mentions, st = S) {
href={`https://x.com/${mention.screen_name}`}
target="_blank"
rel="noopener noreferrer"
style={st.link}
style={S.link}
>
{content}
</a>
);
}
return <span key={i}>{typeof content === 'string' ? linkifyText(content, st.link) : content}</span>;
return <span key={i}>{typeof content === 'string' ? linkifyText(content, S.link) : content}</span>;
});
}
@ -760,14 +682,13 @@ function renderInlineJSX(text, styleRanges, urls, mentions, st = S) {
// Port of renderAtomic() from x-article-renderer.
function renderAtomicJSX(block, artifactMap, opts) {
const st = opts?.st || S;
const entities = block.resolved_entities || [];
if (entities.length === 0) return null;
return entities.map((e, i) => {
switch (e.type) {
case 'divider':
return <hr key={i} style={st.bHr} />;
return <hr key={i} style={S.bHr} />;
case 'media': {
const src = resolveUrl(e.local_path, e.url, artifactMap);
@ -786,7 +707,7 @@ function renderAtomicJSX(block, artifactMap, opts) {
}
}}
>
<img src={src} style={st.bImg} loading="lazy" alt="" />
<img src={src} style={S.bImg} loading="lazy" alt="" />
</a>
);
}
@ -798,7 +719,7 @@ function renderAtomicJSX(block, artifactMap, opts) {
href={`https://x.com/i/status/${e.tweet_id}`}
target="_blank"
rel="noopener noreferrer"
style={st.bTweet}
style={S.bTweet}
>
<XLogo />
View post on X
@ -807,8 +728,8 @@ function renderAtomicJSX(block, artifactMap, opts) {
case 'link':
return e.url ? (
<p key={i} style={st.bP}>
<a href={e.url} target="_blank" rel="noopener noreferrer" style={st.link}>
<p key={i} style={S.bP}>
<a href={e.url} target="_blank" rel="noopener noreferrer" style={S.link}>
{e.url}
</a>
</p>
@ -817,8 +738,8 @@ function renderAtomicJSX(block, artifactMap, opts) {
case 'markdown': {
const md = e.markdown ?? e.data?.markdown ?? '';
return (
<pre key={i} style={st.bMdPre}>
<code style={st.bMdCode}>{md}</code>
<pre key={i} style={S.bMdPre}>
<code style={S.bMdCode}>{md}</code>
</pre>
);
}
@ -843,16 +764,15 @@ function renderAtomicJSX(block, artifactMap, opts) {
// Port of renderBlock() from x-article-renderer.
function renderBlockJSX(block, key, artifactMap, opts) {
const st = opts?.st || S;
const type = block.type || '';
const text = block.text || '';
const styleRanges = block.inline_style_ranges || [];
const data = block.data || {};
const inner = renderInlineJSX(text, styleRanges, data.urls || [], data.mentions || [], st);
const inner = renderInlineJSX(text, styleRanges, data.urls || [], data.mentions || []);
switch (type) {
case 'header-one':
return <h1 key={key} style={st.bH1}>{inner}</h1>;
return <h1 key={key} style={S.bH1}>{inner}</h1>;
case 'header-two': {
const m = text.match(/(?:x\.com|twitter\.com)\/i\/status\/(\d+)/);
@ -863,32 +783,32 @@ function renderBlockJSX(block, key, artifactMap, opts) {
href={`https://x.com/i/status/${m[1]}`}
target="_blank"
rel="noopener noreferrer"
style={st.bTweet}
style={S.bTweet}
>
<XLogo />
View post on X
</a>
);
}
return <h2 key={key} style={st.bH2}>{inner}</h2>;
return <h2 key={key} style={S.bH2}>{inner}</h2>;
}
case 'unstyled':
if (!text.trim()) return <span key={key} style={st.bSpacer} />;
return <p key={key} style={st.bP}>{inner}</p>;
if (!text.trim()) return <span key={key} style={S.bSpacer} />;
return <p key={key} style={S.bP}>{inner}</p>;
case 'blockquote':
return <blockquote key={key} style={st.bQuote}>{inner}</blockquote>;
return <blockquote key={key} style={S.bQuote}>{inner}</blockquote>;
case 'unordered-list-item':
case 'ordered-list-item':
return <li key={key} style={st.bLi}>{inner}</li>;
return <li key={key} style={S.bLi}>{inner}</li>;
case 'atomic':
return <span key={key}>{renderAtomicJSX(block, artifactMap, opts)}</span>;
default:
return text ? <p key={key} style={st.bP}>{inner}</p> : null;
return text ? <p key={key} style={S.bP}>{inner}</p> : null;
}
}
@ -897,7 +817,6 @@ function renderBlockJSX(block, key, artifactMap, opts) {
// Groups consecutive same-type list items into a single ul/ol.
function renderBlocksJSX(blocks, artifactMap, opts) {
const st = opts?.st || S;
const items = [];
let i = 0;
@ -911,7 +830,7 @@ function renderBlocksJSX(blocks, artifactMap, opts) {
listItems.push(renderBlockJSX(blocks[i], i, artifactMap, opts));
i++;
}
items.push(<ul key={`ul-${startIdx}`} style={st.bUl}>{listItems}</ul>);
items.push(<ul key={`ul-${startIdx}`} style={S.bUl}>{listItems}</ul>);
} else if (b.type === 'ordered-list-item') {
const startIdx = i;
const listItems = [];
@ -919,7 +838,7 @@ function renderBlocksJSX(blocks, artifactMap, opts) {
listItems.push(renderBlockJSX(blocks[i], i, artifactMap, opts));
i++;
}
items.push(<ol key={`ol-${startIdx}`} style={st.bOl}>{listItems}</ol>);
items.push(<ol key={`ol-${startIdx}`} style={S.bOl}>{listItems}</ol>);
} else {
items.push(renderBlockJSX(b, i, artifactMap, opts));
i++;
@ -931,15 +850,9 @@ function renderBlocksJSX(blocks, artifactMap, opts) {
// Article renderer
function ArticleRenderer({ article, tweetAuthor, artifactMap, fullPage, onXArticle }) {
function ArticleRenderer({ article, tweetAuthor, artifactMap }) {
const [lightboxSrc, setLightboxSrc] = useState(null);
// Notify the page shell (PreviewPage) that it should switch to X dark palette.
useEffect(() => {
if (fullPage) onXArticle?.(true);
}, [fullPage, onXArticle]);
const st = fullPage ? SF : S;
const cover = article.cover_media || {};
const author = article.author || tweetAuthor || {};
const date = article.first_published_at_secs
@ -951,8 +864,8 @@ function ArticleRenderer({ article, tweetAuthor, artifactMap, fullPage, onXArtic
const coverSrc = resolveUrl(cover.local_path, cover.url, artifactMap);
const avatarSrc = resolveUrl(author.avatar_local_path, author.avatar_url, artifactMap);
const body = (
<>
return (
<div style={S.article}>
{lightboxSrc && (
<MediaLightbox
items={[{ src: lightboxSrc, alt: '' }]}
@ -968,44 +881,34 @@ function ArticleRenderer({ article, tweetAuthor, artifactMap, fullPage, onXArtic
style={{ display: 'block', cursor: 'zoom-in' }}
onClick={e => { if (!e.metaKey && !e.ctrlKey) { e.preventDefault(); setLightboxSrc(coverSrc); } }}
>
<img src={coverSrc} style={st.aCover} alt="Article cover" />
<img src={coverSrc} style={S.aCover} alt="Article cover" />
</a>
)}
<div style={st.aMeta}>
<div style={S.aMeta}>
{article.title && (
<div style={st.aTweetTitle}>{article.title}</div>
<div style={S.aTweetTitle}>{article.title}</div>
)}
<div style={st.aAuthorRow}>
<div style={S.aAuthorRow}>
{avatarSrc
? <img src={avatarSrc} style={st.aAvatar} alt={author.name || ''} />
: <div style={st.aAvatarPh} />
? <img src={avatarSrc} style={S.aAvatar} alt={author.name || ''} />
: <div style={S.aAvatarPh} />
}
<div>
<div style={st.aAuthorName}>
<div style={S.aAuthorName}>
{author.name || author.screen_name || 'Unknown'}
</div>
{subLine && (
<div style={st.aAuthorSub}>{subLine}</div>
<div style={S.aAuthorSub}>{subLine}</div>
)}
</div>
</div>
</div>
<hr style={st.aDivider} />
<div style={st.aBody}>
{renderBlocksJSX(article.blocks || [], artifactMap, { onImgClick: setLightboxSrc, st })}
<hr style={S.aDivider} />
<div style={S.aBody}>
{renderBlocksJSX(article.blocks || [], artifactMap, { onImgClick: setLightboxSrc })}
</div>
</>
</div>
);
if (fullPage) {
return (
<div style={SF.article}>
<div style={SF.articleInner}>{body}</div>
</div>
);
}
return <div style={S.article}>{body}</div>;
}
// Tweet card
@ -1245,7 +1148,7 @@ function TweetCard({ tweet, isInThread, isLast, artifactMap }) {
// TweetPreview
export default function TweetPreview({ archiveId, entryUid, artifacts, entityKind, fullPage, onXArticle }) {
export default function TweetPreview({ archiveId, entryUid, artifacts, entityKind }) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [tweets, setTweets] = useState([]);
@ -1355,7 +1258,7 @@ export default function TweetPreview({ archiveId, entryUid, artifacts, entityKin
const tweet = tweets[0];
if (tweet.is_article && tweet.article) {
return <ArticleRenderer article={tweet.article} tweetAuthor={tweet.author} artifactMap={artifactMap} fullPage={fullPage} onXArticle={onXArticle} />;
return <ArticleRenderer article={tweet.article} tweetAuthor={tweet.author} artifactMap={artifactMap} />;
}
return (

View file

@ -515,22 +515,6 @@ select {
.artifact-name { font-size: 13px; color: var(--link); min-width: 0; word-break: break-word; }
.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-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-wrap { display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 12px; }