7 Commits

Author SHA1 Message Date
claude 4f5dad383b fix(typst-preview): use persistent session to avoid Rc ownership panics
Build and Deploy Verso / deploy (push) Successful in 11m4s
Every call to renderToSvg({ artifactContent }) internally routes through
runWithSession, which calls session.free() after fn resolves. Because the JS
GC may still hold a reference to the first RenderSession wrapper, the next
render's Rc::try_unwrap() panics with 'attempted to take ownership of Rust
value while it was borrowed'.

Fix: use renderer.createModule(data) once to create a persistent RenderSession
that is never freed during the component's lifetime. Subsequent renders call
session.manipulateData({ action: 'reset', data }) (synchronous, no ownership
transfer) + session.renderToSvg({ container }) which routes through
withinOptionSession's renderSession fast-path — bypassing runWithSession and
its session.free() entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 15:49:50 +00:00
claude eedf4b50f6 fix(typst-preview): use RenderByContentOptions to avoid Rust aliasing
Build and Deploy Verso / deploy (push) Successful in 10m35s
Replace runWithSession + manipulateData + session.renderToSvg with the
direct RenderByContentOptions form: renderer.renderToSvg({ format: 'vector',
artifactContent, container }).

The session-based API kept hitting 'recursive use of an object detected
which would lead to unsafe aliasing in rust' because runWithSession holds
a mutable borrow of the session while renderToSvg also takes one —
regardless of whether you call renderer.renderToSvg({ renderSession }) or
session.renderToSvg(). The content-based form creates and disposes the
session internally without any caller-visible borrow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 15:22:56 +00:00
claude c6d71e58b7 fix(typst-preview): fix recursive Rust aliasing error and stale renders
Build and Deploy Verso / deploy (push) Successful in 12m44s
Two bugs fixed:

1. 'recursive use of an object' Rust error: inside runWithSession(), calling
   renderer.renderToSvg({ renderSession: session }) passes the session to the
   renderer while runWithSession already holds it — double-aliasing the same
   Rust object. Fixed by using session.renderToSvg({ container }) directly.

2. Stale preview after edits: concurrent doRender calls (compile finishes
   while previous render is still in progress) would both enter runWithSession
   simultaneously, causing the Rust error and leaving the view frozen. Fixed
   with a render guard (isRenderingRef) that queues the latest vectorData and
   flushes it once the current render completes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:52:49 +00:00
claude 06085cda21 fix(csp): allow WebAssembly instantiation via wasm-unsafe-eval
Build and Deploy Verso / deploy (push) Successful in 11m54s
WebAssembly.instantiateStreaming() requires 'wasm-unsafe-eval' in the
script-src CSP directive. Unlike 'unsafe-eval', this only permits WASM
compilation and does not allow arbitrary eval() calls.

Needed for the typst.ts WASM preview (both compiler and renderer).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:35:26 +00:00
claude 8515a899ac fix(typst-preview): fix race condition and error-masking in WASM preview
Build and Deploy Verso / deploy (push) Successful in 12m35s
Two bugs were causing a brief red error then blank screen:

1. triggerCompile closed over `view` in useCallback deps, so every time
   the CodeMirror view reference changed, useEffect terminated and
   recreated the entire worker. Fixed by reading view via a ref, making
   triggerCompile stable (empty dep array).

2. When 'compiled' arrived before the renderer WASM finished loading,
   the old code called setStatus('ready') to silently skip rendering —
   this cleared any existing error and left a blank screen. Fixed by
   buffering the vectorData in pendingVectorRef and flushing it once
   the renderer is ready.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 14:05:41 +00:00
claude 200bff4ecb feat(typst): browser-side live preview via typst.ts WASM
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>
2026-06-19 13:42:39 +00:00
claude b5cf5f9e7b docs: add AI writing assistant to alpha-4 TODO
Build and Deploy Verso / deploy (push) Successful in 59s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 12:29:15 +00:00
15 changed files with 624 additions and 4037 deletions
+9
View File
@@ -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.
+1 -1
View File
@@ -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">
<PdfViewer />
{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%);
}
}
+5
View File
@@ -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",
+5
View File
@@ -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": "Louverture de ce contenu sur Overleaf a échoué car larchive na 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",
+3
View File
@@ -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) => {
+131 -4033
View File
File diff suppressed because it is too large Load Diff