Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bc51d6d14 | |||
| 95829f04b2 |
@@ -21,7 +21,10 @@ export default defineConfig({
|
|||||||
service: { entrypoint: 'astro/assets/services/noop' }
|
service: { entrypoint: 'astro/assets/services/noop' }
|
||||||
},
|
},
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()]
|
plugins: [tailwindcss()],
|
||||||
|
build: {
|
||||||
|
chunkSizeWarningLimit: 600,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
adapter: node({
|
adapter: node({
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback, lazy, Suspense } from 'react';
|
||||||
import { EditorView, keymap, placeholder as cmPlaceholder } from '@codemirror/view';
|
import { EditorView, keymap, placeholder as cmPlaceholder } from '@codemirror/view';
|
||||||
import { EditorState, Compartment } from '@codemirror/state';
|
import { EditorState, Compartment } from '@codemirror/state';
|
||||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||||
import { languages } from '@codemirror/language-data';
|
import { languages } from '@codemirror/language-data';
|
||||||
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
||||||
import { vim } from '@replit/codemirror-vim';
|
|
||||||
import { search, searchKeymap } from '@codemirror/search';
|
import { search, searchKeymap } from '@codemirror/search';
|
||||||
import { closeBrackets } from '@codemirror/autocomplete';
|
import { closeBrackets } from '@codemirror/autocomplete';
|
||||||
import { renderMarkdown } from '../../../lib/markdown';
|
|
||||||
import { getPost, savePost, deletePost, getAssets, uploadAsset, ApiError } from '../../../lib/api';
|
import { getPost, savePost, deletePost, getAssets, uploadAsset, ApiError } from '../../../lib/api';
|
||||||
import type { Asset } from '../../../lib/types';
|
import type { Asset } from '../../../lib/types';
|
||||||
import AssetManager from './AssetManager';
|
|
||||||
|
const AssetManager = lazy(() => import('./AssetManager'));
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
editSlug?: string;
|
editSlug?: string;
|
||||||
@@ -80,6 +79,7 @@ export default function Editor({ editSlug }: Props) {
|
|||||||
const previewTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const previewTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const updatePreviewRef = useRef<() => void>(() => {});
|
const updatePreviewRef = useRef<() => void>(() => {});
|
||||||
const uploadFnRef = useRef<(files: File[], insertAt?: number) => void>(() => {});
|
const uploadFnRef = useRef<(files: File[], insertAt?: number) => void>(() => {});
|
||||||
|
const renderMarkdownRef = useRef<((src: string) => string) | null>(null);
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [slug, setSlug] = useState(editSlug || '');
|
const [slug, setSlug] = useState(editSlug || '');
|
||||||
@@ -107,10 +107,14 @@ export default function Editor({ editSlug }: Props) {
|
|||||||
setTimeout(() => setAlert(null), 5000);
|
setTimeout(() => setAlert(null), 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePreview = useCallback(() => {
|
const updatePreview = useCallback(async () => {
|
||||||
if (!showPreview || !viewRef.current || !previewRef.current) return;
|
if (!showPreview || !viewRef.current || !previewRef.current) return;
|
||||||
|
if (!renderMarkdownRef.current) {
|
||||||
|
const mod = await import('../../../lib/markdown');
|
||||||
|
renderMarkdownRef.current = mod.renderMarkdown;
|
||||||
|
}
|
||||||
const content = viewRef.current.state.doc.toString();
|
const content = viewRef.current.state.doc.toString();
|
||||||
previewRef.current.innerHTML = renderMarkdown(content);
|
previewRef.current.innerHTML = renderMarkdownRef.current(content);
|
||||||
}, [showPreview]);
|
}, [showPreview]);
|
||||||
|
|
||||||
useEffect(() => { updatePreviewRef.current = updatePreview; }, [updatePreview]);
|
useEffect(() => { updatePreviewRef.current = updatePreview; }, [updatePreview]);
|
||||||
@@ -197,12 +201,19 @@ export default function Editor({ editSlug }: Props) {
|
|||||||
return () => { view.destroy(); viewRef.current = null; };
|
return () => { view.destroy(); viewRef.current = null; };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Hot-swap vim mode via compartment reconfiguration
|
// Hot-swap vim mode via compartment reconfiguration; lazy-load vim module
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!viewRef.current) return;
|
if (!viewRef.current) return;
|
||||||
viewRef.current.dispatch({
|
if (!vimEnabled) {
|
||||||
effects: vimCompartment.reconfigure(vimEnabled ? vim() : []),
|
viewRef.current.dispatch({ effects: vimCompartment.reconfigure([]) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cancelled = false;
|
||||||
|
import('@replit/codemirror-vim').then(({ vim }) => {
|
||||||
|
if (cancelled || !viewRef.current) return;
|
||||||
|
viewRef.current.dispatch({ effects: vimCompartment.reconfigure(vim()) });
|
||||||
});
|
});
|
||||||
|
return () => { cancelled = true; };
|
||||||
}, [vimEnabled]);
|
}, [vimEnabled]);
|
||||||
|
|
||||||
// Load existing post for editing
|
// Load existing post for editing
|
||||||
@@ -641,7 +652,9 @@ export default function Editor({ editSlug }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div className="p-4 md:p-6 overflow-y-auto flex-1 bg-[var(--base)]/50">
|
<div className="p-4 md:p-6 overflow-y-auto flex-1 bg-[var(--base)]/50">
|
||||||
|
<Suspense fallback={<div className="text-center py-12 font-display italic text-[var(--subtext0)]">Loading assets…</div>}>
|
||||||
<AssetManager mode="select" onSelect={handleAssetSelect} />
|
<AssetManager mode="select" onSelect={handleAssetSelect} />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -75,10 +75,17 @@ export default function Inbox() {
|
|||||||
className="border border-[var(--surface2)]/60"
|
className="border border-[var(--surface2)]/60"
|
||||||
style={{ borderRadius: 1 }}
|
style={{ borderRadius: 1 }}
|
||||||
>
|
>
|
||||||
<button
|
<div
|
||||||
type="button"
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
onClick={() => setExpandedId(isOpen ? null : m.id)}
|
onClick={() => setExpandedId(isOpen ? null : m.id)}
|
||||||
className="w-full flex flex-col md:flex-row md:items-baseline md:justify-between gap-2 md:gap-4 px-5 py-4 text-left hover:bg-[var(--surface0)]/40 transition-colors"
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
setExpandedId(isOpen ? null : m.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full flex flex-col md:flex-row md:items-baseline md:justify-between gap-2 md:gap-4 px-5 py-4 text-left hover:bg-[var(--surface0)]/40 transition-colors cursor-pointer"
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
>
|
>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -87,13 +94,23 @@ export default function Inbox() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="font-display italic text-xs text-[var(--subtext0)] tracking-wider mt-1 truncate">
|
<div className="font-display italic text-xs text-[var(--subtext0)] tracking-wider mt-1 truncate">
|
||||||
{m.name ? `${m.name} · ` : ''}
|
{m.name ? `${m.name} · ` : ''}
|
||||||
{m.email ?? 'no email'}
|
{m.email ? (
|
||||||
|
<a
|
||||||
|
href={`mailto:${m.email}${m.subject ? `?subject=${encodeURIComponent('Re: ' + m.subject)}` : ''}`}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
className="underline decoration-[var(--surface2)] underline-offset-2 hover:text-[var(--mauve)] hover:decoration-[var(--mauve)] transition-colors"
|
||||||
|
>
|
||||||
|
{m.email}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
'no email'
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="font-display italic text-xs text-[var(--subtext0)] tracking-wider shrink-0">
|
<div className="font-display italic text-xs text-[var(--subtext0)] tracking-wider shrink-0">
|
||||||
{formatDate(m.received_at)}
|
{formatDate(m.received_at)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="px-5 pb-5 pt-2 border-t border-[var(--surface2)]/40 space-y-4">
|
<div className="px-5 pb-5 pt-2 border-t border-[var(--surface2)]/40 space-y-4">
|
||||||
<pre className="font-sans whitespace-pre-wrap text-[var(--text)] text-sm leading-relaxed">
|
<pre className="font-sans whitespace-pre-wrap text-[var(--text)] text-sm leading-relaxed">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Marked } from 'marked';
|
|||||||
import markedKatex from 'marked-katex-extension';
|
import markedKatex from 'marked-katex-extension';
|
||||||
import { markedHighlight } from 'marked-highlight';
|
import { markedHighlight } from 'marked-highlight';
|
||||||
import { gfmHeadingId } from 'marked-gfm-heading-id';
|
import { gfmHeadingId } from 'marked-gfm-heading-id';
|
||||||
import hljs from 'highlight.js';
|
import hljs from 'highlight.js/lib/common';
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
|
||||||
function escapeHtml(s: string): string {
|
function escapeHtml(s: string): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user