feat(typst): browser-side live preview via typst.ts WASM
Build and Deploy Verso / deploy (push) Successful in 12m51s
Build and Deploy Verso / deploy (push) Successful in 12m51s
Adds a dual-mode Typst preview: a new "Live (browser)" mode compiles and
renders Typst documents entirely in-browser using typst.ts WASM (28 MB
compiler + 1 MB renderer). The existing server-side PDF mode is preserved
and selectable via a new "Preview mode" section in the recompile dropdown,
visible only for Typst projects.
Architecture:
- Web Worker (typst-preview-worker.ts) runs the WASM compiler; queues
compile requests so only the latest compile runs after each keypress
- TypstWasmPreview component initialises the renderer on the main thread,
listens to changedAt from the compile context, debounces at 400 ms, and
renders SVG into a container div via renderToSvg
- typstPreviewMode ('wasm'|'pdf') is persisted per-project in localStorage
- isTypstProject, changedAt, typstPreviewMode, setTypstPreviewMode are
exposed through both LocalCompileContext and DetachCompileContext
- Fonts loaded from jsDelivr CDN (text subset only) on first use
- Phase 1: single-file Typst only (no #include, no images)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2265,6 +2265,11 @@
|
|||||||
"turn_on": "",
|
"turn_on": "",
|
||||||
"turn_on_link_sharing": "",
|
"turn_on_link_sharing": "",
|
||||||
"typst_export_feedback_message": "",
|
"typst_export_feedback_message": "",
|
||||||
|
"typst_preview_mode": "",
|
||||||
|
"typst_preview_pdf": "",
|
||||||
|
"typst_preview_wasm": "",
|
||||||
|
"typst_wasm_error": "",
|
||||||
|
"typst_wasm_loading": "",
|
||||||
"unarchive": "",
|
"unarchive": "",
|
||||||
"uncategorized": "",
|
"uncategorized": "",
|
||||||
"uncategorized_projects": "",
|
"uncategorized_projects": "",
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ function PdfCompileButton() {
|
|||||||
smoothPdfTransition,
|
smoothPdfTransition,
|
||||||
setSmoothPdfTransition,
|
setSmoothPdfTransition,
|
||||||
isLatexProject,
|
isLatexProject,
|
||||||
|
isTypstProject,
|
||||||
|
typstPreviewMode,
|
||||||
|
setTypstPreviewMode,
|
||||||
} = useCompileContext()
|
} = useCompileContext()
|
||||||
const { enableStopOnFirstError, disableStopOnFirstError } =
|
const { enableStopOnFirstError, disableStopOnFirstError } =
|
||||||
useStopOnFirstError({ eventSource: 'dropdown' })
|
useStopOnFirstError({ eventSource: 'dropdown' })
|
||||||
@@ -172,6 +175,30 @@ function PdfCompileButton() {
|
|||||||
{t('off')}
|
{t('off')}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</li>
|
</li>
|
||||||
|
{isTypstProject && (
|
||||||
|
<>
|
||||||
|
<DropdownDivider />
|
||||||
|
<DropdownHeader>{t('typst_preview_mode')}</DropdownHeader>
|
||||||
|
<li role="none">
|
||||||
|
<DropdownItem
|
||||||
|
as="button"
|
||||||
|
onClick={() => setTypstPreviewMode('wasm')}
|
||||||
|
trailingIcon={typstPreviewMode === 'wasm' ? 'check' : null}
|
||||||
|
>
|
||||||
|
{t('typst_preview_wasm')}
|
||||||
|
</DropdownItem>
|
||||||
|
</li>
|
||||||
|
<li role="none">
|
||||||
|
<DropdownItem
|
||||||
|
as="button"
|
||||||
|
onClick={() => setTypstPreviewMode('pdf')}
|
||||||
|
trailingIcon={typstPreviewMode === 'pdf' ? 'check' : null}
|
||||||
|
>
|
||||||
|
{t('typst_preview_pdf')}
|
||||||
|
</DropdownItem>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{isLatexProject && (
|
{isLatexProject && (
|
||||||
<>
|
<>
|
||||||
<DropdownDivider />
|
<DropdownDivider />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ElementType, memo, Suspense } from 'react'
|
import { ElementType, lazy, memo, Suspense } from 'react'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import PdfViewer from './pdf-viewer'
|
import PdfViewer from './pdf-viewer'
|
||||||
import { FullSizeLoadingSpinner } from '../../../shared/components/loading-spinner'
|
import { FullSizeLoadingSpinner } from '../../../shared/components/loading-spinner'
|
||||||
@@ -12,12 +12,22 @@ import PdfCodeCheckFailedBanner from '@/features/pdf-preview/components/pdf-code
|
|||||||
import getMeta from '@/utils/meta'
|
import getMeta from '@/utils/meta'
|
||||||
import PdfLogsViewer from '@/features/pdf-preview/components/pdf-logs-viewer'
|
import PdfLogsViewer from '@/features/pdf-preview/components/pdf-logs-viewer'
|
||||||
|
|
||||||
|
const TypstWasmPreview = lazy(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "typst-wasm-preview" */
|
||||||
|
'@/features/typst-preview/components/typst-wasm-preview'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
function PdfPreviewPane() {
|
function PdfPreviewPane() {
|
||||||
const {
|
const {
|
||||||
pdfUrl,
|
pdfUrl,
|
||||||
pdfViewer,
|
pdfViewer,
|
||||||
darkModePdf: darkModeSetting,
|
darkModePdf: darkModeSetting,
|
||||||
activeOverallTheme,
|
activeOverallTheme,
|
||||||
|
isTypstProject,
|
||||||
|
typstPreviewMode,
|
||||||
} = useCompileContext()
|
} = useCompileContext()
|
||||||
const { compileTimeout } = getMeta('ol-compileSettings')
|
const { compileTimeout } = getMeta('ol-compileSettings')
|
||||||
const isHtmlOutput = pdfUrl?.includes('output.html')
|
const isHtmlOutput = pdfUrl?.includes('output.html')
|
||||||
@@ -26,8 +36,9 @@ function PdfPreviewPane() {
|
|||||||
activeOverallTheme === 'dark' &&
|
activeOverallTheme === 'dark' &&
|
||||||
darkModeSetting
|
darkModeSetting
|
||||||
|
|
||||||
|
const isWasmMode = isTypstProject && typstPreviewMode === 'wasm'
|
||||||
const classes = classNames('pdf', 'full-size', {
|
const classes = classNames('pdf', 'full-size', {
|
||||||
'pdf-empty': !pdfUrl,
|
'pdf-empty': !pdfUrl && !isWasmMode,
|
||||||
'pdf-dark-mode': darkModePdf,
|
'pdf-dark-mode': darkModePdf,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -46,7 +57,11 @@ function PdfPreviewPane() {
|
|||||||
</PdfPreviewMessages>
|
</PdfPreviewMessages>
|
||||||
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
|
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
|
||||||
<div className="pdf-viewer" data-testid="pdf-viewer">
|
<div className="pdf-viewer" data-testid="pdf-viewer">
|
||||||
<PdfViewer />
|
{isWasmMode ? (
|
||||||
|
<TypstWasmPreview />
|
||||||
|
) : (
|
||||||
|
<PdfViewer />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<PdfLogsViewer />
|
<PdfLogsViewer />
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
memo,
|
||||||
|
type FC,
|
||||||
|
} from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { createTypstRenderer } from '@myriaddreamin/typst.ts'
|
||||||
|
import type { TypstRenderer } from '@myriaddreamin/typst.ts'
|
||||||
|
import rendererWasmUrl from '@myriaddreamin/typst-ts-renderer/pkg/typst_ts_renderer_bg.wasm'
|
||||||
|
import { useLocalCompileContext } from '@/shared/context/local-compile-context'
|
||||||
|
import { useEditorViewContext } from '@/features/ide-react/context/editor-view-context'
|
||||||
|
|
||||||
|
type Status = 'initializing' | 'ready' | 'compiling' | 'error'
|
||||||
|
|
||||||
|
const createTypstWorker = () =>
|
||||||
|
new Worker(
|
||||||
|
/* webpackChunkName: "typst-preview-worker" */
|
||||||
|
new URL('../typst-preview-worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' }
|
||||||
|
)
|
||||||
|
|
||||||
|
const TypstWasmPreview: FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { changedAt } = useLocalCompileContext()
|
||||||
|
const { view } = useEditorViewContext()
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const workerRef = useRef<Worker | null>(null)
|
||||||
|
const rendererRef = useRef<TypstRenderer | null>(null)
|
||||||
|
const isReadyRef = useRef(false)
|
||||||
|
const compileTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
|
const [status, setStatus] = useState<Status>('initializing')
|
||||||
|
const [errorMsg, setErrorMsg] = useState('')
|
||||||
|
|
||||||
|
const triggerCompile = useCallback(() => {
|
||||||
|
const worker = workerRef.current
|
||||||
|
if (!worker || !isReadyRef.current) return
|
||||||
|
const content = view?.state.doc.toString() ?? ''
|
||||||
|
if (!content) return
|
||||||
|
|
||||||
|
if (compileTimerRef.current) clearTimeout(compileTimerRef.current)
|
||||||
|
compileTimerRef.current = setTimeout(() => {
|
||||||
|
setStatus('compiling')
|
||||||
|
worker.postMessage({
|
||||||
|
type: 'compile',
|
||||||
|
content,
|
||||||
|
mainFilePath: '/main.typ',
|
||||||
|
})
|
||||||
|
}, 400)
|
||||||
|
}, [view])
|
||||||
|
|
||||||
|
// Init renderer (main thread, needs DOM)
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
const renderer = createTypstRenderer()
|
||||||
|
renderer
|
||||||
|
.init({ getModule: () => fetch(rendererWasmUrl) })
|
||||||
|
.then(() => {
|
||||||
|
if (!cancelled) rendererRef.current = renderer
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setStatus('error')
|
||||||
|
setErrorMsg(`Renderer init failed: ${err}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Init worker and wire message handler
|
||||||
|
useEffect(() => {
|
||||||
|
const worker = createTypstWorker()
|
||||||
|
workerRef.current = worker
|
||||||
|
|
||||||
|
worker.addEventListener('message', async event => {
|
||||||
|
const msg = event.data as
|
||||||
|
| { type: 'ready' }
|
||||||
|
| { type: 'compiled'; vectorData: Uint8Array; diagnostics: any[] }
|
||||||
|
| { type: 'error'; message: string }
|
||||||
|
|
||||||
|
if (msg.type === 'ready') {
|
||||||
|
isReadyRef.current = true
|
||||||
|
setStatus('ready')
|
||||||
|
triggerCompile()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'compiled') {
|
||||||
|
const renderer = rendererRef.current
|
||||||
|
const container = containerRef.current
|
||||||
|
if (!renderer || !container) {
|
||||||
|
setStatus('ready')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await renderer.runWithSession(async session => {
|
||||||
|
session.manipulateData({
|
||||||
|
action: 'reset',
|
||||||
|
data: msg.vectorData,
|
||||||
|
})
|
||||||
|
await renderer.renderToSvg({
|
||||||
|
renderSession: session,
|
||||||
|
container,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
setStatus('ready')
|
||||||
|
setErrorMsg('')
|
||||||
|
} catch (e) {
|
||||||
|
setStatus('error')
|
||||||
|
setErrorMsg(`Render failed: ${e}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'error') {
|
||||||
|
setStatus('error')
|
||||||
|
setErrorMsg(msg.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (compileTimerRef.current) clearTimeout(compileTimerRef.current)
|
||||||
|
worker.terminate()
|
||||||
|
workerRef.current = null
|
||||||
|
isReadyRef.current = false
|
||||||
|
}
|
||||||
|
}, [triggerCompile])
|
||||||
|
|
||||||
|
// Re-compile when document changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (changedAt > 0) triggerCompile()
|
||||||
|
}, [changedAt, triggerCompile])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="typst-wasm-preview">
|
||||||
|
{status === 'initializing' && (
|
||||||
|
<div className="typst-wasm-preview-status">
|
||||||
|
<div className="typst-wasm-preview-spinner" />
|
||||||
|
<span>{t('typst_wasm_loading')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{status === 'error' && (
|
||||||
|
<div className="typst-wasm-preview-error">
|
||||||
|
<strong>{t('typst_wasm_error')}</strong>
|
||||||
|
{errorMsg && <pre>{errorMsg}</pre>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="typst-wasm-preview-container"
|
||||||
|
style={{ display: status === 'initializing' ? 'none' : 'block' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(TypstWasmPreview)
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { createTypstCompiler, preloadRemoteFonts } from '@myriaddreamin/typst.ts'
|
||||||
|
import type { TypstCompiler } from '@myriaddreamin/typst.ts'
|
||||||
|
import compilerWasmUrl from '@myriaddreamin/typst-ts-web-compiler/pkg/typst_ts_web_compiler_bg.wasm'
|
||||||
|
|
||||||
|
type IncomingMessage = {
|
||||||
|
type: 'compile'
|
||||||
|
content: string
|
||||||
|
mainFilePath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutgoingMessage =
|
||||||
|
| { type: 'ready' }
|
||||||
|
| { type: 'compiled'; vectorData: Uint8Array; diagnostics: any[] }
|
||||||
|
| { type: 'error'; message: string }
|
||||||
|
|
||||||
|
let compiler: TypstCompiler | null = null
|
||||||
|
let compileQueue: IncomingMessage | null = null
|
||||||
|
let isCompiling = false
|
||||||
|
|
||||||
|
async function runCompile(msg: IncomingMessage) {
|
||||||
|
if (!compiler) return
|
||||||
|
isCompiling = true
|
||||||
|
try {
|
||||||
|
compiler.resetShadow()
|
||||||
|
compiler.addSource(msg.mainFilePath, msg.content)
|
||||||
|
const result = await (compiler as any).compile({
|
||||||
|
mainFilePath: msg.mainFilePath,
|
||||||
|
format: 0, // CompileFormatEnum.vector
|
||||||
|
diagnostics: 'full',
|
||||||
|
})
|
||||||
|
if (result?.result) {
|
||||||
|
self.postMessage(
|
||||||
|
{
|
||||||
|
type: 'compiled',
|
||||||
|
vectorData: result.result,
|
||||||
|
diagnostics: result.diagnostics ?? [],
|
||||||
|
} satisfies OutgoingMessage,
|
||||||
|
[result.result.buffer]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
self.postMessage({
|
||||||
|
type: 'error',
|
||||||
|
message: 'Compilation produced no output',
|
||||||
|
} satisfies OutgoingMessage)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
self.postMessage({
|
||||||
|
type: 'error',
|
||||||
|
message: String(e),
|
||||||
|
} satisfies OutgoingMessage)
|
||||||
|
} finally {
|
||||||
|
isCompiling = false
|
||||||
|
if (compileQueue) {
|
||||||
|
const next = compileQueue
|
||||||
|
compileQueue = null
|
||||||
|
await runCompile(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initPromise = (async () => {
|
||||||
|
const c = createTypstCompiler()
|
||||||
|
await c.init({
|
||||||
|
getModule: () => fetch(compilerWasmUrl),
|
||||||
|
beforeBuild: [preloadRemoteFonts([], { assets: ['text'] })],
|
||||||
|
})
|
||||||
|
compiler = c
|
||||||
|
self.postMessage({ type: 'ready' } satisfies OutgoingMessage)
|
||||||
|
})().catch(err => {
|
||||||
|
compiler = null
|
||||||
|
self.postMessage({
|
||||||
|
type: 'error',
|
||||||
|
message: `Typst compiler init failed: ${err}`,
|
||||||
|
} satisfies OutgoingMessage)
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener('message', async (event: MessageEvent<IncomingMessage>) => {
|
||||||
|
if (event.data.type === 'compile') {
|
||||||
|
await initPromise
|
||||||
|
if (!compiler) return
|
||||||
|
if (isCompiling) {
|
||||||
|
compileQueue = event.data
|
||||||
|
} else {
|
||||||
|
await runCompile(event.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -77,6 +77,10 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
|
|||||||
smoothPdfTransition: _smoothPdfTransition,
|
smoothPdfTransition: _smoothPdfTransition,
|
||||||
setSmoothPdfTransition: _setSmoothPdfTransition,
|
setSmoothPdfTransition: _setSmoothPdfTransition,
|
||||||
isLatexProject: _isLatexProject,
|
isLatexProject: _isLatexProject,
|
||||||
|
isTypstProject: _isTypstProject,
|
||||||
|
changedAt: _changedAt,
|
||||||
|
typstPreviewMode: _typstPreviewMode,
|
||||||
|
setTypstPreviewMode: _setTypstPreviewMode,
|
||||||
} = localCompileContext
|
} = localCompileContext
|
||||||
|
|
||||||
const [animateCompileDropdownArrow] = useDetachStateWatcher(
|
const [animateCompileDropdownArrow] = useDetachStateWatcher(
|
||||||
@@ -159,6 +163,24 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
|
|||||||
'detacher',
|
'detacher',
|
||||||
'detached'
|
'detached'
|
||||||
)
|
)
|
||||||
|
const [isTypstProject] = useDetachStateWatcher(
|
||||||
|
'isTypstProject',
|
||||||
|
_isTypstProject,
|
||||||
|
'detacher',
|
||||||
|
'detached'
|
||||||
|
)
|
||||||
|
const [changedAt] = useDetachStateWatcher(
|
||||||
|
'changedAt',
|
||||||
|
_changedAt,
|
||||||
|
'detacher',
|
||||||
|
'detached'
|
||||||
|
)
|
||||||
|
const [typstPreviewMode] = useDetachStateWatcher(
|
||||||
|
'typstPreviewMode',
|
||||||
|
_typstPreviewMode,
|
||||||
|
'detacher',
|
||||||
|
'detached'
|
||||||
|
)
|
||||||
const [lastCompileOptions] = useDetachStateWatcher(
|
const [lastCompileOptions] = useDetachStateWatcher(
|
||||||
'lastCompileOptions',
|
'lastCompileOptions',
|
||||||
_lastCompileOptions,
|
_lastCompileOptions,
|
||||||
@@ -418,6 +440,13 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
|
|||||||
'detacher'
|
'detacher'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const setTypstPreviewMode = useDetachAction(
|
||||||
|
'setTypstPreviewMode',
|
||||||
|
_setTypstPreviewMode,
|
||||||
|
'detached',
|
||||||
|
'detacher'
|
||||||
|
)
|
||||||
|
|
||||||
useCompileTriggers(startCompile, setChangedAt)
|
useCompileTriggers(startCompile, setChangedAt)
|
||||||
useLogEvents(setShowLogs)
|
useLogEvents(setShowLogs)
|
||||||
|
|
||||||
@@ -482,6 +511,10 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
|
|||||||
smoothPdfTransition,
|
smoothPdfTransition,
|
||||||
setSmoothPdfTransition,
|
setSmoothPdfTransition,
|
||||||
isLatexProject,
|
isLatexProject,
|
||||||
|
isTypstProject,
|
||||||
|
changedAt,
|
||||||
|
typstPreviewMode,
|
||||||
|
setTypstPreviewMode,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
animateCompileDropdownArrow,
|
animateCompileDropdownArrow,
|
||||||
@@ -541,6 +574,10 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
|
|||||||
smoothPdfTransition,
|
smoothPdfTransition,
|
||||||
setSmoothPdfTransition,
|
setSmoothPdfTransition,
|
||||||
isLatexProject,
|
isLatexProject,
|
||||||
|
isTypstProject,
|
||||||
|
changedAt,
|
||||||
|
typstPreviewMode,
|
||||||
|
setTypstPreviewMode,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ export type CompileContext = {
|
|||||||
setDarkModePdf: (value: boolean) => void
|
setDarkModePdf: (value: boolean) => void
|
||||||
activeOverallTheme: ActiveOverallTheme
|
activeOverallTheme: ActiveOverallTheme
|
||||||
isLatexProject: boolean
|
isLatexProject: boolean
|
||||||
|
isTypstProject: boolean
|
||||||
|
changedAt: number
|
||||||
|
typstPreviewMode: 'wasm' | 'pdf'
|
||||||
|
setTypstPreviewMode: (value: 'wasm' | 'pdf') => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LocalCompileContext = createContext<CompileContext | undefined>(
|
export const LocalCompileContext = createContext<CompileContext | undefined>(
|
||||||
@@ -305,6 +309,9 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
|||||||
const [isTypstProject, setIsTypstProject] = useState(false)
|
const [isTypstProject, setIsTypstProject] = useState(false)
|
||||||
const [isQuartoProject, setIsQuartoProject] = useState(false)
|
const [isQuartoProject, setIsQuartoProject] = useState(false)
|
||||||
const isLatexProject = !isTypstProject && !isQuartoProject
|
const isLatexProject = !isTypstProject && !isQuartoProject
|
||||||
|
const [typstPreviewMode, setTypstPreviewMode] = usePersistedState<
|
||||||
|
'wasm' | 'pdf'
|
||||||
|
>(`typst_preview_mode:${projectId}`, 'wasm', { listen: true })
|
||||||
const smoothPdfTransition = isTypstProject
|
const smoothPdfTransition = isTypstProject
|
||||||
? smoothTransitionTypst
|
? smoothTransitionTypst
|
||||||
: smoothTransitionLatex
|
: smoothTransitionLatex
|
||||||
@@ -879,6 +886,10 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
|||||||
smoothPdfTransition,
|
smoothPdfTransition,
|
||||||
setSmoothPdfTransition,
|
setSmoothPdfTransition,
|
||||||
isLatexProject,
|
isLatexProject,
|
||||||
|
isTypstProject,
|
||||||
|
changedAt,
|
||||||
|
typstPreviewMode,
|
||||||
|
setTypstPreviewMode,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
animateCompileDropdownArrow,
|
animateCompileDropdownArrow,
|
||||||
@@ -937,6 +948,10 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
|||||||
smoothPdfTransition,
|
smoothPdfTransition,
|
||||||
setSmoothPdfTransition,
|
setSmoothPdfTransition,
|
||||||
isLatexProject,
|
isLatexProject,
|
||||||
|
isTypstProject,
|
||||||
|
changedAt,
|
||||||
|
typstPreviewMode,
|
||||||
|
setTypstPreviewMode,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -520,3 +520,64 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.typst-wasm-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--pdf-bg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typst-wasm-preview-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-04);
|
||||||
|
padding: var(--spacing-08);
|
||||||
|
color: var(--content-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typst-wasm-preview-spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--neutral-40);
|
||||||
|
border-top-color: var(--content-primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: typst-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typst-spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.typst-wasm-preview-error {
|
||||||
|
padding: var(--spacing-06);
|
||||||
|
color: var(--content-danger);
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
margin-top: var(--spacing-02);
|
||||||
|
background: var(--bg-dark-tertiary);
|
||||||
|
padding: var(--spacing-03);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.typst-wasm-preview-container {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing-04);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto var(--spacing-04);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgb(0 0 0 / 20%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2899,6 +2899,11 @@
|
|||||||
"turn_on_password_visibility": "Turn on password visibility",
|
"turn_on_password_visibility": "Turn on password visibility",
|
||||||
"tutorials": "Tutorials",
|
"tutorials": "Tutorials",
|
||||||
"typst_export_feedback_message": "This conversion may contain errors — pandoc does not support all LaTeX constructs.",
|
"typst_export_feedback_message": "This conversion may contain errors — pandoc does not support all LaTeX constructs.",
|
||||||
|
"typst_preview_mode": "Preview mode",
|
||||||
|
"typst_preview_pdf": "PDF (server)",
|
||||||
|
"typst_preview_wasm": "Live (browser)",
|
||||||
|
"typst_wasm_error": "Preview error",
|
||||||
|
"typst_wasm_loading": "Loading Typst compiler…",
|
||||||
"uk": "Ukrainian",
|
"uk": "Ukrainian",
|
||||||
"unable_to_extract_the_supplied_zip_file": "Opening this content on Overleaf failed because the zip file could not be extracted. Please ensure that it is a valid zip file. If this keeps happening for links on a particular site, please report this to them.",
|
"unable_to_extract_the_supplied_zip_file": "Opening this content on Overleaf failed because the zip file could not be extracted. Please ensure that it is a valid zip file. If this keeps happening for links on a particular site, please report this to them.",
|
||||||
"unarchive": "Restore",
|
"unarchive": "Restore",
|
||||||
|
|||||||
@@ -2904,6 +2904,11 @@
|
|||||||
"turn_on_password_visibility": "Activer la visibilité du mot de passe",
|
"turn_on_password_visibility": "Activer la visibilité du mot de passe",
|
||||||
"tutorials": "Tutoriels",
|
"tutorials": "Tutoriels",
|
||||||
"typst_export_feedback_message": "Cette conversion peut contenir des erreurs — pandoc ne supporte pas toutes les constructions LaTeX.",
|
"typst_export_feedback_message": "Cette conversion peut contenir des erreurs — pandoc ne supporte pas toutes les constructions LaTeX.",
|
||||||
|
"typst_preview_mode": "Mode de prévisualisation",
|
||||||
|
"typst_preview_pdf": "PDF (serveur)",
|
||||||
|
"typst_preview_wasm": "Direct (navigateur)",
|
||||||
|
"typst_wasm_error": "Erreur de prévisualisation",
|
||||||
|
"typst_wasm_loading": "Chargement du compilateur Typst…",
|
||||||
"uk": "Ukrainien",
|
"uk": "Ukrainien",
|
||||||
"unable_to_extract_the_supplied_zip_file": "L’ouverture de ce contenu sur Overleaf a échoué car l’archive n’a pas pu être extraite. Veuillez vous assurer de la validité de cette archive. Si cela se produit régulièrement pour un site donné, veuillez leur faire part du problème.",
|
"unable_to_extract_the_supplied_zip_file": "L’ouverture de ce contenu sur Overleaf a échoué car l’archive n’a pas pu être extraite. Veuillez vous assurer de la validité de cette archive. Si cela se produit régulièrement pour un site donné, veuillez leur faire part du problème.",
|
||||||
"unarchive": "Restaurer",
|
"unarchive": "Restaurer",
|
||||||
|
|||||||
@@ -84,6 +84,9 @@
|
|||||||
"@customerio/cdp-analytics-node": "^0.3.9",
|
"@customerio/cdp-analytics-node": "^0.3.9",
|
||||||
"@google-cloud/bigquery": "^8.1.1",
|
"@google-cloud/bigquery": "^8.1.1",
|
||||||
"@google-cloud/storage": "^7.19.0",
|
"@google-cloud/storage": "^7.19.0",
|
||||||
|
"@myriaddreamin/typst-ts-renderer": "0.7.0",
|
||||||
|
"@myriaddreamin/typst-ts-web-compiler": "0.7.0",
|
||||||
|
"@myriaddreamin/typst.ts": "0.7.0",
|
||||||
"@node-oauth/oauth2-server": "^5.3.0",
|
"@node-oauth/oauth2-server": "^5.3.0",
|
||||||
"@node-saml/passport-saml": "^5.1.0",
|
"@node-saml/passport-saml": "^5.1.0",
|
||||||
"@overleaf/access-token-encryptor": "workspace:*",
|
"@overleaf/access-token-encryptor": "workspace:*",
|
||||||
|
|||||||
@@ -750,6 +750,13 @@ const BASE_COMPILE_CONTEXT_MOCK = {
|
|||||||
darkModePdf: false,
|
darkModePdf: false,
|
||||||
setDarkModePdf: () => {},
|
setDarkModePdf: () => {},
|
||||||
activeOverallTheme: 'light',
|
activeOverallTheme: 'light',
|
||||||
|
smoothPdfTransition: false,
|
||||||
|
setSmoothPdfTransition: () => {},
|
||||||
|
isLatexProject: true,
|
||||||
|
isTypstProject: false,
|
||||||
|
changedAt: 0,
|
||||||
|
typstPreviewMode: 'wasm' as const,
|
||||||
|
setTypstPreviewMode: () => {},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const makeDetachCompileProvider = (mockCompileOnLoad: boolean = false) => {
|
const makeDetachCompileProvider = (mockCompileOnLoad: boolean = false) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user