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
122 changes: 66 additions & 56 deletions src/components/WriteEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export default function WriteEditor() {
const [busy, setBusy] = useState(false);
const [uploads, setUploads] = useState(0);
const [dragging, setDragging] = useState(false);
const [preview, setPreview] = useState(false);
const [view, setView] = useState<'editor' | 'preview' | 'both'>('both');
const editor = useRef<HTMLTextAreaElement>(null);
const imageInput = useRef<HTMLInputElement>(null);
const openedDeepLink = useRef(false);
Expand Down Expand Up @@ -589,7 +589,25 @@ export default function WriteEditor() {

return (
<div className="write-shell">
<main className="write-layout">
<div className="write-view-switch" role="group" aria-label="화면 보기">
{(
[
['editor', '에디터 보기'],
['preview', '미리보기'],
['both', '둘 다 보기'],
] as const
).map(([value, label]) => (
<button
key={value}
type="button"
aria-pressed={view === value}
onClick={() => setView(value)}
>
{label}
</button>
))}
</div>
<main className={`write-layout view-${view}`}>
<section className="write-pane" aria-label="마크다운 글쓰기">
<div className="write-head">
<Button
Expand Down Expand Up @@ -784,75 +802,67 @@ export default function WriteEditor() {
)}
</div>
</div>
<footer className="write-footer">
<Button variant="ghost" asChild>
<a href="/">← 나가기</a>
</Button>
<div className="write-footer-actions">
<Button
variant="ghost"
className="write-mobile-preview"
onClick={() => setPreview(!preview)}
>
{preview ? '편집' : '미리보기'}
</Button>
<Button
variant="ghost"
disabled={busy || uploads > 0}
onClick={() => void save(false)}
>
임시저장
</Button>
{id && posts.some((post) => post.id === id) && (
<Button
variant="ghost"
disabled={busy || uploads > 0}
onClick={moveToTrash}
>
휴지통으로
</Button>
)}
<Button
className="write-publish-button"
disabled={busy || uploads > 0}
onClick={() => setPanel('publish')}
>
{busy ? '처리 중…' : '출간하기'}
</Button>
</div>
</footer>
</section>
<section
className={`write-preview ${preview ? 'is-visible' : ''}`}
aria-label="글 미리보기"
>
<section className="write-preview" aria-label="글 미리보기">
<div className="write-preview-content">
{title ? (
<h1>{title}</h1>
) : (
<p className="write-preview-placeholder">미리보기</p>
)}
{tags.length > 0 && (
<div className="write-preview-tags">
{tags.map((tag) => (
<span key={tag}>{tag}</span>
))}
</div>
)}
<header className="article-heading">
{title ? (
<h1>{title}</h1>
) : (
<p className="write-preview-placeholder">미리보기</p>
)}
{tags.length > 0 && (
<div className="write-preview-tags">
{tags.map((tag) => (
<span key={tag}>{tag}</span>
))}
</div>
)}
</header>
{previewTooLong ? (
<p className="write-preview-placeholder">
본문이 100,000자를 넘어 미리보기를 생략했습니다. 임시저장과
출간은 계속할 수 있습니다.
</p>
) : (
<div
className="write-markdown"
className="prose"
dangerouslySetInnerHTML={{ __html: previewHtml }}
/>
)}
</div>
</section>
</main>
<footer className="write-footer">
<Button variant="ghost" asChild>
<a href="/">← 나가기</a>
</Button>
<div className="write-footer-actions">
<Button
variant="ghost"
disabled={busy || uploads > 0}
onClick={() => void save(false)}
>
임시저장
</Button>
{id && posts.some((post) => post.id === id) && (
<Button
variant="ghost"
disabled={busy || uploads > 0}
onClick={moveToTrash}
>
휴지통으로
</Button>
)}
<Button
className="write-publish-button"
disabled={busy || uploads > 0}
onClick={() => setPanel('publish')}
>
{busy ? '처리 중…' : '출간하기'}
</Button>
</div>
</footer>
{status && (
<div className="write-status" role="status">
{status}
Expand Down
137 changes: 38 additions & 99 deletions src/components/write-editor.css
Original file line number Diff line number Diff line change
@@ -1,16 +1,46 @@
.write-shell {
min-height: 100vh;
height: 100dvh;
display: flex;
flex-direction: column;
}
.write-view-switch {
display: flex;
justify-content: center;
gap: 4px;
padding: 8px;
border-bottom: 1px solid var(--border);
}
.write-view-switch button {
border: 0;
border-radius: 6px;
padding: 7px 12px;
color: var(--muted-foreground);
font-size: 13px;
font-weight: 600;
}
.write-view-switch button[aria-pressed='true'] {
background: var(--secondary);
color: var(--foreground);
}
.write-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
min-height: 100vh;
min-height: 0;
flex: 1;
}
.write-layout.view-editor,
.write-layout.view-preview {
grid-template-columns: minmax(0, 1fr);
}
.view-editor .write-preview,
.view-preview .write-pane {
display: none;
}
.write-pane {
display: flex;
flex-direction: column;
min-width: 0;
height: 100vh;
height: 100%;
background: var(--background);
}
.write-head {
Expand Down Expand Up @@ -205,27 +235,18 @@
.write-publish-button:hover {
background: color-mix(in oklch, var(--primary) 90%, transparent) !important;
}
.write-mobile-preview {
display: none;
}
.write-preview {
min-width: 0;
height: 100vh;
height: 100%;
overflow: auto;
background: var(--muted);
background: var(--background);
border-left: 1px solid var(--border);
}
.write-preview-content {
max-width: 800px;
margin: 0 auto;
padding: 68px 56px 100px;
}
.write-preview h1 {
margin: 0 0 24px;
font-size: clamp(32px, 3vw, 44px);
letter-spacing: -0.04em;
line-height: 1.35;
}
.write-preview-placeholder {
color: var(--muted-foreground);
font-size: 23px;
Expand All @@ -238,68 +259,6 @@
margin: 0 0 38px;
color: var(--primary);
}
.write-markdown {
font-size: 17px;
line-height: 1.85;
overflow-wrap: anywhere;
}
.write-markdown h1,
.write-markdown h2,
.write-markdown h3 {
margin: 1.6em 0 0.7em;
line-height: 1.4;
}
.write-markdown h1 {
font-size: 2em;
}
.write-markdown h2 {
font-size: 1.55em;
}
.write-markdown h3 {
font-size: 1.25em;
}
.write-markdown p {
margin: 0 0 1.3em;
}
.write-markdown img {
max-width: 100%;
height: auto;
}
.write-markdown pre {
overflow: auto;
padding: 18px;
background: var(--muted);
border-radius: 6px;
font-size: 14px;
}
.write-markdown code {
background: var(--muted);
border-radius: 3px;
padding: 2px 4px;
font-size: 0.9em;
}
.write-markdown pre code {
padding: 0;
}
.write-markdown blockquote {
margin: 1.5em 0;
padding: 2px 0 2px 18px;
border-left: 4px solid var(--primary);
color: var(--muted-foreground);
}
.write-markdown a {
color: var(--primary);
}
.write-markdown table {
border-collapse: collapse;
display: block;
overflow-x: auto;
}
.write-markdown th,
.write-markdown td {
border: 1px solid var(--border);
padding: 8px 12px;
}
.write-status {
position: fixed;
top: 24px;
Expand Down Expand Up @@ -504,33 +463,13 @@
}

@media (max-width: 900px) {
.write-layout {
display: block;
}
.write-pane,
.write-preview {
width: 100%;
}
.write-preview {
display: none;
position: fixed;
inset: 0 0 68px;
height: auto;
z-index: 2;
}
.write-preview.is-visible {
display: block;
.write-layout.view-both {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
}
.write-preview-content {
padding: 35px 24px;
}
.write-footer {
position: relative;
z-index: 3;
}
.write-mobile-preview {
display: inline-block;
}
}

@media (max-width: 600px) {
Expand Down
Loading