Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,45 @@
}

.metaBar {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
padding: var(--space-2) var(--space-4);
font-size: var(--text-xs);
color: var(--text-secondary);
border-bottom: 1px solid var(--border-primary);
flex-shrink: 0;
}

.viewToggle {
display: flex;
gap: var(--space-1);
flex-shrink: 0;
}

.viewToggleBtn,
.viewToggleBtnActive {
padding: var(--space-1) var(--space-3);
border: none;
border-radius: var(--radius-sm);
background: none;
font-size: var(--text-xs);
font-family: inherit;
cursor: pointer;
color: var(--text-secondary);
}

.viewToggleBtn:hover {
color: var(--text-primary);
}

.viewToggleBtnActive {
background: var(--bg-primary);
color: var(--text-primary);
font-weight: var(--font-semibold);
}

/* No fixed dimensions: the viewer hugs whichever content branch renders.
Centering keeps a small image visually anchored when the modal's
min-width forces the box wider than the image's intrinsic size. */
Expand Down Expand Up @@ -58,6 +90,15 @@
box-sizing: border-box;
}

.markdownPreview {
width: min(1280px, 92vw);
height: calc(92vh - 100px);
padding: var(--space-4);
overflow: auto;
box-sizing: border-box;
background: var(--bg-primary);
}

.message {
padding: var(--space-6) var(--space-8);
color: var(--text-secondary);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react'
import { Modal } from './Modal'
import { MarkdownContent } from './MarkdownContent'
import styles from './AttachmentPreviewModal.module.css'

// A single shape that covers both pre-send attachments (base64 in memory)
Expand Down Expand Up @@ -33,7 +34,8 @@ function classify(att: AttachmentPreviewItem) {
|| TEXT_MIMES.has(att.type)
|| TEXT_EXT_RE.test(att.name)
)
return { isImage, isPdf, isText }
const isMarkdown = isText && att.name.toLowerCase().endsWith('.md')
return { isImage, isPdf, isText, isMarkdown }
}

function formatFileSize(bytes: number): string {
Expand All @@ -45,7 +47,10 @@ function formatFileSize(bytes: number): string {
}

export function AttachmentPreviewModal({ isOpen, attachment, onClose }: AttachmentPreviewModalProps) {
const kind = attachment ? classify(attachment) : null
// Memoized: classify() returns a new object each call, and an unstable
// identity here would re-trigger the fetch effect below on every render
// it itself causes (infinite fetch/abort loop).
const kind = useMemo(() => (attachment ? classify(attachment) : null), [attachment])

const imageSrc = useMemo(() => {
if (!attachment || !kind?.isImage) return null
Expand Down Expand Up @@ -74,8 +79,10 @@ export function AttachmentPreviewModal({ isOpen, attachment, onClose }: Attachme
const [textContent, setTextContent] = useState<string | null>(null)
const [textLoading, setTextLoading] = useState(false)
const [textError, setTextError] = useState<string | null>(null)
const [mdView, setMdView] = useState<'preview' | 'source'>('preview')

useEffect(() => {
setMdView('preview')
if (!attachment || !kind?.isText) {
setTextContent(null)
setTextLoading(false)
Expand Down Expand Up @@ -125,6 +132,7 @@ export function AttachmentPreviewModal({ isOpen, attachment, onClose }: Attachme
{kind.isText && lineCount > 0 && <> · {lineCount} line{lineCount !== 1 ? 's' : ''}</>}
</>
)
const showMarkdownToggle = kind.isMarkdown && textContent != null

return (
<Modal
Expand All @@ -134,7 +142,27 @@ export function AttachmentPreviewModal({ isOpen, attachment, onClose }: Attachme
size="auto"
contentClassName={styles.modal}
>
<div className={styles.metaBar}>{meta}</div>
<div className={styles.metaBar}>
{meta}
{showMarkdownToggle && (
<div className={styles.viewToggle}>
<button
type="button"
className={mdView === 'preview' ? styles.viewToggleBtnActive : styles.viewToggleBtn}
onClick={() => setMdView('preview')}
>
Preview
</button>
<button
type="button"
className={mdView === 'source' ? styles.viewToggleBtnActive : styles.viewToggleBtn}
onClick={() => setMdView('source')}
>
Source
</button>
</div>
)}
</div>
<div className={styles.viewer}>
{kind.isImage && imageSrc && (
<img src={imageSrc} alt={attachment.name} className={styles.image} />
Expand All @@ -148,7 +176,11 @@ export function AttachmentPreviewModal({ isOpen, attachment, onClose }: Attachme
) : textError ? (
<div className={styles.message}>{textError}</div>
) : textContent != null ? (
<pre className={styles.text}>{textContent}</pre>
showMarkdownToggle && mdView === 'preview' ? (
<MarkdownContent content={textContent} className={styles.markdownPreview} />
) : (
<pre className={styles.text}>{textContent}</pre>
)
) : null
)}
{!kind.isImage && !kind.isPdf && !kind.isText && (
Expand Down