journal: real markdown editor (EasyMDE) with live preview toggle, fix default template
- Replace the plain textarea with EasyMDE (vendored locally, no CDN) for markdown authoring: syntax highlighting, smart list continuation, and a custom text-based toolbar (built-in EasyMDE toolbar icons require Font Awesome from a CDN, which this app doesn't use). unorderedListStyle is set to "-" to match the app's own template convention. - Add a preview/edit toggle button that swaps the editor for the exact same server-rendered markdown (via /journal/preview) shown after saving, instead of always showing both. - Fix create/edit entry routes to verify the submitted category_id actually belongs to the current user before inserting -- every other write path in this app already checked ownership; this one didn't (found while manually testing the new editor with a typo'd category id that happened to belong to someone else's category, which surfaced as an IntegrityError 500 instead of a clean 404-equivalent). - Fix the default "일상" category template: bare "-" bullet lines don't parse as list items in the markdown renderer (they need a trailing space), and the content_template validator was silently stripping that trailing space off on every save. Backfill migration updates any category still holding the old, broken template text. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+122
-1
@@ -902,8 +902,129 @@ label {
|
||||
}
|
||||
|
||||
.journal-entry-content {
|
||||
white-space: pre-wrap;
|
||||
margin-top: 4px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.journal-preview {
|
||||
margin-top: 6px;
|
||||
padding: 10px 12px;
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--color-bg);
|
||||
min-height: 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 마크다운 에디터 툴바 (EasyMDE는 toolbar:false로 끄고 여기서 자체 버튼으로 대체 —
|
||||
EasyMDE 기본 툴바는 Font Awesome CDN을 전제로 해서 이 앱의 "CDN 금지" 원칙과 안 맞는다) */
|
||||
.markdown-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.md-tool-btn {
|
||||
min-width: 30px;
|
||||
height: 30px;
|
||||
padding: 0 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.md-tool-btn:active {
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
/* EasyMDE(CodeMirror) 컨테이너를 이 앱의 입력 필드 톤에 맞춘다 */
|
||||
.markdown-editor + .EasyMDEContainer .CodeMirror {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: inherit;
|
||||
font-size: 15px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.markdown-editor + .EasyMDEContainer .CodeMirror-cursor {
|
||||
border-left-color: var(--color-text);
|
||||
}
|
||||
|
||||
.markdown-editor + .EasyMDEContainer .editor-statusbar {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.journal-entry-content > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.journal-entry-content > *:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.journal-entry-content p,
|
||||
.journal-entry-content ul,
|
||||
.journal-entry-content ol,
|
||||
.journal-entry-content blockquote,
|
||||
.journal-entry-content pre {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.journal-entry-content h1,
|
||||
.journal-entry-content h2,
|
||||
.journal-entry-content h3,
|
||||
.journal-entry-content h4 {
|
||||
margin: 12px 0 6px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.journal-entry-content h1 { font-size: 19px; }
|
||||
.journal-entry-content h2 { font-size: 17px; }
|
||||
.journal-entry-content h3,
|
||||
.journal-entry-content h4 { font-size: 15px; }
|
||||
|
||||
.journal-entry-content ul,
|
||||
.journal-entry-content ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.journal-entry-content blockquote {
|
||||
margin-left: 0;
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid var(--color-accent);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.journal-entry-content code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
}
|
||||
|
||||
.journal-entry-content pre {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-control);
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.journal-entry-content pre code {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.journal-entry-content a {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.journal-entry-tags {
|
||||
|
||||
Reference in New Issue
Block a user