Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f5dad383b | |||
| eedf4b50f6 | |||
| c6d71e58b7 | |||
| 06085cda21 | |||
| 8515a899ac | |||
| 200bff4ecb | |||
| b5cf5f9e7b |
@@ -39,3 +39,12 @@ Ideas and features deferred from the current alpha.
|
||||
- **Quarto RevealJS**: insert slide divider (`---`), insert speaker
|
||||
notes (`::: notes`), insert columns layout, insert video embed
|
||||
(using Quarto's `{{< video >}}` shortcode).
|
||||
|
||||
### AI writing assistant
|
||||
|
||||
- **In-editor AI assistant** — Inline writing help similar to what Overleaf
|
||||
and CoCalc offer: suggest completions, rephrase selections, explain LaTeX
|
||||
errors, and generate boilerplate (figures, tables, equations). Should work
|
||||
across all three formats (`.tex`, `.typ`, `.qmd`). Backend would proxy
|
||||
requests to a configurable model API (Claude, OpenAI-compatible) so
|
||||
self-hosters can bring their own key.
|
||||
|
||||
@@ -85,7 +85,7 @@ const buildViewPolicy = (
|
||||
viewDirectives
|
||||
) => {
|
||||
const directives = [
|
||||
`script-src 'nonce-${scriptNonce}' 'unsafe-inline' 'strict-dynamic' https: 'report-sample'`, // only allow scripts from certain sources
|
||||
`script-src 'nonce-${scriptNonce}' 'unsafe-inline' 'strict-dynamic' 'wasm-unsafe-eval' https: 'report-sample'`, // only allow scripts from certain sources
|
||||
`object-src 'none'`, // forbid loading an "object" element
|
||||
`base-uri 'none'`, // forbid setting a "base" element
|
||||
...(viewDirectives ?? []),
|
||||
|
||||
@@ -2265,6 +2265,11 @@
|
||||
"turn_on": "",
|
||||
"turn_on_link_sharing": "",
|
||||
"typst_export_feedback_message": "",
|
||||
"typst_preview_mode": "",
|
||||
"typst_preview_pdf": "",
|
||||
"typst_preview_wasm": "",
|
||||
"typst_wasm_error": "",
|
||||
"typst_wasm_loading": "",
|
||||
"unarchive": "",
|
||||
"uncategorized": "",
|
||||
"uncategorized_projects": "",
|
||||
|
||||
@@ -51,6 +51,9 @@ function PdfCompileButton() {
|
||||
smoothPdfTransition,
|
||||
setSmoothPdfTransition,
|
||||
isLatexProject,
|
||||
isTypstProject,
|
||||
typstPreviewMode,
|
||||
setTypstPreviewMode,
|
||||
} = useCompileContext()
|
||||
const { enableStopOnFirstError, disableStopOnFirstError } =
|
||||
useStopOnFirstError({ eventSource: 'dropdown' })
|
||||
@@ -172,6 +175,30 @@ function PdfCompileButton() {
|
||||
{t('off')}
|
||||
</DropdownItem>
|
||||
</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 && (
|
||||
<>
|
||||
<DropdownDivider />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ElementType, memo, Suspense } from 'react'
|
||||
import { ElementType, lazy, memo, Suspense } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import PdfViewer from './pdf-viewer'
|
||||
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 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() {
|
||||
const {
|
||||
pdfUrl,
|
||||
pdfViewer,
|
||||
darkModePdf: darkModeSetting,
|
||||
activeOverallTheme,
|
||||
isTypstProject,
|
||||
typstPreviewMode,
|
||||
} = useCompileContext()
|
||||
const { compileTimeout } = getMeta('ol-compileSettings')
|
||||
const isHtmlOutput = pdfUrl?.includes('output.html')
|
||||
@@ -26,8 +36,9 @@ function PdfPreviewPane() {
|
||||
activeOverallTheme === 'dark' &&
|
||||
darkModeSetting
|
||||
|
||||
const isWasmMode = isTypstProject && typstPreviewMode === 'wasm'
|
||||
const classes = classNames('pdf', 'full-size', {
|
||||
'pdf-empty': !pdfUrl,
|
||||
'pdf-empty': !pdfUrl && !isWasmMode,
|
||||
'pdf-dark-mode': darkModePdf,
|
||||
})
|
||||
|
||||
@@ -46,7 +57,11 @@ function PdfPreviewPane() {
|
||||
</PdfPreviewMessages>
|
||||
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
|
||||
<div className="pdf-viewer" data-testid="pdf-viewer">
|
||||
{isWasmMode ? (
|
||||
<TypstWasmPreview />
|
||||
) : (
|
||||
<PdfViewer />
|
||||
)}
|
||||
</div>
|
||||
</Suspense>
|
||||
<PdfLogsViewer />
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useCallback,
|
||||
memo,
|
||||
type FC,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { createTypstRenderer } from '@myriaddreamin/typst.ts'
|
||||
import type { TypstRenderer, RenderSession } 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()
|
||||
|
||||
// Keep view in a ref so triggerCompile never has stale deps
|
||||
// (avoids recreating the worker every time the view reference changes)
|
||||
const viewRef = useRef(view)
|
||||
useEffect(() => {
|
||||
viewRef.current = view
|
||||
}, [view])
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const workerRef = useRef<Worker | null>(null)
|
||||
const rendererRef = useRef<TypstRenderer | null>(null)
|
||||
// Persistent render session — created once, reused for all subsequent renders
|
||||
// to avoid the Rust borrow/ownership panics that occur when runWithSession
|
||||
// creates and frees a session on every render
|
||||
const sessionRef = useRef<RenderSession | null>(null)
|
||||
const isReadyRef = useRef(false)
|
||||
const compileTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
// If 'compiled' arrives before renderer is ready, buffer the latest vector data
|
||||
const pendingVectorRef = useRef<Uint8Array | null>(null)
|
||||
// Prevent concurrent renders
|
||||
const isRenderingRef = useRef(false)
|
||||
const renderQueueRef = useRef<Uint8Array | null>(null)
|
||||
|
||||
const [status, setStatus] = useState<Status>('initializing')
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
|
||||
const doRender = useCallback(async (vectorData: Uint8Array) => {
|
||||
const renderer = rendererRef.current
|
||||
const container = containerRef.current
|
||||
if (!renderer || !container) {
|
||||
pendingVectorRef.current = vectorData
|
||||
return
|
||||
}
|
||||
|
||||
if (isRenderingRef.current) {
|
||||
renderQueueRef.current = vectorData
|
||||
return
|
||||
}
|
||||
|
||||
let dataToRender: Uint8Array | null = vectorData
|
||||
while (dataToRender) {
|
||||
isRenderingRef.current = true
|
||||
const data = dataToRender
|
||||
dataToRender = null
|
||||
|
||||
try {
|
||||
let session = sessionRef.current
|
||||
if (!session) {
|
||||
// createModule creates a persistent session loaded with the vector data.
|
||||
// It does NOT go through runWithSession, so there is no automatic free()
|
||||
// and no Rc::try_unwrap ownership conflict between renders.
|
||||
session = await (renderer as any).createModule(data) as RenderSession
|
||||
sessionRef.current = session
|
||||
} else {
|
||||
// Reset existing session with new vector data — synchronous, no ownership transfer
|
||||
session.manipulateData({ action: 'reset', data })
|
||||
}
|
||||
// renderToSvg with an existing renderSession bypasses runWithSession entirely
|
||||
// (withinOptionSession short-circuits to fn(session) when renderSession is present)
|
||||
await session.renderToSvg({ container })
|
||||
pendingVectorRef.current = null
|
||||
setStatus('ready')
|
||||
setErrorMsg('')
|
||||
} catch (e) {
|
||||
setStatus('error')
|
||||
setErrorMsg(`Render failed: ${e}`)
|
||||
} finally {
|
||||
isRenderingRef.current = false
|
||||
}
|
||||
|
||||
if (renderQueueRef.current) {
|
||||
dataToRender = renderQueueRef.current
|
||||
renderQueueRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// triggerCompile has no deps — reads view through viewRef
|
||||
const triggerCompile = useCallback(() => {
|
||||
const worker = workerRef.current
|
||||
if (!worker || !isReadyRef.current) return
|
||||
const content = viewRef.current?.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)
|
||||
}, []) // stable — no closure over view
|
||||
|
||||
// Init renderer (main thread, needs DOM). Stable dep: doRender.
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
const renderer = createTypstRenderer()
|
||||
renderer
|
||||
.init({ getModule: () => fetch(rendererWasmUrl) })
|
||||
.then(async () => {
|
||||
if (cancelled) return
|
||||
rendererRef.current = renderer
|
||||
// Flush any compile result that arrived while renderer was loading
|
||||
if (pendingVectorRef.current) {
|
||||
await doRender(pendingVectorRef.current)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (cancelled) return
|
||||
setStatus('error')
|
||||
setErrorMsg(`Renderer init failed: ${err}`)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
sessionRef.current = null
|
||||
rendererRef.current = null
|
||||
}
|
||||
}, [doRender])
|
||||
|
||||
// Init worker. Stable deps: triggerCompile, doRender.
|
||||
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
|
||||
// Don't change status here — doRender will set 'ready' once rendered
|
||||
triggerCompile()
|
||||
}
|
||||
|
||||
if (msg.type === 'compiled') {
|
||||
await doRender(msg.vectorData)
|
||||
}
|
||||
|
||||
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, doRender]) // both stable
|
||||
|
||||
// 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,
|
||||
setSmoothPdfTransition: _setSmoothPdfTransition,
|
||||
isLatexProject: _isLatexProject,
|
||||
isTypstProject: _isTypstProject,
|
||||
changedAt: _changedAt,
|
||||
typstPreviewMode: _typstPreviewMode,
|
||||
setTypstPreviewMode: _setTypstPreviewMode,
|
||||
} = localCompileContext
|
||||
|
||||
const [animateCompileDropdownArrow] = useDetachStateWatcher(
|
||||
@@ -159,6 +163,24 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
'detacher',
|
||||
'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(
|
||||
'lastCompileOptions',
|
||||
_lastCompileOptions,
|
||||
@@ -418,6 +440,13 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
'detacher'
|
||||
)
|
||||
|
||||
const setTypstPreviewMode = useDetachAction(
|
||||
'setTypstPreviewMode',
|
||||
_setTypstPreviewMode,
|
||||
'detached',
|
||||
'detacher'
|
||||
)
|
||||
|
||||
useCompileTriggers(startCompile, setChangedAt)
|
||||
useLogEvents(setShowLogs)
|
||||
|
||||
@@ -482,6 +511,10 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
smoothPdfTransition,
|
||||
setSmoothPdfTransition,
|
||||
isLatexProject,
|
||||
isTypstProject,
|
||||
changedAt,
|
||||
typstPreviewMode,
|
||||
setTypstPreviewMode,
|
||||
}),
|
||||
[
|
||||
animateCompileDropdownArrow,
|
||||
@@ -541,6 +574,10 @@ export const DetachCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
smoothPdfTransition,
|
||||
setSmoothPdfTransition,
|
||||
isLatexProject,
|
||||
isTypstProject,
|
||||
changedAt,
|
||||
typstPreviewMode,
|
||||
setTypstPreviewMode,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -133,6 +133,10 @@ export type CompileContext = {
|
||||
setDarkModePdf: (value: boolean) => void
|
||||
activeOverallTheme: ActiveOverallTheme
|
||||
isLatexProject: boolean
|
||||
isTypstProject: boolean
|
||||
changedAt: number
|
||||
typstPreviewMode: 'wasm' | 'pdf'
|
||||
setTypstPreviewMode: (value: 'wasm' | 'pdf') => void
|
||||
}
|
||||
|
||||
export const LocalCompileContext = createContext<CompileContext | undefined>(
|
||||
@@ -305,6 +309,9 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
const [isTypstProject, setIsTypstProject] = useState(false)
|
||||
const [isQuartoProject, setIsQuartoProject] = useState(false)
|
||||
const isLatexProject = !isTypstProject && !isQuartoProject
|
||||
const [typstPreviewMode, setTypstPreviewMode] = usePersistedState<
|
||||
'wasm' | 'pdf'
|
||||
>(`typst_preview_mode:${projectId}`, 'wasm', { listen: true })
|
||||
const smoothPdfTransition = isTypstProject
|
||||
? smoothTransitionTypst
|
||||
: smoothTransitionLatex
|
||||
@@ -879,6 +886,10 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
smoothPdfTransition,
|
||||
setSmoothPdfTransition,
|
||||
isLatexProject,
|
||||
isTypstProject,
|
||||
changedAt,
|
||||
typstPreviewMode,
|
||||
setTypstPreviewMode,
|
||||
}),
|
||||
[
|
||||
animateCompileDropdownArrow,
|
||||
@@ -937,6 +948,10 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
smoothPdfTransition,
|
||||
setSmoothPdfTransition,
|
||||
isLatexProject,
|
||||
isTypstProject,
|
||||
changedAt,
|
||||
typstPreviewMode,
|
||||
setTypstPreviewMode,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -520,3 +520,64 @@
|
||||
display: flex;
|
||||
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",
|
||||
"tutorials": "Tutorials",
|
||||
"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",
|
||||
"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",
|
||||
|
||||
@@ -2904,6 +2904,11 @@
|
||||
"turn_on_password_visibility": "Activer la visibilité du mot de passe",
|
||||
"tutorials": "Tutoriels",
|
||||
"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",
|
||||
"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",
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
"@customerio/cdp-analytics-node": "^0.3.9",
|
||||
"@google-cloud/bigquery": "^8.1.1",
|
||||
"@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-saml/passport-saml": "^5.1.0",
|
||||
"@overleaf/access-token-encryptor": "workspace:*",
|
||||
|
||||
@@ -750,6 +750,13 @@ const BASE_COMPILE_CONTEXT_MOCK = {
|
||||
darkModePdf: false,
|
||||
setDarkModePdf: () => {},
|
||||
activeOverallTheme: 'light',
|
||||
smoothPdfTransition: false,
|
||||
setSmoothPdfTransition: () => {},
|
||||
isLatexProject: true,
|
||||
isTypstProject: false,
|
||||
changedAt: 0,
|
||||
typstPreviewMode: 'wasm' as const,
|
||||
setTypstPreviewMode: () => {},
|
||||
} as const
|
||||
|
||||
const makeDetachCompileProvider = (mockCompileOnLoad: boolean = false) => {
|
||||
|
||||
Reference in New Issue
Block a user