Mark Alpha 3 as released (drop the "in progress" qualifier) and correct
the bidirectional format export entry: only LaTeX ↔ Typst conversion is
available; DOCX, Markdown and HTML exports are not yet implemented.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Read-only collaborators and token-link users could publish, unpublish,
and rotate presentation share tokens. Change all three write endpoints
from ensureUserCanReadProject to ensureUserCanAdminProject so only the
project owner can perform these actions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tor Browser sets viewport width to ~980px to resist fingerprinting, so
@media (max-width: 767px) never fires on a real phone using it. Add the
secondary condition (pointer: coarse) and (max-width: 1024px) — same
dual-check already used in JS — to all mobile CSS overrides in
ide-lumiere.scss and project-list-lumiere.scss. This activates the font
scaling, toolbar height increase, auto-zoom prevention, and project page
layout fixes on Tor Browser.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Browsers zoom any focused editable element (including contenteditable)
whose font-size is below 16px. Apply font-size: max(1rem, 1em) on
.cm-content inside the mobile media query so the 16px floor is enforced
while still respecting user-set sizes larger than that.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a @media (max-width: 767px) block scoped to .ide-redesign-main
that bumps the CSS font-size tokens one step each and increases the
toolbar height, making buttons, labels, and panel headers readable
on a phone without touching the CodeMirror editor font size (which
is controlled by user settings independently).
Also reverts the unintended rail auto-collapse from the previous
commit — collapsing the sidebar was not the requested change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Editor:
- Auto-collapse the file-tree rail on mobile so the editor+PDF panels
occupy the full screen width instead of sharing it with a 15% sidebar.
The user can still open the rail from the toolbar; the collapse only
fires on first load or when switching to a mobile viewport.
Project page (Lumiere):
- Move the XS/M/L zoom buttons from the mobile filter-pill toolbar into
a row with the New Project button, so neither is stranded alone.
- The search bar gets its own full-width row above the actions row.
- Desktop layout is unchanged (search + new-project stay side-by-side;
zoom stays in the title row). `display:contents` makes the wrapper
div transparent to the desktop flex container.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tor Browser and Firefox with privacy.resistFingerprinting report a
desktop-sized viewport (~980px) even on a real Android phone. This made
window.matchMedia('(max-width: 767px)') return false, so isMobile was
always false on Tor, leaving the editor in side-by-side (horizontal)
layout instead of the expected vertical stack.
Fix: add a secondary check using `(pointer: coarse) and (max-width:
1024px)`. Touch hardware is not spoofed by fingerprinting resistance,
so this reliably catches phones and tablets regardless of the reported
viewport width. Applied to both getInitialLayout() and the live
isMobile state in main-layout.tsx.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Thumbnails: update the actual thumbnail endpoint (ConversionController.js
thumbnailFromBuild) to quality=90 and width=794. The previous fix targeted
ConversionManager.js which handles preview mode, not the thumbnail route
called by ThumbnailManager.mjs.
Mobile layout: move the isMobile guard before the stored-preference check
in getInitialLayout(). The autoSave race fix (build 274) stopped future
bad writes, but a stale 'flat' in localStorage was still being read on
every load, blocking the mobile check. Mobile now always starts in
verticalSplit regardless of any stored value.
CI: add node --check on all server-side .mjs files in the Dockerfile,
after source copy and before webpack compile, so syntax errors like the
escaped-backtick incident fail the build immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sed substitution literal-escaped the backtick and \${ in the
groupSSOReauthenticate subject line, producing invalid JS.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Email:
- Replace Overleaf green palette with Verso teal (#2a9d8f) for CTA
buttons, links, and logo text
- Rename force-overleaf-style CSS class to force-verso-style in all
email body templates and layout
- Replace all user-visible "Overleaf" strings with settings.appName
across email subjects, bodies, and sign-offs (EmailBuilder.mjs)
- Remove Overleaf CEO signature from onboarding email; substitute
"The Verso Team"
- Fix utm_source=overleaf → utm_source=verso in onboarding links
- Point git token docs URL to local siteUrl instead of docs.overleaf.com
- Fix hard-coded Overleaf green (#0F7A06) in SSO disabled email link
Mobile filter chips:
- Switch from overflow-x:auto (still leaks through ancestor flex chain)
to flex-wrap:wrap with flex:1 1 auto on each chip so all chips in a
row grow to equal width — no overflow, clean alignment
- Toolbar becomes flex-column: chips on top, zoom below-right
Misc:
- Remove import_typst_file option from new-project menu
- Add interface_language to extracted-translations.json whitelist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CLSI thumbnails: bump to 794px/q90 (matches preview quality) for
crisp display on 2x/3x phone screens
- Lumière filter chips → underline tab bar: single scrollable row with
teal active indicator, no more wrapping alignment issues; zoom buttons
separated by a vertical divider on the right
- Fix editor vertical split default on mobile: disable react-resizable-panels
autoSaveId on mobile to prevent a stale collapsed PDF pane from firing
onCollapse → changeLayout('flat') and overriding the verticalSplit
default set by getInitialLayout()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Thumbnails: increase CLSI thumbnail from 190px/q50 to 400px/q80.
At 190px/50% JPEG quality, images are noticeably blurry on 2x phone
screens (source needs 380px device pixels but source is only 190px).
Editor mobile layout: getInitialLayout() was returning sideBySide for
any stored 'split' preference (set from a desktop session), even on
mobile. sideBySide on mobile renders vertically via the isMobile check
in main-layout, but the stated default was still wrong. Now on mobile,
any stored value other than 'flat' maps to verticalSplit so the
top-bottom split is always the default; flat is preserved so a user
who explicitly chose editor-only keeps that preference.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
overflow-x: auto only clips content when every ancestor has a definite
width; that constraint wasn't met so the pills were still expanding the
page. Switch to flex-wrap: wrap so pills flow onto multiple rows and
never cause horizontal overflow. Also allow the toolbar itself to wrap
so the zoom control can drop to a new row if needed.
Remove redundant sidebar hide rule — SidebarDsNav already has d-none
d-md-flex so it was never contributing to the overflow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The project-list-wrapper flex row includes the sidebar (min-width:200px)
alongside the content area. On a 375px phone this totals ~575px, causing
the 2x horizontal overflow. Hide the sidebar on mobile — the filter pills
in the mobile toolbar already cover all its navigation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Page width overflow: stack lumiere-header as column on mobile so
lumiere-header-actions (flex-shrink:0) no longer forces the container
wider than the viewport. Search form and new-project button each take
full width on their own line.
2. ⋮ button in compact row: override dropdown-table-button-toggle inside
lumiere-compact-actions with Lumière-palette colours and tighter
padding, replacing the table-context styling.
3. Tile view action strip: add @media (hover: none) rule so
lumiere-card-actions is always opacity:1 on touch devices (phones have
no cursor, so the hover-reveal pattern is unusable).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Overflow fix: ActionsCell (up to 9 icon buttons) was assigned to the
`auto` column in the compact row, blowing out the row to ~300px.
Replace with ActionsDropdown (single ⋮ button) on mobile via
d-md-none / d-none d-md-flex, same pattern as the classic table.
Tile view: remove the isMobile force to compact (effectiveScale=0).
Add a mobile-only toolbar row (d-flex d-md-none) combining the filter
pills and a new XS/M/L zoom control:
- XS (scale=0) → compact rows
- M (scale=1) → 2 tiles per row (CSS: repeat(2, 1fr))
- L (scale≥1.35) → 1 tile per row (CSS: 1fr, class --mobile-1col)
The --lum-card-scale inline var is suppressed on mobile so CSS media
query controls thumbnail height (0.85 for 2-col, 1.3 for 1-col).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Compact row (xs): wrap format/owner/date in a lumiere-compact-meta div
that uses display:contents on desktop (preserving the 6-column grid) and
switches to a 2-row grid layout on mobile — row 1: checkbox/name/actions,
row 2: format·owner·date. Actions are always visible on touch devices.
Filter access: add a horizontal scrollable filter-pill bar (d-md-none)
to the Lumière header so users can switch between All/Yours/Shared/Archived/
Trashed on phone without needing the desktop sidebar.
Also: hide zoom control on mobile (d-md-none), auto-force compact view on
mobile via useIsMobile hook, fix search form min-width overflow on xs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Convert: backend now returns parentFolderId+isNew; frontend calls
dispatchCreateDoc directly so the new file appears without a page refresh
- Set as main: infer compiler from file extension and POST both rootDocId
and compiler together; updateProject propagates the change to the
editor settings dropdown and project list immediately
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Set as main document (context menu):
- canSetRootDocId used selectedEntityIds so .typ/.qmd files couldn't be
set as main via right-click on an unselected file.
Now computed locally from contextMenuEntityId (same pattern as convert)
using isValidTeXFile which already covers .typ, .qmd, .tex etc.
Compiler filter (editor settings):
- docs?.find(id) could return undefined due to ID format mismatch,
causing all engines to show as available for non-LaTeX projects.
Added findInTree fallback so the root doc name is always resolved.
ZIP import compiler:
- Projects created from ZIP always got defaultLatexCompiler ('quarto')
regardless of content.
- findRootDocFileFromDirectory now also searches for .typ and .qmd root
files after finding no .tex file.
- ProjectUploadManager now infers the compiler from the root doc
extension and sets it on the project after import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
canRename required selectedEntityIds.size === 1, so right-clicking an
unselected file hid the convert items even though contextMenuEntityId
was correctly set. Replace canRename with !fileTreeReadOnly for the
convert-specific gate, which is the actual write-access check needed.
Also add showExportDocumentSuccess so the user sees the warning toast
on successful conversion instead of silent nothing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs:
1. Converting when output already exists threw DuplicateNameError (400).
Now overwrites existing doc via setDocument instead of failing.
2. Right-clicking an unselected file left contextMenuEntityId null,
so the first click on Convert silently did nothing. Added
contextMenuEntityId to FileTreeMainContext, set it on right-click
and on the … button click; FileTreeItemMenuItems now uses it for
the convert hooks rather than relying on selectedEntityIds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove RevealJS thumbnails (redundant with Features section) and Upload
reliability (still unresolved). Add Known Issues section documenting
the large file upload timeout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
postJSON without options sends Content-Type: application/json with no
body; body-parser's BodyParserWrapper intercepts the resulting
SyntaxError and returns 400 before the route handler runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add "Convert to Typst (.typ)" in the file tree context menu for .tex
docs, and "Convert to LaTeX (.tex)" for .typ docs. Clicking runs pandoc
on the file content and creates the converted file in the same folder.
- New backend endpoint POST /project/:id/doc/:id/convert/:type that reads
the doc from document-updater, runs pandoc directly, and creates the
result via ProjectEntityUpdateHandler (file tree updates via socket).
- Rewrite the export success toast for typst and latex conversions: no
more link to /contact, replaced with a plain warning that errors are
expected (pandoc does not support all constructs).
- Add i18n keys: convert_to_typst, convert_to_latex,
typst_export_feedback_message, latex_export_feedback_message (EN + FR)
and all four to extracted-translations.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add top/bottom split view and bidirectional format export (LaTeX↔Typst,
LaTeX→DOCX/Markdown/HTML) to the Alpha 3 feature list. Fix the security
section which incorrectly referred to Overleaf as a LaTeX/Typst editor —
Overleaf only supports LaTeX.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
top_bottom_split_view, export_as_typst, and export_as_latex were absent
from the whitelist, so translations-loader.js stripped them from the
locale bundles at build time — causing raw keys to show in the UI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- clsi-nginx: allow hyphens in project-id regex — conversion IDs are UUIDs
which nginx was rejecting, causing 500 on file download after conversion
- CLSI ConversionController/Manager: add 'latex' export type (typst→latex via pandoc)
- Web: add 'latex' to SUPPORTED_CONVERSION_TYPES
- Frontend: add Export as LaTeX button (visible only for typst projects)
- Fix visibility logic: export-as-latex shows for typst, export-as-typst shows for latex
- Add export_as_latex translation key (en + fr)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ConversionController.js: add typst to CONVERSION_CONFIGS (missing entry caused 400→500 chain)
- export-project-with-conversion-button: hide button for non-LaTeX projects (typst/quarto) via compiler check
- project-list-lumiere.tsx + scss: revert lumiere-card-actions back inside .lumiere-card (put them back like they were)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Typst → LaTeX import:
- CLSI ConversionManager: add 'typst' to CONVERSION_CONFIGS
(pandoc input.typ --from typst --to latex --standalone → zip archive)
- Web controller: allow 'typst' as a valid importDocument conversion type
- Frontend modal: add .typ file config to ImportDocumentModal
- New project button modal: add 'import_typst' variant + switch case
- New project button: show "Import Typst file" when enablePandocConversions
is true (no split test gate — Verso has no SaaS split test infra)
- Locales: add choose_typst_file and import_typst_file keys (18 locales)
Export button fix:
- Remove featureFlag="export-typst" from ExportProjectWithConversionButton
so the button shows whenever enablePandocConversions is true, without
needing an unconfigured split test to return 'enabled'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pandoc was not installed in the base image, so the export buttons
(docx, markdown, html, typst) were hidden because ENABLE_PANDOC_CONVERSIONS
defaulted to false.
- Dockerfile-base: add pandoc via apt (Ubuntu Noble ships 3.1.3, which
supports --to typst added in pandoc 3.0)
- env.sh: set ENABLE_PANDOC_CONVERSIONS=true so both the web and CLSI
services expose and serve the export endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a new "Export as Typst" option in the project title dropdown and
File menu, mirroring the existing docx/markdown/html export pipeline.
Changes:
- CLSI ConversionManager: add 'typst' to LATEX_EXPORT_CONFIGS
(compressOutput: false, pandoc --from latex --to typst)
- Web controller: register 'typst' → 'typ' in SUPPORTED_CONVERSION_TYPES
- Frontend: extend conversionType union and add ExportProjectWithConversionButton
- File menu: add 'export-as-typst' to the download group command structure
- Locales: add export_as_typst key to all 18 locale files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The DropdownHeader in the View > Layout menu was using a raw English
string instead of t('editor_settings'). Added the key to all 18 locale
files with appropriate translations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Popper lazy-initializes on first open, causing it to place the menu at
[0,0] before it has computed the toggle's position. renderOnMount forces
Popper to initialize while the component is first mounted, so the
position is ready before the user's first click.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
.lumiere-card has both backdrop-filter and a hover transform, both of
which create a new containing block for position:fixed descendants,
trapping Popper dropdowns inside the card's overflow:hidden bounds.
Fix: move .lumiere-card-actions outside .lumiere-card (sibling inside
.lumiere-card-wrapper, which has position:relative but no filter or
transform). The actions strip is now absolutely positioned at the card
bottom with its own solid background and bottom border-radius.
No backdrop-filter on the strip to avoid re-introducing the same trap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ProjectCard in the Lumiere tile view always showed CompileAndDownloadProjectPDFButtonTooltip,
ignoring the project compiler. Quarto presentation projects need the
DownloadPresentationButtonTooltip (HTML/PDF dropdown) instead, matching
the logic already in actions-cell.tsx and actions-dropdown.tsx.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
layout-context: getInitialLayout() was returning verticalSplit for
any stored 'vertical' preference, including on desktop. Now checks
isMobile first so stored mobile preference doesn't bleed into PC.
compile-and-download-pdf: when compile succeeds but output.pdf is
absent from outputFiles, the code crashed silently at outputFile.build
leaving the user with no feedback. Now shows the error modal instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Footer: use translate('built_on') key instead of hardcoded 'Built on';
update fr.json 'built_on' → 'Basé sur Overleaf'
- Mobile project list: move search bar + button outside the bordered
TableContainer so its width is viewport-constrained, not affected by
the table's fixed layout
- Mobile table rows: use width:100% (not auto) on cells so they fill the
full tr width regardless of the higher-specificity column percentage
rules; add explicit width:100% on tr to anchor flex-column sizing;
keep width:auto on absolutely-positioned actions cell
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
quartoFlavor is set only after a project is compiled with the current
build. Existing Quarto projects that haven't been recompiled have
quartoFlavor=undefined, so the old check (quartoFlavor === 'revealjs')
fell through to the regular PDF compile button with no dropdown.
Drop the quartoFlavor guard — compiler === 'quarto' is sufficient since
all Quarto projects in Verso are RevealJS presentations. Changes applied
to ActionsCell (desktop icon), DownloadPresentationButtonTooltip guard,
and ActionsDropdown (mobile three-dot menu).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Language picker:
- Add fallback href in Pug so language links navigate even if JS fails
- Anchor dropdown to right edge (right:0) so it stays on-screen when
the picker is near the right side of the footer on mobile
Editor layout:
- Read stored pdfLayout from localStorage on init so the last-used
layout is remembered across sessions
- Default to verticalSplit (top/bottom) on mobile when no preference
is stored, so the editor opens in a sensible layout on phones
Translations:
- Add top_bottom_split_view key to all 16 locales that were missing it
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The manual JS click-handler approach (tried with stopPropagation,
containment check, and mousedown variants) never worked reliably on
login/password/settings pages. The browser's native <details>/<summary>
toggle behaviour requires no JavaScript and is immune to Bootstrap JS or
React event delegation interference.
The inline script now only builds the return_to hrefs and handles
outside-click-to-close (setting details.open=false). The CSS gains a
.language-picker-details rule that sets position:relative so the
absolutely-positioned dropdown-menu is positioned correctly, and
details[open] .dropdown-menu { display: block } to show the menu.
The #language-picker-toggle id remains on the <summary> so the existing
CSS (cursor, text-decoration, material-symbols alignment) continues to
apply. The React LanguagePicker is unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Search bar overflow: min-width:0 on form prevents input min-content
from overflowing flex container on narrow screens
- Remove duplicate New Project button from header row (tableTopArea
already provides it for mobile)
- Simplify tableTopArea: single button+search layout regardless of
isLibraryEnabled flag
- 2-line project rows on mobile: merge dash-cell-date-owner +
dash-cell-tag into a single dash-cell-meta so date/owner/tags flow
inline on line 2 below the project name
- Footer mobile: hide language-picker-text on mobile (icon only) to
prevent 3rd line in 2-row footer
- Language picker: use mousedown instead of click for outside-close
handler — click bubbling is unreliable on iOS for non-interactive
elements; mousedown fires for all taps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both ul.site-footer-items rows are now justify-content: center on small
screens, so the copyright/language row and the licence/source row are
symmetrically aligned instead of left-justified.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Mobile vertical layout: add key= based on direction so react-resizable-panels
remounts cleanly when switching between horizontal and vertical; also use
isVertical (not just pdfLayout) for the autoSaveId to avoid restoring a
mismatched layout from localStorage
- Language picker: replace stopPropagation pattern with a containment check on
the document click handler — more robust on React pages where Bootstrap JS or
React's event delegation can interfere with stopPropagation
- Presentation download dropdown: use popperConfig strategy:'fixed' so the menu
escapes overflow:hidden table cells; remove forced drop='up' and let Popper
choose; defer URL.revokeObjectURL by 10 s to give the browser time to start
the download before the blob URL is released
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On mobile (< 768 px) the existing side-by-side layout automatically switches
to a vertical stack (editor on top, PDF/presentation on bottom) without
changing the stored layout preference.
A new "Top / bottom split" option is added to the layout menu so desktop
users can choose the same vertical split explicitly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Using the generic 'download' icon duplicated the ZIP button icon,
giving two identical icons side by side. Switch to picture_as_pdf to
match the previous compile-and-download button's appearance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Quarto Slides (RevealJS) projects compile to output.html, not output.pdf,
so the existing "Download PDF" button was meaningless for them. Replace it
with a two-option dropdown matching the editor's PdfHybridDownloadButton:
- Desktop (ActionsCell): icon button opens a dropup with
"Download standalone HTML" and "Download PDF slides"
- Mobile (ActionsDropdown): two separate dropdown items with the same
choices and per-format spinner while the export is in progress
Both use the same /project/:id/presentation-export/:format endpoint and
show a loading modal (with error reporting) during the server-side render,
exactly as the editor toolbar does.
Non-RevealJS projects continue to show the compile-and-download-PDF button
unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The native <select> looked like a form control ("cheap"). Replace it with
the same HTML+CSS pattern as the React LanguagePicker: btn-inline-link
toggle with a translate icon, Bootstrap dropdown-menu for the list, active
item highlighted in green.
A tiny self-contained inline script handles the toggle since Bootstrap JS
is not loaded on React-layout pages. It builds the return_to URL from
window.location.pathname at click time (same as the old select approach).
Added top: auto / bottom: 100% to .language-picker .dropdown-menu so the
list always opens upward regardless of whether Popper.js is present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Footer (both React and Pug):
- Below md: switch to flex-wrap so items wrap naturally instead of
overflowing; reset line-height from the fixed 49px to normal; add
row-gap so wrapped rows aren't crammed together
- Add footer-sep class to pipe separator <li>s so they can be hidden
on small screens (wrapping mid-separator looks wrong)
- Change col-lg-3 text-end → text-lg-end so the right column (licence,
source code) aligns left when it stacks full-width below lg
Project list:
- Show NewProjectButton on mobile in the header row (the sidebar that
holds the button is already hidden below md via d-none d-md-flex,
leaving users with no way to create projects on their phone)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The content panel had border-top-left-radius: var(--border-radius-large)
at md+ breakpoint, left over from the old Overleaf theme. Everything else
is square now so this corner stood out. Removing it makes the edge flush.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
favicon-compiled.svg, favicon-compiling.svg, favicon-error.svg all used
the Overleaf O logo as the base. Replace with the Verso icon mark (dark
background, four colored circles, white V polygon, clipped to a circle)
while keeping the existing status badge icons (✓ / ↻ / ✗) unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace Overleaf favicons with Verso icon mark across all sizes (16x16,
32x32, 180x180 apple-touch, 192x192/512x512 android-chrome, ICO). Add
OG social preview image (1200x630) for Discord/Twitter link cards.
- New favicon.svg: Verso icon mark with four overlapping circles and V
- mask-favicon.svg: monochrome V polygon (was Overleaf chevron)
- og-image.png: 1200x630 social card with icon mark and "Verso" wordmark
- web.sitemanifest.json: rename "Overleaf" → "Verso", theme_color updated
- _metadata.pug: add og:url tag, fix og:type missing content= attribute,
point CE default OG/twitter images to og-image.png instead of apple-touch-icon.png
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old dropdown relied on data-bs-toggle and AngularJS directives,
neither of which are loaded on React-layout pages (layout-react.pug
intentionally excludes Bootstrap JS). The toggle button was inert on
pages like /user/settings.
Replace with a plain <select> that navigates via window.location.href
onchange — works without any framework on all page types.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bootstrap vanilla JS and react-bootstrap were both handling the dropdown,
causing the toggle to be unresponsive. react-bootstrap manages its own
state and does not need this attribute.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rewrites LanguagePicker to use availableLanguages from ol-footer meta
instead of subdomainLang (which is always empty in single-domain setup)
- Passes availableLanguages through layout-react.pug → ol-footer meta so
React footer picks it up
- Adds InterfaceLanguageSetting component to the editor settings modal
("Spelling and language" tab) for use when no footer is present
- Adds interface_language key to all five locale files (en/fr/de/es/it)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users can now select their UI language directly without relying on
subdomain routing (fr.verso.alocoq.fr etc.).
Resolution order: (1) verso-lang cookie, (2) subdomain host header,
(3) OVERLEAF_SITE_LANGUAGE default — fully backward compatible.
Changes:
- Translations.mjs: read verso-lang cookie in middleware; include all
bundled locale files in availableLanguageCodes regardless of subdomain
config so every loaded locale appears in the picker
- User.mjs: add languageCode field to persist preference per user
- UserController.mjs: setLanguage handler — sets cookie (1 year) and
writes languageCode to DB when called by a logged-in user
- AuthenticationController.mjs: on login, sync DB languageCode to cookie
so preference follows the user to any new browser/device after login
- ExpressLocals.mjs: expose availableLanguages to all Pug templates
- router.mjs: GET /set-language?lng=<code> (anonymous + logged-in),
POST /user/language (logged-in, REST-style)
- language-picker.pug: replace subdomain href links with /set-language
redirect links; iterate availableLanguages instead of subdomainLang
- thin-footer.pug: show picker whenever availableLanguages.length > 1,
not only when multiple subdomains are configured
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use GitHub's > [!CAUTION] admonition (renders with red background) for the
trusted-environment security warning, matching the style used by Collabst.
Remove invented claim that Overleaf is working on Typst support — that was
a hallucination. Replace with a plain "Verso is built on Overleaf's infra"
statement. Add RevealJS as a separate ecosystem project worth supporting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove Contributing section (not accepting PRs/issues)
- Add Security model section: Verso is for trusted environments only;
point untrusted-user use cases at Overleaf non-Community offerings
- Mention Collabst as a promising open-source Typst-only alternative
in the Verso vs Typst.app comparison
- Add Supporting the ecosystem section redirecting to Overleaf (Typst +
RevealJS work) and the Typst project instead of Verso donations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the "how to write a .qmd / .typ / .tex" tutorial content with a
clear positioning narrative: what Verso is vs Overleaf (same engine + more
languages), vs Quarto CLI (browser-based, collaborative, multi-language),
and vs Typst.app (self-hosted, AGPL, OT-backed, three languages).
Add a Releases section covering Alpha 1 (core multi-compiler foundation),
Alpha 2 (Typst grammar overhaul, format sub-types, Python for collaborators),
and Alpha 3 in-progress (Lumière, i18n, visual editors, upload fix).
Keep Quick start, Architecture, Env vars, and License sections intact.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uploads from slow connections consistently fail with 502 after ~60-120s
because an upstream proxy (Traefik or cloud load-balancer) has a
"first response byte" deadline that fires before the request body arrives.
Fix: add startStreamingResponse middleware (after auth, before multer)
that immediately writes HTTP 200 + Transfer-Encoding: chunked + '\n'.
With proxy_request_buffering off in Nginx, this reaches the proxy at T≈0,
so no timeout triggers. The upload body continues streaming; multer writes
to disk; the actual JSON result arrives as the final chunk. Periodic
heartbeat '\n' writes every 30s keep response-idle timeouts at bay too.
Client-side: override Uppy's getResponseData/validateStatus to trim
leading whitespace before JSON.parse so the extra '\n' bytes are ignored.
Server-side: sendUploadResponse() helper handles both streaming mode
(res.headersSent → res.end(json)) and normal mode (res.status(N).json()).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move min-height:100vh from body to html:has(body[data-lumiere]) so the
gradient fills the viewport on short pages without inflating document
height or pushing the footer below the fold
- Remove min-height:60vh from .error-container (was causing scrollbar on
404 when combined with thin footer)
- Replace Bootstrap 3 navbar selectors (.navbar-nav > li > a) with CSS
custom property overrides (--navbar-link-color, --navbar-link-hover-*,
--navbar-bg, etc.) consumed by navbar.scss — fixes header button colours
- Remove position:relative from .navbar-default override; base CSS already
has position:absolute which provides the stacking context for ::before
- Drop proxy_request_buffering off from upload location: buffered mode +
global client_body_timeout 15m (nginx.conf.template) is more compatible
with multer's multipart stream handling on slow connections
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Theming: replace per-controller isLumiere lookups with a single
ExpressLocals middleware that sets res.locals.isLumiere for every
web request. Uses getOverallTheme() (now exported from
UserSettingsHelper) so the date-based default is handled correctly.
This covers 404, settings, setPassword, activate, and all future
server-rendered pages automatically.
Upload timeout: add client_body_timeout 15m to nginx.conf.template
at the http level (was defaulting to 60s globally). This is more
reliable than the location-specific override from build 229.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nginx: add dedicated upload location with client_body_timeout 15m,
client_max_body_size 550m, and proxy_request_buffering off. Default
client_body_timeout of 60s was the actual culprit cutting slow uploads.
Node.js requestTimeout (build 228) remains as a backstop.
Lumière: pass isLumiere from UserPagesController (settings),
PasswordResetController (set-password), and UserActivateController
(first-time activation). auth.scss adds card styling for auth pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Frontend fetch gets AbortSignal.timeout(15 min) so hung connections
fail cleanly. Server requestTimeout raised from Node default (5 min)
to match, preventing large-file uploads from being cut off server-side.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- XS compact row: format column 70px→96px so "QUARTO SLIDES" stays on one line;
trim owner/date cols slightly to compensate
- Welcome page (0 projects): Lumière branch now renders before the 0-projects
check; ProjectListLumiere renders WelcomePageContent when totalProjectsCount=0
so new users get the full onboarding experience in the Lumière shell
- 404 page: notFound() now detects the user's overallTheme and passes isLumiere
to the template; layout-base.pug sets data-lumiere on the body; error-pages.scss
and project-list-lumiere.scss add [data-lumiere='true'] rules for the body
background gradient, navbar white+stripe, and styled error box
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
thumbnailFromBuild() now tries output.pdf → output-slides.pdf → decktape
on output.html (slide 1 only). The web service's ThumbnailManager already
calls this endpoint fire-and-forget on every successful compile, so RevealJS
project cards will show the first slide thumbnail automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents "Quarto Slides" from wrapping to two lines in XS view.
Widens search input from 300px to 360px so French placeholder text fits.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JSDoc blocks in typst-decorations.ts and quarto-decorations.ts contained
*/ sequences inside them, which Babel's parser treats as terminating the
block comment prematurely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add typst-decorations.ts ViewPlugin that runs alongside the existing
LaTeX visual decorations. For Typst files in visual mode it:
- Hides *…* markers and applies font-weight:700 to the StrongBody
- Hides _…_ markers and applies font-style:italic to the EmphBody
- Hides = prefix marks and applies heading CSS (h1–h6 font sizes)
- Hides backtick delimiters and applies monospace to RawInlineContent
Markers reappear when the cursor enters the node (selectionSet trigger).
Register CSS rules for .ol-cm-typst-{strong,emph,heading,h1-h6,raw-inline}
in visual-theme.ts. Wire the plugin into visual.ts after markDecorations.
The visual editor toggle was already available for .typ files since 'typ'
is in validRootDocExtensions — no gating change needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ide-lumiere.scss: declare --file-tree-bg/--outline-bg-color/--outline-container-color-bg:
transparent at [data-lumiere] .ide-redesign-main scope to beat file-tree.scss +
outline.scss which re-declare the same vars at .ide-redesign-main (closer ancestor)
- linked-file-highlight and disconnected-overlay get explicit fallback colors so they
remain usable when --file-tree-bg is transparent
- custom-toggler: replace left:-5px with left:50%/transform:translateX(-50%) for
reliable centering of the 16px pill over the 4px resize handle
- project-list-lumiere.scss: compact rows get teal-tinted bg + teal border (Lumière feel)
- project-format-badge-* overrides inside .project-list-lumiere so the XS compact view
shows the soft Lumière palette (matching .lumiere-format-badge--* on cards)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the CSS-only compact card hack with a dedicated
ProjectCardCompact component that reuses FormatCell, OwnerCell,
LastUpdatedCell (with tooltip + updated-by), ActionsCell (full set),
and InlineTags — identical data to the classic table view. CSS Grid
aligns columns across rows: checkbox | name+tags | format | owner |
date | actions. Actions fade in on row hover.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resize handle toggle: 14px wide (centered over 4px handle) so the
arrow icon is clearly visible. File tree and outline backgrounds set
to transparent so the panel-group noise+gradient shows through.
FR: file_tree→"Fichiers", file_outline→"Plan" (+ hide variants).
IT: file_tree→"File", IT/ES hide keys shortened to match.
DE unchanged (Dateibaum/Gliederung already concise).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Search bar: set min-width: 300px so the placeholder text is fully
visible. XS zoom level (value 0): adds lumiere-card-grid--compact class
which switches cards to horizontal rows, hides the thumbnail, and shows
only name / badge / owner / date / actions in a compact strip. Stored
0.75 from old S already fell back to 1 (S); new 0 is cleanly additive.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Old S (0.75×) removed — too small. Old M→S (1×), old L→M (1.35×),
new L added at 1.75×. Stored 0.75 in localStorage falls back to 1×.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace Overleaf icons with Verso branding on setPassword.pug
(square icon) and primaryEmailCheck.pug (wordmark). Replace hardcoded
French aria-label "Taille des cartes" on zoom control with t('card_size')
and add card_size key to all 5 locale files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use verso-square.svg (small icon) instead of the wordmark on the
password reset page. Align card thumbnail gradients and format badge
colours to match the classic project list badge colours (LaTeX green
#098842, Typst teal #239dad, Quarto blue #447099, Quarto Slides
pink-red #e4637c). Split quarto-slides badge from quarto badge.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace Overleaf icon with Verso wordmark on passwordReset.pug.
Fix ES password_reset_email_sent (meaning error: said the password
was sent, not a link). Translate <0>Log in with SSO</0> link text
in FR/DE/IT/ES (was left in English in all languages).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Card owner: "You" now goes through t('you') so it renders as
"Vous"/"Tu"/"Du" etc. instead of always "You"
- Relative dates (fromNow): moment.locale() is now set from the app
language so "2 days ago" becomes "il y a 2 jours" etc.
- Footer: "Built on", "Source code", "AGPL licence" are now translated
via t() with keys added to all locale files and extracted-translations
- New keys: built_on, source_code, agpl_licence (FR/DE/IT/ES translations
added manually)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All four locales are now at 100% key coverage (was FR 38%, DE 50%,
IT 12%, ES 21%). Translated ~7,700 missing keys using deep-translator
with placeholder preservation (__varName__, <N>…</N> React-Trans tags).
Manual corrections can be made on top of this baseline as needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Header: revert column layout; title+zoom stay on left, search+button
on right — all on one row (flex-wrap handles narrow viewports)
- Selected cards: switch from teal tint (invisible on teal bg) to solid
white + blue ring, clearly distinguishable from the page background
- Editor toolbar: replace flat teal wash with an exponential-like decay
(20%→9%→3%→0% teal overlay) so only the very top has colour
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
S/M/L buttons now appear inline with the page title rather than in the
action bar. Bootstrap tooltips get pointer-events:none so they can no
longer steal :hover from the card beneath them, preventing the lift
animation from flickering when hovering over tag dots.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- extracted-translations.json: add n_projects_selected, n_projects_selected_plural,
deselect_all — the translations-loader.js only bundles keys present in this file,
which is why the FR translations were silently stripped from the webpack chunk
- Revert lang.default workaround in i18n.ts (not needed)
- SCSS: remove border-top above .ds-nav-verso-logo in sidebar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- i18n: unwrap webpack module object on dynamic JSON import (lang.default ?? lang)
so French bundle keys are correctly registered in the i18next store
- Login logo: use flex centering on wrapper instead of display:block + margin:auto
- Footer (project list + login): align-items:center on .row for vertical centering
- Tile zoom: S/M/L control in header with CSS custom property (--lum-card-scale)
that scales grid column width and card thumbnail height; persisted in localStorage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Footer: remove font-size/letter-spacing overrides so the browser
default applies; monospace right col keeps its uppercase + tracking
but no longer shrinks the font
- Card tags: move coloured dots into the .lumiere-card-meta flex row
instead of a separate div — zero added height, all cards uniform
- Card tags tooltip: replace native title attr with OLTooltip (React
Bootstrap tooltip) for a consistent Verso look on hover
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Footer: pale teal (#edf7f4) with noise grain instead of dark navy;
serif author credit darker (#1a2e3b), right col monospace muted;
teal→blue 2px stripe preserved; same treatment on login page
- Card tags: render as 8px coloured dots only (title attr for tooltip)
— no text means no wrapping, consistent card heights across the grid
- i18n fr.json: add deselect_all → "Tout désélectionner"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Footer: dark navy (#0d1b24) with noise texture, teal→blue gradient
top stripe; serif author credit on the left, monospace/uppercase
meta links on the right; same design on login page
- Thumbnail: larger inset (14px top, 12px sides) so more background
gradient shows around the preview; subtle drop shadow added
- Card tags: projects now show their label chips on the card (colored
dot + name, read-only, max 3, no close button inside the link)
- Selection bar: btn-secondary recoloured to Lumière palette; dropdown
menus rounded with teal accent on hover
- i18n fr.json: add n_projects_selected, n_projects_selected_plural,
toolbar_selected_projects (×4 variants)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verso forces showThinFooter=true for non-SaaS instances, rendering
footer.site-footer everywhere — never .fat-footer. All previous footer
theme rules silently matched nothing. Fix both project and login page
footer selectors. Also increase login logo max-width to 520px, and
remove bottom inset/radius from thumbnail (folder-behind-card effect).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Thumbnail: explicit top/left/right/bottom + width/height on the img so
object-fit has a well-defined box in all browsers (inset shorthand alone
was underspecified for some).
Hover effect: the card already rises translateY(-3px) on hover. Add a
matching translateY(+3px) counter-transform on the thumbnail image so
its net viewport motion is zero — the document preview appears to float
in place while the gradient tile and card body lift up around it.
Login logo: raise max-width from 300px to 400px now that the competing
inline style has been removed from the pug template.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Increase pdftocairo output from 190px/q50 to 380px/q82 — 2× resolution
for crisp rendering on retina displays, higher quality to eliminate
visible compression artefacts.
Inset the thumbnail image 6px from the tile edges (inset: 6px) with a
4px border-radius so the card's colour gradient is visible as a frame
around the document preview.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous implementation delegated to ConversionManager which uses
the Docker-based CommandRunner and is gated behind enablePdfConversions
(ENABLE_PDF_CONVERSIONS env var). Neither is configured in the Verso
deployment, so every thumbnail request 404'd before doing any work.
poppler-utils (which provides pdftocairo) is already installed directly
in the CLSI base image via install_deps.sh. Rewrite thumbnailFromBuild
to call pdftocairo via execFile instead — no feature flag, no Docker
image, no ConversionManager indirection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After a successful compile, web service calls a new CLSI endpoint
(GET /project/:id/user/:uid/build/:bid/thumbnail) which runs pdftocairo
page-1 to a 190px-wide JPEG using the existing thumbnail preset. The
JPEG is stored in Redis (90-day TTL, overwritten on next compile) by
the new ThumbnailManager.
GET /project/:Project_id/thumbnail serves the cached JPEG to authenticated
users, returning 404 when no thumbnail exists. Project cards in the
Lumière grid show the image overlaying the coloured gradient tile; if
the image 404s (project never compiled or cache expired) the onerror
handler hides it and the gradient + initial letter shows through.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Trim SVG viewBox from 760 to 590 (content ends ~x=570; the blank
right whitespace was making the wordmark look left-biased). Remove the
scale(1.2) transform from the sidebar logo — the negative-margin
container already fills the sidebar width. Change login logo max-width
to be CSS-controlled only (removed inline 480px override).
Footer: switch to `background` shorthand !important so the dark-theme
`var(--footer-background)` shorthand can't compete; deepen the teal to
#c8e4de so the Lumière colour is clearly visible. Add a
`body:has(.login-page) .fat-footer` rule so the login-page footer gets
the same treatment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restructures ProjectCard so the card is a <div> container instead of <a>
(buttons cannot be nested inside anchor elements). A .lumiere-card-link
anchor wraps the thumb+body area; a .lumiere-card-actions strip sits below
it and fades in on hover.
Buttons added (reusing the same tooltip components as the classic table):
- Copy project (opens CloneProjectModal)
- Download project zip
- Compile & download PDF
- Archive project (skipped when already archived)
- Trash project (skipped when already trashed)
Action icons are coloured $lum-text-muted at rest and shift to $lum-teal
on hover, matching the Lumière palette.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Login: centre logo (display:block + margin:auto, max-width 300px) and
increase h1 from 1.4rem to 1.75rem
- Sidebar logo: switch from width:120%/margin-left:-10% to transform:scale(1.2)
so the image scales symmetrically from centre and isn't cut on the left
- Footer: use !important on background-color/color to beat the dark-theme
selector's higher specificity (:root [data-theme] = 0,4,0 vs our 0,3,0)
- Notifications: replace near-transparent rgba backgrounds with solid
opaque colours so the teal page gradient can't bleed through; make the
CTA button neutral slate-grey (not teal) with border-radius:8px
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Code/Visual editor toggle now uses a teal sliding indicator (::before
pseudo-element) that glides between tabs via translateX instead of the
plain background-color crossfade. Container and labels get border-radius:
10px/7px to match the rest of the Lumière toolbar's rounded-square style.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Logo (all themes): scale the Verso wordmark to 120% width, centered and
clipped to the sidebar column — the word mark visually fills the full
sidebar width. Uses overflow:hidden + width:120% !important + margin-left:-10%
to override the existing inline width style.
- Compile button (Lumière): replace the all-corners border-radius:7px on the
split button group with corner-specific rules — .compile-button gets 7px on
the left side only, .compile-dropdown-toggle gets 7px on the right side only,
so the shared inner edge stays flat as expected for a joined split button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Warning notification: switch from teal to amber (#b45309) so it reads as
a genuine warning and doesn't blend into the teal UI chrome
- Notification CTA button (.btn-secondary): style with teal tint so the
'Send confirmation code' button matches the Lumière theme
- Footer: override dark footer on .project-list-lumiere with a light teal
background (#edf7f5), dark text, teal section headings — selector has same
specificity as the default-theme dark rule but appears later in the cascade
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each project card now has a checkbox (top-left corner, semi-transparent by
default, fully opaque on hover). When any card is selected a selection bar
slides in above the grid showing: select-all checkbox, count, the existing
bulk-action toolbar (archive, trash, tags, delete), and a deselect-all button.
:has(input:checked) keeps all checkboxes visible once a selection is active.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Settings modal: teal accent stripe on header, teal gradient nav background,
teal active-tab highlight, teal section titles, teal focus rings on form
controls — scoped via :has(.ide-settings-modal-body) so other modals are
unaffected
- Login page: grainy teal gradient background, white rounded-square card with
teal/blue accent stripe, teal labels, focus rings, primary button — always
applied since users haven't set a theme yet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Override Bootstrap orange/yellow warning and generic blue info colors
with the Lumière teal palette. Warning banners now use a soft teal
tint instead of orange; info banners use the Lumière blue. Both types
get 10px border-radius and a subtle shadow to match the card style.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove font-size: 0.8rem from Admin navbar button (was shrinking text)
- Add border-radius: 7px to .toolbar-pdf .btn so the Recompile, Logs and
Download buttons in the PDF panel get the Lumière rounded-square shape
- Add border-radius to compile-button-group .btn to cover the dropdown
arrow toggle next to the Recompile button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- logged-in-items: pass showThemeToggle to AccountMenuItems so the theme
switcher is accessible from the top-right navbar (was lost when the
sidebar account icons were removed); AccountMenuItems already gates on
hasOverallThemes so it's a no-op on non-themed pages
- project-list-lumiere: restyle Account + Admin navbar buttons — rounded
square (8px) instead of pill, teal resting tint on Account, subtle
teal border on hover; matches Lumière design language
- ide-lumiere: extend rounded-square styling to all toolbar action buttons
(Share, Present, History, Layout, File/Edit/Help menu buttons) via
.ide-redesign-toolbar-actions and .ide-redesign-toolbar-menu selectors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces "Overleaf subscription"/"Overleaf Commons"/"Overleaf premium features"
with Verso equivalents in the institution subscription and commons upgrade
notification strings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- project-list-ds-nav.scss: remove display:none for .nav-item-account on
desktop — it was hidden because the sidebar handled it, but now the sidebar
no longer has the account icon so this made it invisible everywhere
- logged-in-items / nav-dropdown-menu: show User icon alongside 'Account'
text in navbar dropdown so it's recognisable as an account button
- Lumière: remove border-top from .ds-nav-verso-logo (was doubling up with
.ds-nav-sidebar-lower border)
- Logo hover: drop scale transform in both themes, use filter:brightness only
- Gradient: drop background-attachment:fixed (unreliable in scroll containers);
switch to circle gradients at 0.60/0.45 opacity; base colour #e8f5f2
- Editor ide-lumiere: rounded square (7px) on .ol-cm-toolbar-button with teal
hover/active states to match the Lumière design language
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- en.json: replace 'Overleaf' with 'Verso' in 6 user-visible strings
(email_already_registered, add_manager_user_not_found, compile_timeout,
download_metadata, to_confirm_email address, welcome_opening_workspace)
- groups-and-enterprise-banner: use dynamic appName instead of hard-coded
'Overleaf'
- SidebarLowerSection: add showAccountIcons prop (default true); set false
in project dashboard sidebar — account menu is already in the top-right
navbar, so the bottom-left duplicate is removed for all themes
- ds-nav-verso-logo: replace opacity-fade hover with scale+brightness
transform so logo is fully visible at rest
- Lumière: scope new-project-dropdown sidebar padding to avoid misaligning
the button when it appears next to the search bar in the header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove overflow:hidden from toolbar — it was clipping dropdown menus
- Increase SVG noise opacity 0.06→0.12 and gradient orb opacity for more
visible texture on the dashboard background
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Lumière container now carries project-ds-nav-page and
project-list-wrapper so the sidebar picks up all its existing styles.
The grey-rectangle button issue and broken sidebar layout were caused
by those expected parent classes being absent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bold (Ctrl+B) and italic (Ctrl+I) now unwrap when the cursor is already
inside a Strong/Emphasis node. Added #underline[…] and #smallcaps[…]
wrap commands (toolbar only) and #link("")[…] with Ctrl+K shortcut that
places the cursor in the URL field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New theme with gradient document cards, serif title typography and a
light airy palette. Set as the default for new users. Existing users
keep their current theme; all users can switch via the theme toggle
(new sparkle icon). Classic Dark / Classic Light are renamed accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OVERLEAF_LATEX_SHELL_ESCAPE=true was added to the prod workflow but
missed in the test workflow, so the svg package still failed on
test.alocoq.fr despite inkscape being installed in the image.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
inkscape's apt dependencies include python3-numpy, which pip can't
uninstall (no RECORD file). Moving inkscape to its own RUN layer after
the pip installs avoids the conflict: pip numpy lands in /usr/local/lib
first, then apt installs its numpy into /usr/lib alongside it, and
Python resolves /usr/local/lib first at import time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
inkscape pulls in python3-numpy 1.26.4 via apt; pip can't uninstall apt
packages (no RECORD file). --ignore-installed makes pip install its own
copy into /usr/local/lib without touching the apt version; /usr/local/lib
takes import precedence so runtime code gets the pip-managed numpy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The LaTeX svg package converts .svg files to PDF at compile time by
shelling out to Inkscape (requires --shell-escape). Without Inkscape in
the image and the flag enabled, compilation fails with "Did you run the
export with Inkscape?".
- Dockerfile-base: add inkscape to the apt install block
- settings.js: expose OVERLEAF_LATEX_SHELL_ESCAPE env var → clsi.latexShellEscape
- LatexRunner.js: pass -shell-escape to latexmk when the setting is on
- deploy-verso-prod.yml: set OVERLEAF_LATEX_SHELL_ESCAPE=true (trusted-user instance)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
userCanInstallPython passed null as the token, so anonymous users
accessing via a share link got privilege level NONE from the WithoutUser
path and allowPythonInstall was always false for them.
Read the token from req.session.anonTokenAccess via
TokenAccessHandler.getRequestToken and forward it through
userCanInstallPython to getPrivilegeLevelForProject. For TOKEN_BASED
projects this resolves the anonymous user's access level via
getPrivilegeLevelForProjectWithToken, enabling package installation.
Also update Quarto Slides badge color to #e4637c.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
options.compiler is set from req.body.compiler which the frontend never
sends, so the condition was never true and quartoFlavor was never written.
Use ProjectGetter to read the stored compiler instead. Fire-and-forget so
it does not delay the compile response.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Existing projects have no quartoFlavor value in the database (new field),
so defaulting to 'Quarto PDF' incorrectly labelled all of them. Show the
plain 'Quarto' label until the first compile sets the flavor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously userCanInstallPython used ignorePublicAccess: true, which
blocked token-link users (not-yet-joined) and logged-in readers of public
projects from installing packages. This caused Quarto presentations with
Python cells to fail for shared read-only users even when the required
packages were already listed in requirements.vrf.
The security model is: what gets installed is fully controlled by
requirements.vrf, which is only writable by members with write access.
There is therefore no security reason to block other readers from
triggering installation of already-approved packages.
Drop ignorePublicAccess so all users with any privilege level (direct,
token-based, or public-project) can trigger the venv install.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix wrong import path '../models/Project.mjs' → '../../models/Project.mjs'
(from Features/Compile/, '..' is Features/, not src/; the server would
crash on startup with ERR_MODULE_NOT_FOUND in Node.js ESM)
- Log MongoDB errors instead of silently swallowing them
- Remove null from Mongoose String enum (not a valid enum value for strings)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a quartoFlavor field ('revealjs' | 'pdf') to the Project model.
After each successful Quarto compile, CompileController detects the output
type (output.html → revealjs, otherwise pdf) and persists it.
ProjectListController includes it in the projection and serialization so
it reaches the frontend without an extra round-trip.
Badge variants:
- quartoFlavor unset (new/uncompiled) → "Quarto PDF" #447099
- quartoFlavor 'pdf' → "Quarto PDF" #447099 (Quarto blue)
- quartoFlavor 'revealjs' → "Quarto Slides" #7e56c2 (purple)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous approach (pdfFile?.path === 'output.html') caused a
chicken-and-egg problem: the button only appeared after a successful
RevealJS compile, but you need to add packages before the first compile.
Use compiler === 'quarto' from ProjectSettingsContext instead — this is
set from project metadata and available immediately, before any compile.
Quarto supports Jupyter Python cells in all output formats (RevealJS HTML,
PDF via LaTeX, PDF via Typst), so showing the button for any Quarto project
is the correct behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LaTeX badge: #13c965 (Overleaf brand green, from upstream overleaf/overleaf)
- Typst badge: #239dad (Typst brand blue/teal, from typst.app)
- Python packages toolbar button: only shown when the compiled output is
output.html, i.e. a Quarto RevealJS presentation. Uses the same
pdfFile?.path === 'output.html' check as PresentationPreviewButton.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
stroke: 0.8pt + brand broke arg-list parsing because '+' was not a grammar
terminal. The parser exited CodeArgs via error recovery, so subsequent
named args (radius:, inset:, fill:) were never seen as CodeArgKey.
Add codeArgValue { codeValue | codeArgValue !add "+" codeValue } — a
left-recursive inline rule used only inside CodeArgs. The !add cut point
gives the shift strict dominance over the reduce (prec add > 0 vs 0), so
a '+' after a value greedily extends the expression. Because codeArgValue
only appears inside CodeArgs, the codeStatement* LALR-merging that caused
trouble for the earlier callSuffix* approach does not apply here.
Also add PLUS to codeIdentTokenizer's valid-predecessor list so identifiers
after '+' (the right-hand operand) are correctly tokenized as CodeIdent.
Add "+" to @tokens @precedence so it beats MarkupContent in merged states.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the opaque CodeBlockBody external tokenizer with grammar-parsed
codeStatement* so that keywords (show, let, set, …) and identifiers
inside #{ } code blocks receive proper Lezer nodes and are highlighted.
Key grammar changes:
- CodeBlock { "{" codeStatement* "}" } — structured, not opaque
- codeStatement uses two explicit alternatives for keyword lines:
CodeKeyword !kw callOrValueAndBody (grabs the subject eagerly)
CodeKeyword keywordBody? (bare keyword or body-only form)
The !kw cut-point gives shift prec kw > 0 over the unannotated reduce,
resolving the LALR merge ambiguity without @left/@right on kw.
- callOrValue { FuncExpr | CodeIdent | CodeString } — replaces CallExpr
{ CodeIdent !call callSuffix* }. The * quantifier annotated both
shift and reduce with !call, making them a same-prec tie that @right
could not reliably resolve in merged states. Using FuncExpr (required
callSuffixes) + bare CodeIdent makes the tie strict (call > 0 for
FuncExpr shift vs 0 for bare-ident reduce), then @right handles only
the extension-of-callSuffixes case (shift = call<<2, FuncExpr reduce
= call<<2 - 1 via @right encoding).
- KeywordExpr gets the same two-alternative structure as codeStatement
so nested show/set/let inside a code block (e.g. show sel: set text)
also parse without LALR state-merge conflicts.
- CallExpr removed; its role is split between FuncExpr (has args/chain)
and bare CodeIdent (no args). Styling updated: CodeExpr/CodeIdent
replaces CallExpr/CodeIdent for bare #ident function-style highlights.
- codeKeywordTokenizer and codeIdentTokenizer already accept keywords /
identifiers after { and ; (added in previous commit) — consistent with
the new grammar.
Parse results:
#{ show strong: link.with(url); body }
→ CodeKeyword "show", CodeIdent "strong", FuncExpr "link.with(url)",
CodeIdent "body" — all properly highlighted, no ⚠ errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
align: (left, center, left) is a Typst array literal. Without a grammar
rule for it, the parser treated the ')' closing the tuple as the ')' closing
the enclosing function call, so everything after align: — all ContentBlock
args and any subsequent named keys like 'caption:' — fell outside the parsed
call tree and was highlighted as MarkupContent.
Add CodeArray { "(" codeArgList? ")" } as a codeValue alternative so
parenthesised arrays and dictionaries parse correctly. Also regenerate
typst.mjs / typst.terms.mjs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The tok-attributeName CSS class relies on each theme defining it, but
26 of 41 themes never had it. Defining the colour directly in
typstHighlightStyle (like we do for heading/strong/emphasis) applies
it universally regardless of which theme is active.
Amber #c47900 is legible on both light and dark backgrounds.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds { tag: t.attributeName, color: '#cc0000', fontWeight: 'bold' } to
typstHighlightStyle so named arg keys are unmistakably red if the
CodeArgKey token is reaching the highlighter. Will be removed once
the pipeline is confirmed working and replaced with per-theme colors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The webpack plugin that compiles typst.grammar may silently skip
recompilation when file mtimes are ambiguous in Docker BuildKit layers.
Committing typst.mjs and typst.terms.mjs guarantees the build always
ships the correct parser without depending on build-time generation.
To regenerate after grammar changes:
node -e "const {buildParserFile}=require('/tmp/lezertest/...'); ..."
(or: yarn run lezer-latex:generate from services/web)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The core bug: MarkupContent { ![...]+ } did not exclude ']', so inside
#figure(table([A],[B]), caption:[...]) the tokenizer consumed ']' as
MarkupContent, ContentBlocks never closed, and all remaining args like
'caption:' were swallowed as MarkupContent instead of CodeArgKey.
Fix mirrors the LaTeX grammar pattern (its Normal token excludes \] and
\[): add ']' to MarkupContent's exclusion set and provide ClosingSquare
{ "]" } as an item alternative for bare ']' in body text. The grammar's
existing @precedence { "]" ClosingSquare } ensures "]" wins and closes
the ContentBlock; outside a ContentBlock only ClosingSquare is valid.
Also change URL style tag from t.url (tok-url, unstyled in all themes)
to t.string (tok-string, styled in every theme).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add URL token (https://... / http://...) so '://' is never split into
':' + LineComment '//', preventing URLs from being styled as comments
- Stop headingTitleTokenizer before '<label>' patterns so labels at the
end of headings get Label node styling instead of being consumed as
heading title text
- Style URL nodes with t.url tag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three grammar gaps caused large blocks of code to be unhighlighted:
1. KeywordExpr now accepts an exclusive keywordBody: '#show sel: body' is
parsed via ':', and '#let name = value' via '='. callOrValue extends
the subject to include CodeString so '#import "pkg"' highlights the path.
2. ContentBlock added to callSuffix so '#func("arg")[content]' and
'#next-step("url")[...]' parse their trailing content block as code
rather than falling back to markup.
3. Tokenizer: COLON added as a valid predecessor so identifiers (e.g. 'blue'
in 'fill: blue') and keywords (e.g. 'set' in '#show link: set text(...)')
are recognised after ':'. EQUALS already added in the previous commit.
The ident-chain backward scan now also skips whitespace before testing for
'#' or ':', enabling 'text' in 'set text' to trace back to '#' through the
keyword gap. @precedence updated with CodeString, '[', ':' to resolve
overlapping-token conflicts with MarkupContent in merged states.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
canShift(CodeIdent) returns false in LALR-merged states that arise after
reducing a complex first argument (e.g. figure(table(...), caption: ...)).
The previous guard `!couldBeIdent && !canShift(CodeArgKey)` then caused
an early exit before the character-level scan ran, silently dropping the
CodeArgKey token for any named arg key that follows such a reduction.
Fix: run the backward character scan first and derive `couldBeArgKey`
from the raw predecessor char ('(' or ',') rather than from canShift.
The early-exit now reads `!couldBeIdent && !couldBeArgKey`, so arg-key
positions always proceed to the full scan regardless of parser state.
Also stop calling canShift(CodeArgKey) entirely — it is unreliable here.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Typst grammar now emits CodeArgKey (mapped to tok-attributeName) for
named argument keys like 'columns:', 'align:', 'caption:'. 15 of 41
editor themes had no .tok-attributeName rule, so those keys appeared in
the default text color (black) despite the correct CSS class being set.
Chose colors that complement each theme's existing palette:
light themes → warm dark-orange family (#994409 / #7B3814 / #735C0F)
dark themes → each theme's accent color (gold, warm red, lavender…)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
canShift(CodeArgKey) was consistently returning false because LALR
state merging folds the codeArgItem start state into others where
CodeArgKey is not in the valid set. As a result, named arg keys like
'columns:', 'align:', 'caption:' were always falling through to
CodeIdent (black) instead of CodeArgKey (tok-attributeName).
Fix: detect named arg key position by inspecting the nearest
non-whitespace predecessor character instead of trusting canShift.
prev == '(' or ',' means we are inside a call argument list — the only
positions where a named arg key can appear. prev == last char of a
keyword word (e.g. 'w' of 'show') correctly excludes '#show heading:'
from being treated as a named arg.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Named value identifiers like 'left', 'center', 'right' were being
highlighted as tok-function (blue) because codeValue used CallExpr
(callSuffix*), which styled any identifier in value position as a
function. Fix: add FuncExpr { CodeIdent callSuffix+ } (requires at
least one argument list or method suffix) and use it in codeValue
instead of CallExpr. Plain identifiers in value position now fall
through to CodeIdent → tok-variableName. CallExpr (callSuffix*) is
kept for codeExprBody and KeywordExpr where zero-suffix idents are
valid.
Tokenizer safety: only acceptToken(CodeIdent) when canShift(CodeIdent)
is true, preventing emission in LALR-merged states where neither
CodeArgKey nor CodeIdent is expected.
Outline: track '$'-parity across lines so that lines inside a display
math block (e.g. '= b+c$') are not incorrectly reported as headings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Named arg keys (columns:, align:, caption:) were appearing in black
because LALR state merging broke the CodeArgs/CodeIdent path for
multi-line expressions. Fix: emit a dedicated CodeArgKey token from
codeIdentTokenizer (forward-peek for ':' to pre-disambiguate), declare
it in the grammar's codeArgItem rule, and map it to t.attributeName in
styleTags — bypassing LALR lookahead entirely.
Multi-line display math ($ ...\n... $) was consuming the rest of the
document as orange text when contextual:true caused a backward scan to
find a previous closing '$' and falsely set isDisplay=true. Fix:
revert mathContentTokenizer to contextual:false with '\n' stop (each
MathContent token covers one line), and change InlineMath to
MathContent* so @skip consumes the newlines between lines.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mathContentTokenizer now detects inline vs display math by scanning back
to the opening '$': if @skip consumed whitespace between '$' and the
content the tokenizer removes the newline stop (display math), otherwise
it keeps it (inline math). contextual: true prevents the tokenizer from
firing outside InlineMath entirely, avoiding the orange-body-text
regression seen when this was previously attempted without the guard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- codeIdentTokenizer: extend guard to scan back through the keyword word
and accept when '#' immediately precedes it, so 'text' in '#set text(...)'
and 'heading' in '#show heading:' are highlighted as function names
- classHighlighter: add tags.attributeName → tok-attributeName mapping;
all 26 themes already define .tok-attributeName colours but the tag was
never mapped to the class, leaving named arg keys (columns:, caption:)
completely unstyled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'\n' inside CodeArgs was an invalid token, triggering Lezer error recovery
and resetting parser state before codeIdentTokenizer could fire. Heading
detection is unaffected — headingTokenizer uses raw input.peek(-1) char
reads which see the '\n' byte regardless of what @skip consumes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
codeIdentTokenizer's backward scan stopped at \n, so identifiers at the
start of a new indented line inside multi-line arg lists (e.g. image(),
table() inside #figure(...)) never matched the '(' guard and stayed black.
Extend the whitespace skip to also cross newlines.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two LALR state-merging bugs prevented Strong/Emphasis nodes from ever being
produced (confirmed: tok-strong/tok-emphasis count = 0 in browser diagnostic).
Bug 1 — _italic_ consumed as CodeIdent:
CodeIdent was a @tokens rule with identHead = [A-Za-z_], so '_italic_' (the
entire string including both underscores) matched as one CodeIdent token.
LALR merging caused CodeIdent to be in item*'s valid set, and CodeIdent >
"_" in @precedence, so the parser never opened Emphasis.
Fix: move CodeIdent to an external tokenizer (codeIdentTokenizer) with a
character-level guard — only fires when the preceding non-whitespace char
is one of '#', '.', '(', ',' (genuine code-context positions). In body
text where peek-back finds a newline, space, or markup delimiter, the
tokenizer returns without emitting, letting '"_"' open Emphasis correctly.
Bug 2 — StrongText never produced inside Strong:
The strongItem* / emphItem* loops merged with item* states via Lezer's
aggressive LALR merging. In the merged state MarkupContent was in the
valid set (from the item* side) and MarkupContent > StrongText in
@precedence, so MarkupContent was always produced — not a valid strongItem,
leading to error recovery with no StrongText in the tree.
Fix: replace the recursive strongItem* / emphItem* loops with flat external
tokens StrongBody / EmphBody (contextual: true). These fire only inside
Strong → "*" . StrongBody? "*" and Emphasis → "_" . EmphBody? "_", states
specific enough that canShift is reliable. They read everything up to the
closing delimiter or newline in one token, bypassing the LALR merging
entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous change switched mathContentTokenizer to contextual:true with no
newline stop, intending to support multi-line Typst block math. However,
LALR state merging causes canShift(MathContent) to spuriously return true in
body-text positions (e.g. after a RawInline backtick close), so the tokenizer
consumed everything until the next '$' — turning a full paragraph orange.
Revert to contextual:false with newline stop. This correctly handles both
inline ($x^2$) and single-line block ($ integral ... $) math. Multi-line
block math ($ formula\n continuation $) remains a separate issue for later.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mathContentTokenizer was stopping at newlines, causing a parse error for
Typst block math that spans multiple lines. The parser then entered a bad
state that cascaded: the stale error-recovery left the item* parser in a
degraded mode, causing body text below the math block to be highlighted as
t.string (orange).
Fix: switch to contextual: true (only fires inside InlineMath where
MathContent is actually expected) and remove the newline restriction so
the tokenizer reads until the closing '$' regardless of line boundaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add .tok-strong and .tok-emphasis CSS to the static editor theme so
bold/italic markup actually renders visually.
Move CodeKeyword from @tokens to an external tokenizer (codeKeywordTokenizer)
with a peek(-1)==='#' guard. LALR state-merging causes code-mode states to be
reachable in markup positions, making common English words like "in", "for",
"while", "return" trigger CodeKeyword highlighting in body text. The '#' guard
ensures keywords only fire immediately after the '#' sigil, never in prose.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
canShift(RawBlockBody) returns true in states LALR-merged with the
post-RawBlockOpen state, causing the tokenizer to consume all remaining
body text as one giant RawBlockBody. Add a backward character scan:
require newline immediately before input.pos, then walk back past any
lang tag (A-Za-z0-9) and verify three backticks precede it. Body-text
positions never have backtick-backtick-backtick there, so the guard
rejects them.
This was the root cause of everything after the first heading being
black: RawBlockBody swallowed the entire document from the user-name
line onward, making headings, bold, italic and math invisible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The single-HeadingLine token approach caused everything after the first
heading to be unparsed. Reverting to the two-token structure but adding a
backward character scan in headingTitleTokenizer: after canShift(), walk
backward past whitespace and require '=' immediately before the current
position. Body-text positions in LALR-merged states will have a letter or
closing bracket there instead, so the tokenizer returns without accepting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The two-token approach (HeadingMark + HeadingTitle) caused LALR state
merging: the parser state waiting for HeadingTitle after HeadingMark was
merged into body-text item* states. In those merged states the
headingTitleTokenizer fired for every paragraph line, swallowing bold,
italic, math and inline function tokens — leaving body text black.
Fix: collapse the heading into a single HeadingLine external token that
covers the entire heading line (= prefix + title). A single-token Heading
rule leaves no post-token parser state waiting for a second token, so no
LALR merging can occur. The ViewPlugin and all HeadingMark/HeadingTitle
infrastructure are removed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removing HeadingTitle from the grammar left HeadingTitle? undeclared,
causing the Lezer grammar compiler to fail and producing no parser
output — hence everything rendered as unstyled black text.
Dual approach to prevent heading style bleed:
- HeadingTitle exists in grammar with contextual: true + canShift guard
(prevents it from matching in body-text LALR states)
- HeadingTitle is intentionally absent from styleTags so even spurious
matches cannot apply heading colour to body text
- ViewPlugin styles heading titles by finding HeadingMark nodes and
extending tok-heading decoration to end-of-line
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes:
1. Heading style bleeding (Typst): the HeadingTitle external token approach
was unreliable — even with contextual:true and canShift(), body text was
being styled as headings. Remove HeadingTitle from the grammar entirely.
Instead, a ViewPlugin (headingLinePlugin in languages/typst/index.ts)
walks the syntax tree, finds HeadingMark nodes, and decorates the rest of
the line with tok-heading class + bold. This is unconditionally correct
because it is based on the syntax tree rather than the LR tokenizer state.
2. smooth_pdf_transition raw key shown in all locales: the key was in the
JSON locale files but missing from extracted-translations.json, which is
the allowlist the webpack translation loader uses to decide what to bundle.
Add it there so all locales (including fr, es, de already added) resolve
to their translated strings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Draft compile mode and stop-on-first-error are LaTeX-only features not
supported by TypstRunner or QuartoRunner. Hide both sections from the
recompile dropdown for non-LaTeX projects. Also detect Quarto root files
(.qmd/.md/.Rmd) alongside Typst (.typ) to correctly set isLatexProject.
Add missing smooth_pdf_transition translations for French, Spanish, and
German (the English key already existed).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two unrelated fixes:
1. quarto-log-parser: handle the two-line Quarto schema-validation
error format:
ERROR: In file main.qmd
(line 6, columns 24--27) Field "section-numbering" has value …
Previously neither the file name nor the line number were extracted,
so the error appeared without a red highlight. Now the first line
stores the filename in pendingLocation and the second line creates
the log entry with the correct file and line so the editor can jump
to and highlight it.
2. headingTitleTokenizer: change contextual: false → contextual: true
and guard with stack.canShift(HeadingTitle). With contextual: false
Lezer calls the tokenizer speculatively at positions beyond the strict
post-HeadingMark state; in some LALR-merged states the resulting token
was accepted for body-text lines, making them render as bold-blue
heading text. The contextual guard ensures the tokenizer only fires
in the one state where HeadingTitle is legitimately valid.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two ideas borrowed from the Collabst project (a Typst-native
collaborative editor): typst.ts WASM in-browser preview and Tinymist
LSP integration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
typst watch outputs the "[HH:MM:SS] compiled with errors" status line
FIRST, then the full diagnostic output (file:line:col, source snippets,
hints) AFTERWARDS. The previous code resolved the pending compile
promise as soon as COMPILE_DONE_RE fired, discarding all post-status
diagnostic lines. Those lines then got cleared by the next cycle's
COMPILE_START_RE, so output.log only ever contained the bare status
line — explaining the "zero verbosity" symptom.
Fix: introduce a two-phase buffering model. When COMPILE_DONE_RE fires,
enter "post-done" phase (storing doneResult) and keep accumulating into
currentLines. _finalizeCompile() is called either when the next
COMPILE_START_RE arrives (zero added latency) or after FLUSH_DELAY_MS
(150 ms fallback for the last compile). It concatenates pre-done and
post-done lines before resolving, so output.log now contains the full
diagnostic output.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When typst watch detects a file change and compiles before the CLSI
resolver is registered (ResourceWriter writes files → typst compiles →
runTypst is called), _resolveAllPending was discarding the result
because pendingResolvers was empty. This caused two symptoms:
1. output.log only contained "compiled with errors" (no diagnostics)
because the result carrying the full stdout was thrown away.
2. Every other manual compile failed with "compilation already gone"
because the missed result caused a timeout, which killed the watcher
and triggered a watcher restart cycle (success → miss → timeout →
kill → restart → success → miss → ...).
Fix: when _resolveAllPending fires with no pending resolvers, store the
result in entry.pendingResult. _waitForNextCompile checks this field
first and resolves immediately if a cached result is present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- local-compile-context: suppress failure/exited error state when
changedAt > 0 (another compile is already queued), preventing the UI
from flashing an error banner mid-typing that resolves moments later
- TypstRunner + CompileController: detect "compiled with errors" from
typst watch and non-zero exit from typst compile, and signal
status:'failure' to the frontend so the log panel opens automatically
with the parsed error details (previously always returned 'success')
- footer.scss: add dark-mode overrides for footer.site-footer so the
thin footer on project/marketing pages uses bg-dark-primary and
content-primary-dark text in dark theme instead of hardcoded light bg
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KeywordExpr { CodeKeyword CallExpr? } merges the post-keyword LR state
with document-level markup states, where "_" opens Emphasis. CodeIdent
starts with identHead which includes "_", so the two tokens overlap.
Adding "_" after CodeIdent in @precedence resolves the conflict: CodeIdent
wins in the merged state (correct for '#set _name(...)'), and in pure markup
states CodeIdent is not in the valid set so "_" still opens Emphasis.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
yamlFrontmatter() embeds the Markdown content as an overlay on the top-level
YAML-frontmatter tree. The previous mode (IgnoreMounts | IgnoreOverlays)
skipped that overlay entirely, so ATXHeading nodes were never visited and the
Quarto (.qmd) file outline was always empty.
Dropping the mode flag lets the iterator descend into overlay and mounted
subtrees. This is safe because every enterNode function already filters by
node name — visiting extra nodes from foreign-language mounts is a no-op.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KeywordExpr now optionally includes a CallExpr, so '#set text(size: 12pt)'
parses 'text' as a CallExpr/CodeIdent and gets the function-name highlight
colour. The optional CallExpr only shifts when the lookahead is CodeIdent,
so there is no shift/reduce conflict with other items.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
headingTitleTokenizer now stops reading when it encounters '//' or '/*',
so '= Heading // note' correctly produces a HeadingTitle token for 'Heading'
and a LineComment for the rest of the line. Without this, the comment was
consumed into HeadingTitle, getting heading highlight and appearing verbatim
in the file outline.
Also strip trailing line comments from heading titles in the regex-based
document outline scanner, which reads raw text independently of the tree.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All cm6 themes define .tok-function but the classHighlighter had no entry
for tags.function(tags.variableName), so function-name tokens fell back to
tok-variableName (which themes leave unstyled). This affected Typst function
calls (#func(...)) and would affect any future language that tags function
names with t.function(t.variableName).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both tokens are "read until delimiter" catchalls that match almost every
non-newline character, causing buildTokenGroups conflicts with every other
literal token in LALR-merged states. Moving them to ExternalTokenizer (the
same pattern already used for HeadingTitle, RawBlockBody, etc.) makes them
context-isolated: the LR state machine only calls them when those tokens are
actually valid, so they never participate in the static token-group overlap
check.
Also exclude '<' from StrongText/EmphText so Label ('<' LabelName '>') is
recognised inside strong/emphasis spans rather than being consumed as plain
text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lezer's buildTokenGroups rejects grammars with ambiguous token sets.
Eight overlaps existed:
EscapeChar vs spaces — EscapeChar { _ } matches \t; after '\'
it must win over the @skip spaces token.
"(" / "." vs text tokens — in the LALR-merged state after #CodeIdent,
callSuffix delimiters must beat
MarkupContent / StrongText / EmphText.
"]" vs LineCommentContent — inside #[...], the ContentBlock closer
must win even if it follows "//".
One extended @precedence declaration resolves all eight.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LineCommentContent { ![\n]* } matches the empty string, which Lezer
rejects as a zero-length token (infinite-loop risk). Change to ![\n]+
and mark it optional in the LineComment rule so empty // comments parse.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Any item shared between headingTitleItem and document-level item causes
a shift/reduce conflict: the LALR automaton merges the two contexts and
makes the shared token ambiguous. The only structural fix is to make
HeadingTitle a terminal (external tokenizer) that reads greedily to EOL,
giving the LR state machine a context-isolated token that can never
collide with document-level item tokens.
Removes headingTitleItem sub-rule, HeadingText token, and updates
styleTags to match HeadingTitle directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removing Strong and Emphasis from headingTitleItem eliminates the
conflict: both appear in document-level item, causing the LR automaton
to merge heading-title states with document-item states and make "*"
ambiguous (Strong opener vs. end of heading).
HeadingText is widened to ![\n$#`<@\\]+ so "*" and "_" inside headings
are consumed as plain text rather than producing error nodes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
headingTitleItem* allowed an empty HeadingTitle, causing a shift/reduce
conflict: after HeadingMark, seeing "*" the LR parser couldn't decide
whether to shift it as a Strong inside the heading or reduce HeadingTitle
to empty and treat "*" as a document-level item.
Changing to headingTitleItem+ forces HeadingTitle to be non-empty, so
"*" after HeadingMark must be inside the heading. Empty headings are
handled by Lezer's error recovery.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Strong{strongItem{Emphasis}} and Emphasis{emphItem{Strong}} created a
mutual-recursion cycle that caused Lezer's LR automaton builder to
produce exponentially many states and crash.
Remove each construct from the other's item list. StrongText already
includes '_' and EmphText already includes '*', so nested delimiters
render as plain text inside the opposite construct rather than errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the StreamLanguage tokenizer with a full LR grammar compiled by
@lezer/generator, giving Typst the same parse-tree infrastructure that
LaTeX and BibTeX already use.
Grammar features:
- Headings (=, ==, …) via SOL-detecting external tokenizer
- Code expressions (#keyword, #func(args), #ident.method, #{…}, #[…])
- Named argument highlighting (key: value in function calls)
- Inline and display math ($…$)
- Strong (*…*) and emphasis (_…_) with bold/italic formatting
- Raw blocks (```lang…```) and inline raw (`…`)
- Nested block comments (/* /* */ */) via depth-tracking external tokenizer
- Labels (<name>) and references (@name)
- Backslash escapes
Infrastructure changes:
- lezer-typst/typst.grammar — new Lezer grammar
- lezer-typst/tokens.mjs — external tokenizers for context-sensitive lexing
- scripts/lezer-latex/generate.mjs — Typst added to grammars array so the
existing lezer-latex:generate script (and Dockerfile step) compile it
- .gitignore — generated typst.mjs / typst.terms.mjs excluded from git
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the custom regex-based ViewPlugin with the official
@codemirror/lang-yaml package. yamlFrontmatter({ content: mdLS })
wraps the Markdown language with a mixed parser: the leading ---/---
block is handed to the full Lezer YAML parser (proper key/value/scalar/
anchor/alias highlighting), while the document body continues to use
the Markdown parser. The manual Frontmatter extension import is also
removed since yamlFrontmatter handles frontmatter recognition itself.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Typst: heading tokenizer now colors the entire heading line (not just the
'=' prefix), and bold/italic markers (*/_) map to strong/emphasis tags
rather than the generic operator tag. A typstHighlightStyle applies
bold/italic formatting even when the active theme lacks .tok-heading.
Quarto: enable @lezer/markdown's Frontmatter extension so the YAML header
is no longer mis-parsed as Setext headings. A new ViewPlugin decorates
frontmatter lines with type-appropriate CSS classes: keys (tok-typeName),
string/bool/number values, comments, and the --- delimiter markers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Apply the same CSS inversion filter to the HTML iframe as is already
applied to pdfjs PDF pages, so Quarto RevealJS presentations respect
the dark mode toggle. Also show the theme button for HTML outputs and
relax the darkModePdf condition to activate for iframes regardless of
the pdfViewer setting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When output.html exists, findOutputFiles includes project media files
(images, videos) in outputFiles via the MEDIA_REGEX exception so they
get served from the cache. _removeExtraneousFiles then treated them
as extraneous and deleted them. On the next incremental compile,
unchanged binary files are not re-synced, so the files were gone when
Quarto ran and when _appendMissingResourceWarnings checked for them.
Fix: skip deletion for any file that is a project input resource.
Those files appear in outputFiles to be served, not cleaned up.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When typst watch doesn't emit "compiled with errors" after a failed
compile, currentLines accumulates indefinitely. The next successful
compile then flushes the buffer including the stale error from the
prior cycle. Reset currentLines at the start of each compile cycle
("[HH:MM:SS] compiling ...") so each log only contains output from
one compile.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per-project-type setting: Typst defaults to on, LaTeX defaults to off.
Toggle appears in the compile dropdown under "Smooth PDF transition".
The enableTransition flag is read via a ref so toggling does not
reload the current PDF.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The auto-compile effect was calling debouncedAutoCompile() on every changedAt
update (every keystroke), including while a compile was already running. With
a 1000ms maxWait the debounce fired every second even mid-compile, chaining
compiles back-to-back and making the user wait for all of them to drain.
Fix: add `compiling` to the effect's dependency array.
- While compiling: the effect cancels the debounce immediately, preventing
any new compile from being queued.
- When compile finishes (compiling → false): the effect re-runs; if changedAt
is still > 0 (changes were made during the compile), it re-arms the debounce
exactly once. One follow-up compile, then idle.
Also remove the debouncedAutoCompile() re-queue from compiler.ts's
wasCompiling guard — the effect now owns that responsibility.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
document.startViewTransition with an async callback places a ::view-transition
overlay on top of the entire page, intercepting pointer events for the duration
of the callback (up to the 1s safety timeout + 250ms animation). With rapid
auto-compiles this created interface freezes and overlapping transitions that
could leave the visual lock in a broken state, causing 'stuck on compiling'.
Replace with a canvas snapshot overlay + CSS opacity fade-out:
- pointer-events:none so the overlay never blocks input
- snapshot covers the canvas-clear from setDocument() (no white flash)
- on pagerendered: opacity transitions to 0 over 250ms, then overlay removed
- gives the same smooth visual crossfade, reliably, in all browsers
Chrome 126+ retains the element-level startViewTransition path which is
scoped to the PDF container and does not affect the rest of the page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
document.startViewTransition generates both a named pseudo-element for the PDF
container (ol-pdf-viewer) and a root-level pseudo-element that covers the entire
page, causing the editor to fade along with the PDF.
Inject a temporary <style> that sets animation:none on the root pseudo-elements
before starting the transition, then remove it in transition.finished. Only the
named PDF container crossfades; the editor is unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The original code used container.startViewTransition(setDocument) with a
synchronous callback, giving a 250ms CSS crossfade that looked smooth when
the PDF happened to re-render before the animation ended — but was a race.
Now there are three tiers:
- Chrome 126+: element-level startViewTransition, async, waits for pagerendered
- Chrome 111+ (Brave 138, Edge 111+): document-level startViewTransition with
view-transition-name scoped to the PDF container, same async pattern
- Firefox / Safari / older Chromium: canvas snapshot overlay (no animation,
but seamless — introduced in build #108)
The document-level path restores the smooth fade the user saw on Edge build
#93, now guaranteed to crossfade old→new rather than old→blank.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rate limit: auto-compile requests already have a client-side debounce; skip
the 1-second server-side recently-compiled gate for them to avoid spurious
'too-recently-compiled' rejections that were blocking ~1/3 of Typst compiles.
PDF flicker: add _snapshotCanvases() fallback for browsers without element-level
View Transitions (Chrome <126, Firefox, Safari). Before setDocument() clears the
canvases it copies each rendered page to a positioned overlay; the overlay is
removed once the first page of the new document fires pagerendered, giving a
seamless old→new swap in all browsers. Chrome 126+ continues to use the
startViewTransition async callback path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous implementation used useState() to detect the project type, but the
file tree is loaded asynchronously after the WebSocket joinProject event, so
pathInFolder() always returns null on the initial render.
Use useEffect() instead — it re-runs when getRootDocInfo's reference changes
(i.e. when the file tree populates), correctly detecting .typ root docs.
Also adds updateAutoCompileDebounce() to DocumentCompiler so the tight
debounce can be applied at that point.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Typst projects default autocompile to enabled (300ms debounce / 1s max-wait
instead of 2.5s/5s), so the PDF refreshes nearly as the user types.
- Make startViewTransition wait for the first page to render before completing
the crossfade, eliminating the old-PDF→blank flash on Chrome 126+.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of cold-starting 'typst compile' on every request, TypstRunner
now maintains a long-lived 'typst watch' process per project. Subsequent
compiles reuse the warm process, which caches fonts, packages, and the
compiled AST via Typst's comemo framework — dramatically faster.
Architecture:
- WatchTable: maps compileName → live watcher process + state
- _startWatcher: spawns 'typst watch input.typ output.pdf', registers
stdout/close handlers, then immediately awaits the first compile result.
The resolver is pushed to pendingResolvers synchronously inside the
Promise constructor before any I/O event can fire — eliminating the
race between file-write detection and resolver registration.
- _onWatcherData: parses stdout line-by-line, resolves pending callers
on "compiled successfully/with warnings/with errors" (the three terminal
lines typst watch emits at the end of each compile cycle).
- Graceful restart: watcher is restarted after MAX_COMPILES_BEFORE_RESTART
(1000) cycles to stay clear of Typst's ~65k FileId limit, or immediately
if the "ran out of file ids" message is detected in stdout.
- killTypst: tears down both the watcher and any cold-start fallback job;
called by stopCompile (user-initiated) and clearProject/clearProjectWithListing
(before compile-dir deletion).
- Docker fallback: Settings.clsi.dockerRunner=true falls back to the
original cold-start 'typst compile' path unchanged.
- process.on('exit') kills all watcher process groups on CLSI shutdown.
CompileManager: call TypstRunner.promises.killTypst before deleting the
compile directory in both clearProject and clearProjectWithListing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Typst has no --synctex CLI option (open feature request #289 since 2023).
Revert the frontend guard back to LaTeX-only and remove --synctex from
the Typst compile command. Also remove the temporary logger.warn calls
added for diagnosing the LaTeX synctex issue (now resolved).
The official Typst binary installation in Dockerfile-base is kept as it
is cleaner than using Quarto's modified fork for .typ compilation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Quarto bundles a modified Typst fork that lacks --synctex, making
bidirectional sync impossible. Install the official Typst binary
(v0.13.1) from upstream and use it in TypstRunner instead.
This also means .typ projects now use the unmodified Typst compiler,
which is correct since TypstRunner handles plain .typ files (not .qmd).
QuartoRunner continues to use Quarto's bundled Typst internally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of going through 'quarto typst compile' (which intercepts
--synctex before it reaches Typst), call the Typst binary bundled in
the Quarto .deb directly at /opt/quarto/bin/tools/x86_64/typst.
This allows passing --synctex output.synctex.gz to generate the SyncTeX
file for bidirectional editor↔PDF sync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Typst's CLI requires options before positional arguments (INPUT OUTPUT).
Placing --synctex after output.pdf caused it to be treated as an extra
positional arg and rejected with 'unexpected argument'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TypstRunner: add --synctex output.synctex.gz to quarto typst compile,
generating a synctex file alongside the PDF (requires Typst 0.11+,
bundled in Quarto 1.5+).
- use-synctex: extend the root-doc guard from LaTeX-only to also cover
.typ files, enabling the Show in PDF / Show in code buttons for Typst.
The rest of the sync infrastructure (OutputCacheManager, synctex binary,
SynctexOutputParser, CLSI routes) is already format-agnostic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The synctex binary was not included in scheme-basic and was not
explicitly installed, causing `spawn synctex ENOENT` on every
sync request. Add it alongside latexmk and texcount.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Logs: request params, directory used, whether output.synctex.gz
is found, and the actual synctex binary output or error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verso added 'qmd' and 'typ' to validRootDocExtensions, which caused
isValidTeXFile() to return true for Typst/Quarto files — enabling
SyncTeX UI controls for projects that never produce output.synctex.gz.
Replace the open-doc extension check in canSyncToPdf with a
LaTeX-only regex on the project root document path (tex|ltx|Rtex|Rnw),
and add the same guard in _syncToCode so PDF-click sync never fires
an API request for non-LaTeX projects.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Replace token-link email with 6-digit code on SSO registration
Unverified SSO emails previously received a long-lived token link
(90-day TTL) via UserEmailsConfirmationHandler. This replaces that
flow with the same 6-digit code verification used for password
registration, redirecting through /registration/confirm-email.
- SSOManager.registerSSO now always confirms email (caller must
verify first); removes sendConfirmationEmail / _finishRegistration
- SSOController._signUp sends confirmation code and stores
pendingSSORegistration in session when IdP email_verified is false
- New SSOConfirmEmailHandler completes registration after code check
via completeSSOEmailConfirmation module hook
- OnboardingController confirm-email handlers accept
pendingSSORegistration alongside pendingUserRegistration
confirmEmailFromToken (POST /user/emails/confirm) removal is deferred
to a follow-up PR to avoid breaking in-flight 90-day tokens.
Closes#28607
* Fix unverified-email edge cases; Add ORCID e2e tests;
* Rename `confirmEmail` parameter to `emailVerifiedByIdP` in _signUp function
* Remove `sendConfirmationEmail`
* Mock getUserByAnyEmail in tests
* Extract _finishSSORegistration helper to deduplicate the register →
set session flags → allocate referral → finishSaasLogin → finishLogin
sequence shared by both the direct and deferred (code-confirmed) paths.
* Stop duplicating session data in pendingSSORegistration
analyticsId, splitTests, and referal_* are already in the session at
confirmation time — no need to copy them into pendingSSORegistration.
Re-fetch splitTests fresh on completion instead.
* Simplify the code
* Remove dead confirmEmail template
No callers remain after sendConfirmationEmail was deleted. The token-link
flow (confirmEmailFromToken) only validates tokens, never sends email.
* Remove dead reconfirmEmail template
* Address comments from Copilot
* Clear stale pending registration when starting a new flow
* Add unit tests for completeSSOEmailConfirmation
* Add `verificationMethod` param
* Fix camelcase issues
* Extract _createSSOUser and _registerAndFinish helpers to deduplicate registration logic
* Remove obscure "registration_error"
* Prevent FormTextIcon from shrinking
* Enable "email_already_registered_sso" error
* Misc. improvements to confirm-email-form.tsx
* Remove `UserEmailsConfirmationHandler` mock
Co-authored-by: Olzhas Askar <olzhas.askar@overleaf.com>
* Add info on sso_email.pug page
---------
Co-authored-by: Olzhas Askar <olzhas.askar@overleaf.com>
GitOrigin-RevId: d0196ebc6d81ff61bcd27726d0b899b743d08d64
* [monorepo] consolidate clsi-lb host/ip env-vars
Target env-var is CLSI_LB_HOST. Keep CLSI_LB_IP populated for a week.
* [clsi] initial version of /convert/pdf-to-jpeg
* [rails] use fake-secrets in CI and Codespaces
* [rails] adapt tests for using clsi to convert PDFs to image
* [rails] add rake task for comparing clsi conversion with transloadit
* [clsi] double check that output.jpg is a regular file
Co-authored-by: Brian Gough <brian.gough@overleaf.com>
* [clsi] fix composing basename
* [monorepo] fix clsi-lb host env-var post merge
* [monorepo] sort dev-environment.env hosts
* [rails] use local pdf file rather than downloading it again
Download from the old renderer code path still. It's dead code.
* [terraform] clsi: enable pdf to jpg conversion
---------
Co-authored-by: Brian Gough <brian.gough@overleaf.com>
GitOrigin-RevId: 5ecaa8559d299486340bb3961f06b29f7c4dfcca
* [web] Order plans in Change Plan modal consistently
Reorder the plans returned by `buildPlansListForSubscriptionDash` so the
Subscription page "Change plan" modal lists them top-to-bottom as:
1. Student annual
2. Student monthly
3. Standard monthly
4. Standard annual
5. Pro monthly
6. Pro annual
Previously `buildPlansList` produced three per-period buckets which the
dash function concatenated, giving an order that flipped per family.
Replace that with an explicit `CHANGE_PLAN_MODAL_PLAN_CODES` list so the
order matches the Design QA spec at a glance. The now-unused
`studentAccounts`, `individualMonthlyPlans`, `individualAnnualPlans`,
`groupMonthlyPlans`, and `groupAnnualPlans` buckets are dropped from
`buildPlansList` (no other callers).
Closes#34024
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [web] Update personal-plan acceptance test for new buildPlansList shape
The previous test asserted `buildPlansList().individualMonthlyPlans`,
which no longer exists after the change-plan modal reorder dropped the
per-period buckets. Move the assertion to
`buildPlansListForSubscriptionDash()`, which is where the personal-plan
exclusion is now enforced (via `CHANGE_PLAN_MODAL_PLAN_CODES`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [web] Drop now-dead client-side plan filter
`IndividualPlansTable` used to filter out `paid-personal`,
`paid-personal-annual` and `institutional_commons` defensively because
the old `buildPlansListForSubscriptionDash` returned every non-group
plan that wasn't `hideFromUsers`. The previous commit pins the modal to
an explicit six-plan list (`CHANGE_PLAN_MODAL_PLAN_CODES`), so none of
those plan codes ever reach the frontend and the filter is dead. Remove
it and the now-unused `useMemo` import.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "[web] Drop now-dead client-side plan filter"
This reverts commit 83e8448f2cfa2c68e44b749d5a2bc350a7443c6d.
We'll do that in a later cleanup
* Swap "Student monthly" and "Student annual" for consistency
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 046a235e14e7ad6622288f5a5a723f5a4f7f14da
* [web] Redirect missing AI add-on purchase to subscription dashboard
The two error paths in `previewAddonPurchase` redirected to
`/user/subscription/plans#ai-assist`, but the `#ai-assist` anchor was
removed when the AI Assist add-on was retired, so users land at the top
of the plans page with no context. Align both with the other error
branches in the same function and the `plans-2026-phase-1` enabled
branch, which already redirect to
`/user/subscription?redirect-reason=ai-assist-unavailable` — the
subscription dashboard shows the matching warning alert
(`redirect-alerts.tsx`).
Update the acceptance test to match the new redirect target.
Closes#34074
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [web] Update ai-assist-unavailable warning to reflect bundled AI features
The previous copy said "AI Assist isn't available to you due to your
current subscription type", which read as a hard block. Now that the AI
Assist add-on has been retired and AI features are included with every
paid plan, the warning should point users to the pricing page instead of
implying their plan can't access AI at all.
Keep the existing translation key for now — a follow-up can rename it
once #33624 (AI page CTA destination) is resolved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [web] Link the ai-assist-unavailable warning to the pricing page
* [web] Rename key `ai_assist_unavailable_due_to_subscription_type` -> `ai_assist_unavailable`
* [web] Update french and german translations
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: ae1319fa5b857d8f292de77c82ef0bda1c7ad144
* add script to finalise broken history-v1 chunks
* use history-id instead of project-id
* update project-id to history-id in tests
* silence unwanted event emitter warnings
* fix up test for historyId
GitOrigin-RevId: 58d2a768f1eff296e921e2ed985f6faf3929f619
* Allow admin access to user PATs
* Tests for new screen in admin panel
* Adding error for invalid token and way to parse error for OAuth 2
* Git bridge handles expired PAT
* Script for alerting on close to expiry and expired git tokens
* Refactoring and simplifying
* Updating email templates to match agreed docs
* tweak to email subject to include Overleaf
* Allowing dry run in scripts and general tidy up
* removing redundant tests and dry running script
* Fixing CI errors
* Adding new tab to admin test expectation
* Address PR feedback on oauth2-server changes
- Replace ad-hoc overleafErrorCode prop with a TokenExpiredError subclass
- Collapse listTokens/listTokensForAdmin into a single hook
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Adding cron definitions for alerting on expiring git pat
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
GitOrigin-RevId: 69b9fd901a201592a580c69abe7bd7d603e85d3a
Replace the six nested secretKeyRef env entries with a single
'envFrom: - secretRef: { name: verso-smtp, optional: true }' in both the
standalone app manifest and the prod workflow. Avoids the deep nesting that
tripped strict server-side decoding, and is simpler to edit. The secret's keys
must now be named exactly like the env vars (OVERLEAF_EMAIL_*).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add server-ce/k8s/verso-prod-data.yaml (Mongo + Redis) and
verso-prod-app.yaml (Verso app), mirroring the workflow so the verso
namespace can be bootstrapped/validated by hand.
- Drop 'kubectl create namespace verso' from the prod workflow (namespace is
pre-created), so the runner only needs namespaced rights in verso, matching
the test namespace.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add server-ce/k8s/verso-prod-pvcs.yaml (mongo-data/redis-data/verso-data,
ReadWriteOnce, storageClassName left for the operator to set — use a Ceph RBD
block class).
- Drop the inline PVC definitions from deploy-verso-prod.yml so it won't fight
the operator-provisioned PVCs; the deploy now assumes they already exist.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New .gitea/workflows/deploy-verso-prod.yml triggered by pushes to the 'prod'
branch — a real production target distinct from the ephemeral test rig:
- Runs in the 'verso' namespace; Mongo/Redis/app-data on PersistentVolumeClaims,
applied idempotently and NEVER deleted (data survives deploys).
- Replica set initialised only once; admin created only if no users exist.
- Builds/pushes verso:stable (separate tag from test's verso:latest);
imagePullPolicy Always so each rollout pulls the new build.
- SMTP via an optional 'verso-smtp' Secret (no credentials in the repo);
anonymous read-write sharing left off and public registration off
(friends-only).
- Example Ingress for verso.alocoq.fr at server-ce/k8s/verso-prod-ingress.example.yaml
(apply by hand to match the existing TLS/annotation setup).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- README: show the Verso wordmark logo instead of a text title.
- README: original Overleaf copyright now 2014-2026; Verso modifications 2026.
- Instance/version title: 'alpha' -> 'Alpha'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Fix the projects dashboard footer needing a scroll to reach: the main area
used min-height: 100% which always pushed the footer a full screen down.
Lay the content out as a flex column with main growing (flex: 1 0 auto), so
the footer sticks to the bottom of the viewport when the list is short.
- Bump the instance-name/version text to ~33px ('7.5', between font-size-07
and -08).
- Rewrite README to match the current triple-compiler product (Quarto + LaTeX
+ Typst), the editor language support, format badge, publishing flow and
Python venv option; drop the stale 'Quarto-only / TeX Live removed' notes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Reduce the dashboard instance-name/version font size (07 -> 06).
- Enlarge the Verso logo in the loading animation (160px -> 240px).
- Preserve the current RevealJS slide across recompiles: capture the deck's
URL hash (same-origin) and re-append it to the iframe src so the new build
reopens on the same slide instead of jumping to the start.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Hide the Present button when the current output is a PDF (it only makes
sense for HTML/RevealJS decks).
- Publish now supports PDF projects: snapshot output.pdf and serve it inline
via a small index.html wrapper at /p/:token, so link holders can view the
PDF straight from the published version.
- Add a Typst document outline (scans '=' headings) wired into the file
outline panel.
- Dashboard branding: enlarge the instance-name/version text and let the
sidebar Verso wordmark span the full column width.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* upgrade from eslint version 8 to eslint version 10
* remove unsupported eslint-env directive
* include jsx files in latexqc linting
* use basePath and extends to maintain paths in writefull eslint
* fix yarn.lock
with ./bin/yarn install
* preserve existing glob patterns in web eslint config
* restore original comments
* fix worker path
* corrected comment about eslint-plugin-mocha
* remove unused imports
* remove unused import of includeIgnoreFile
* switch to individual eslit.config.mjs files
* fix lint errors on eslint.config.mjs in web
* update build scripts for eslint.config.mjs
* update volumes for RUN_LINTING_CI_MONOREPO in web Makefile
updated manually as this makefile is not autogenerated
the RUN_LINTING_CI_MONOREPO command is only used for prettier, not eslint, but updating for consistency.
* migrate from mocha/no-skipped-tests to mocha/no-pending-tests
see https://github.com/lo1tuma/eslint-plugin-mocha/pull/365
"rule no-skipped-tests has been removed, its functionality has been merged into the existing no-pending-tests rule"
GitOrigin-RevId: 2c8f25c8049a0dba374a51df1214286bb5093a51
- Add a Typst language (stream highlighting + completions) for .typ, and
Quarto completions (code chunks, callouts, cross-refs) for .qmd/markdown.
- Project dashboard: new Format column (Quarto/Typst/LaTeX) from the cheap
project compiler field, surfaced through the projects list API.
- Compiler dropdown: grey out engines that don't match the root file's
extension (.qmd->Quarto, .typ->Typst, .tex->LaTeX engines).
- Replace the Overleaf fill loader with an animated Verso logo: the four
quadrant circles drift on their own orbits while colour warms up with load
progress; reused on the token-access screen too.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Instance name: stamp the nav title with the build number at deploy time
("Verso V0.<run> alpha") via a sed placeholder fed by GITHUB_RUN_NUMBER,
instead of the static "Verso V1.0 Alpha".
- Title typeface: self-host the EB Garamond latin subset (same one embedded in
the logo SVGs) and apply it to .navbar-title so the instance name matches the
Verso wordmark.
- Sidebar wordmark: let the logo fill the full sidebar column width (drop the
160px cap).
- Project filters: switch the ds-nav active state (filter selection + theme
toggle) from the green tokens to the blue scale, matching the rail.
- Present button: rename the presentation toolbar action from "Preview" to
"Present" / "Présenter" and add a tooltip explaining it publishes the
presentation and opens it in a new tab. New keys present /
present_publishes_and_opens_in_new_tab in en, fr and extracted-translations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Editor rail: the active item used the Overleaf green accent. Switch
--ide-rail-link-active-color/background to the blue scale (--blue-10/70,
--bg-info-03) to match the Verso palette.
- Footers: remove the default "Fork on GitHub!" right_footer item (redundant
with the "Built on Overleaf" link); right_footer now defaults to [].
- Login: move the hero wordmark into a full-width centered block and bump it to
max-width 480px so it's no longer constrained by the form column.
- Projects dashboard: restore the instance name in the top-left navbar (set
OVERLEAF_NAV_TITLE="Verso V1.0 Alpha") instead of the wordmark logo, and move
the full Verso wordmark to the sidebar's lower section (where the old
"Digital Science" mark sat). Revert HeaderLogoOrTitle to its title-first
behaviour now that the dashboard no longer passes a logo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The file-tree "Python packages" button rendered the literal text
"deployed_code" in the icon font because that glyph was missing from the
outlined/unfilled Material Symbols subset (MaterialSymbolsRoundedUnfilledPartialSlice.woff2),
so the ligature never resolved. The toolbar buttons all use the unfilled
variant, so switching this one to the full filled font would look inconsistent.
Add 'deployed_code' to unfilled-symbols.mjs and regenerate the subset woff2
(same Google Fonts request build-unfilled.mjs makes) so the box/package icon
renders, matching the other outlined toolbar icons.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three follow-ups after the visual-identity deploy:
- Footer: restore the React <Footer> on the projects dashboard (both
ProjectListDsNav and the legacy DefaultNavbarAndFooter). Removing it earlier
was an overcorrection — it now renders the Verso/AGPL thin footer rather than
the old "Powered by Overleaf" line. Other pages already kept the pug footer.
- Navbar brand: HeaderLogoOrTitle previously hid the logo whenever a nav title
was set, so on the dashboard only the "Verso" instance-name text showed and
the wired-up Verso logo never appeared. Make a configured logo (custom logo
or the Verso brand logo) take precedence over the title text; fall back to the
title only when no logo is provided (unchanged for other navbars).
- Login: enlarge the hero wordmark (max-width 260px -> 380px, full column width).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Build #78 failed in the compile step while Yarn Classic prepared the
@replit/codemirror-* git deps: fetching esbuild's per-platform binaries
returned truncated tarballs ("the file appears to be corrupt" / missing
.yarn-tarball.tgz). The tmpfs classic cache is fresh each build, so there is no
stale entry to blame and nothing to fall back to — it is a transient download
failure (builds #75-77 passed with an identical Dockerfile).
Wrap both the install and compile steps in a 3-attempt retry loop that wipes
the Yarn Classic cache (/usr/local/share/.cache/yarn) and re-fetches before
giving up, dumping pack.log on final failure. The persistent Berry cache and
YARN_NETWORK_CONCURRENCY=1 are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce the Verso brand marks as self-contained SVGs with the EB Garamond
latin subset embedded as a base64 @font-face, so they render identically in
every context (favicon, CSS background, <img>, inline) with no runtime Google
Fonts dependency — important for the self-hosted alpha. Falls back to Georgia
serif if a browser ignores SVG-embedded fonts.
Assets:
- verso-square.svg — rounded "V" tile (200×200); used as favicon.svg and the
editor top-left toolbar logo.
- verso-logo.svg / verso-logo-dark.svg — wide "verso · ONLINE EDITOR" wordmark
(760×200), light + dark wordmark variants.
Wiring:
- favicon: public/favicon.svg replaced with the square mark.
- editor toolbar: --redesign-toolbar-logo-url (light + dark) -> verso-square.svg.
- projects dashboard navbar: ProjectListDsNav logo -> verso-logo(.dark), with
--navbar-brand-width widened to 200px to fit the wide wordmark.
- login page: centered Verso wordmark above the form; suppress the top navbar
so the hero logo stands alone (no competing Overleaf mark).
PNG favicons / apple-touch-icon are left as-is (no raster tooling available);
modern browsers use the SVG favicon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#74 corrupted the persistent fallback cache again despite serialising the
fetch, so the cause isn't a write race: BuildKit evicts part of that persistent
cache mount between builds (the first build after each id bump always passed,
later ones failed). Mount /usr/local/share/.cache/yarn as tmpfs so it's clean
every build and nothing can be half-evicted; the Berry cache stays persistent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Swap the Overleaf marketing footer (careers, pricing, 'for universities', …)
shown on the login and other auth pages for a clean Verso footer: copyright
(Aloïs Coquillard -> alocoq.fr), 'Built on Overleaf' (-> Overleaf repo), and on
the right the AGPL licence (-> repo LICENSE) and Source code (-> the Gitea
repo). This also satisfies the AGPL source-offer on the public domain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an 'Add collaborators' heading above the email-invite section in the share
modal so it's visually distinct from the presentation-sharing section.
Add the missing French 'not_found' key so the 404 page shows 'Introuvable'
instead of the raw 'not_found' (the 404 pug template translates server-side;
the key existed in en.json but not fr.json).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a 'Python packages' button to the file-tree toolbar that opens a modal to
edit the project's requirements.vrf (one package per line, pip syntax), backed
by GET/POST /project/:id/python-requirements (read via ProjectEntityHandler,
write via EditorController.upsertDocWithPath, write-gated). The .vrf file is now
hidden from the file tree, so it is managed only through this editor rather than
appearing as a loose file. Adds python_packages / python_packages_help i18n.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Option A: when a {python} cell fails with ModuleNotFoundError/ImportError, the
log now suggests the exact PyPI package to add (with a module->package map, e.g.
cv2 -> opencv-python, sklearn -> scikit-learn), names the Verso requirements
file, and notes it could instead be a local module — so the langmuirthermalstudy
case isn't mistaken for a PyPI package.
Switch the per-project requirements file from requirements.txt to a Verso-
specific requirements.vrf (so it won't be confused with arbitrary .txt files);
QuartoRunner now looks for requirements.vrf, and 'vrf' is registered as an
editable text extension. The dedicated in-UI editor (and hiding it from the
file tree) follows in a separate change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The global python3 kernelspec hardcodes /usr/bin/python3, so even with
QUARTO_PYTHON pointing at the project venv, Quarto launched the kernel in the
base interpreter — packages installed into the venv (e.g. openpyxl) were not
importable. Register a python3 kernelspec inside the venv via
'ipykernel install --sys-prefix' (kernel.json argv -> the venv's python); since
Quarto runs kernel discovery through QUARTO_PYTHON, the venv's kernelspec is
found ahead of the global one and the kernel runs in the venv.
Bump the completion marker (.verso-complete -> .verso-ready) so venvs built
before this change are rebuilt with the kernelspec.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Base image: add opencv-python-headless (cv2) and tqdm to the bundled
scientific stack, and python3-venv (needed to build per-project venvs).
Per-project dependencies: a project's requirements.txt is now installed into a
venv cached by its sha256 (python3 -m venv --system-site-packages, so the
bundled stack stays visible and only extra packages are installed); QuartoRunner
points Quarto at it via QUARTO_PYTHON. A per-hash flock serialises concurrent
builds; pip output is merged into output.log; on failure the render falls back
to the base interpreter. Venvs live under PYTHON_VENVS_DIR
(default /var/lib/overleaf/data/python-venvs).
Gating: PythonVenvGate.userCanInstallPython restricts installs to the project
owner + invited collaborators (ignorePublicAccess excludes anonymous/link
users), threaded to CLSI as allowPythonInstall on the editor compile,
presentation export, and publish paths. Behind OVERLEAF_ENABLE_PROJECT_PYTHON_VENV
(enabled in the deployment). Design doc updated; Phase 2 (egress policy) and
Phase 3 (venv eviction) remain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The web build's 'yarn install' re-prepares the git-sourced @replit/codemirror-*
deps whenever the Berry cache misses (BuildKit GCs it between builds). Each
prepare uses Yarn Classic, which pulls every esbuild platform binary into the
single shared /usr/local/share/.cache/yarn folder; running several prepares in
parallel races and corrupts it ('tar content corrupt', EEXIST, missing
.yarn-tarball.tgz). Bumping the cache id only cleared it until the next
cache-miss build (#69).
Serialise Yarn's fetch with YARN_NETWORK_CONCURRENCY=1 on the install and
compile steps so the prepares no longer write that cache concurrently, and bump
the fallback cache id (v2 -> v3) once more to discard the currently-corrupt
cache. Slightly slower fetch, but no more random cache corruption.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Quarto's own jupyter wrapper (/opt/quarto/share/jupyter/jupyter.py ->
notebook.py) does 'from yaml import safe_load', so executing a {python} cell
failed with ModuleNotFoundError: No module named 'yaml'. The minimal jupyter
stack didn't pull PyYAML in (psutil/ipython already come via ipykernel), so
add pyyaml explicitly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Captures the proposed requirements.txt -> cached virtualenv approach (keyed by
hash, --system-site-packages, QUARTO_PYTHON), its guard rails (auth gating,
egress restriction, resource caps) given anonymous write is enabled, lifecycle
(eviction, failure UX), a phased rollout, and the open decisions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a {python} cell fails with ModuleNotFoundError/ImportError, the Quarto
log parser now emits an actionable error ('Python package "X" is not
installed on the server') noting which scientific packages are pre-installed,
instead of leaking an opaque traceback line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pre-install numpy, pandas, scipy, matplotlib, seaborn, scikit-learn, sympy,
plotly and tabulate so the common data-science libraries are available to
Quarto's Python code cells out of the box. matplotlib uses the headless Agg
backend automatically in the compile environment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Quarto executes ```{python}``` cells via a Jupyter kernel, but the base image
had no Jupyter ('Jupyter: (None)') and the runtime user (www-data) couldn't
create Quarto's log dir or Jupyter's runtime dir ('Permission denied: mkdir
/var/www/.local/...').
Install the headless Jupyter execution stack (jupyter-client, nbclient,
nbformat, ipykernel) for the system python3 Quarto uses, and register a
system-wide python3 kernelspec under /usr/local/share/jupyter. Also make
/var/www/.local writable by www-data so Quarto/Jupyter can write their
runtime/log files (mirrors the existing /var/www/.cache setup).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The web compile step failed packing the git-sourced @replit/codemirror-*
deps with 'tar content corrupt' / EEXIST / missing .yarn-tarball.tgz errors,
all under /usr/local/share/.cache/yarn/v6 — i.e. a corrupted BuildKit
fallback-cache mount (likely left half-written by an interrupted build), not
a code or dependency change. Bump the fallback cache id so BuildKit
allocates a fresh empty cache; the berry and webpack caches are untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The HTML/PDF export links were plain downloads that left the browser
silently spinning during the server-side render and, on failure, saved an
error page as pdf.txt/pdf.htm. Replace them with fetch-based downloads that
show a modal: a spinner with a 'this can take up to a minute' message while
compiling, and the actual compile log inline if the export fails. The user
can dismiss at any time; a stale request that finishes after dismissal no
longer reopens the modal. Adds the three i18n keys (en/fr + extracted).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
embed-resources cannot be enabled from the CLI: Quarto only honours it when
nested under the format, and a document's own format block fully overrides
project/CLI metadata (confirmed in Quarto docs). So --metadata embed-resources
was silently ignored and the 'standalone' HTML was the ordinary non-embedded
deck referencing a sibling _files/ dir — unstyled, no math, no images once
downloaded on its own.
For the html-standalone export, render a temporary copy of the root .qmd with
embed-resources/self-contained-math enabled and chalkboard disabled inside its
revealjs block (replacing an existing chalkboard key rather than duplicating
it), then clean the temp file up. Falls back to the original file if the deck
isn't an editable nested-revealjs document, so the export is never worse than
before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The slide-PDF export failed because the CLSI runtime user has no writable
HOME, so Chromium's crashpad couldn't create its database and the browser
died on launch ('chrome_crashpad_handler: --database is required'). Give
decktape's Chromium a fresh writable temp dir via HOME/XDG_*/--user-data-dir
(plus --disable-gpu).
The standalone-HTML export kept returning the old non-embedded file partly
because the GET response had no cache headers, so the browser served its
cached copy; add Cache-Control: no-store to both export responses. Also
switch the embed-resources flags to the long '--metadata KEY:VALUE' form
(the documented Quarto syntax) to remove any ambiguity vs the '-M' alias.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* [web] Fix footer For Students link to activate student toggle
The footer link only set itm_referrer plus a #student-annual hash. The
plans page reads the active plan/period from `plan` and `period` query
params (PlansHelper.getPlansPageViewOptions), so the student tab never
activated from the footer link.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* syncStudentModeFromPlanType after in handleDeprecatedHash
* Change URL update to use replaceState in the pricing page
* Revert "Change URL update to use replaceState in the pricing page"
This reverts commit eac71f193029e3f1c75e0c97261d8a5982c0d35c.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 69d689d0fe89fc68cefab9233739fc61da8f2ced
Insert a new "Do you offer discounts for nonprofits?" accordion item
under the educational group discount question in the "Overleaf
multi-license plans" FAQ tab. Routes the "contact sales" link through
the existing `faqContactLink` mixin so click tracking stays consistent
with the other FAQ contact links.
Closes#33494
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 582517f1d1f1f7934610253c252cf0f8af2b68a2
* [web] Stop bolding AI features unconditionally on the interstitial
The four `strong: true` flags on the AI features in `sectionMain2026`
caused those rows to render bold on every interstitial visit, regardless
of paywall context. The original intent (per Design QA #34022) was for
boldness to highlight the features relevant to the specific paywall the
user came from (e.g. AI paywall -> AI features bolded) — that
conditional logic was never wired up, and currently no `purchaseReferrer`
or paywall reason is plumbed through to the feature config.
Remove the unconditional `strong: true` so the cards render consistently
with the pricing page. Reintroduce conditional bolding in a follow-up
once the paywall→features mapping is scoped by design.
Closes#34022
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Remove `card-include-strong` and related code
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 2112214217f3b53d34518efbca546082ce559e26
* [web] Render "Try for free instead" CTA as link, not button
Design QA wants the "Try for free instead" CTAs on the pricing and
interstitial pages styled as marketing links (`link-monospace link-lg`)
rather than the current `btn-ghost` button. Add a `link` button type to
the `plans-cta` mixin that drops the `btn` class and applies the link
classes, and set `buttonType: 'link'` on the six `try_for_free_instead`
CTAs (plans-individual, plans-student, interstitial-payment).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Make link smaller
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: f911698a9bfa19f8180e58edb3cebcea90468cbd
* Add tests on plurals
* Update `collabs_per_proj` and its pluralisations
* Update `n_user` and its pluralisations
* Update `showing_x_results` and its pluralisations
* Update `show_x_more_projects` and its pluralisations
* `bin/run web npm run extract-translations`
* Populate `_plural` keys in non-en locales
For 2-form languages (da, de, es, fi, fr, it, nl, no, pt, sv, tr), copy
the existing bare-key value into the new `_plural` sibling to prevent
i18next from falling back to English for count!=1.
Also remove orphan singular keys (`collabs_per_proj_single`,
`showing_1_result*`) left over from the previous commits.
Bare-key values remain in their original plural form pending translator
review — count=1 will still render the plural form in non-en until
translators flip those to singular. Multi-form (cs, pl, ru) and
single-form (ja, ko, zh-CN, zh-TW) locales are unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Flip non-en bare-key values to singular form
Per review, the i18next v3 plural convention uses the bare key for count=1
(singular) and `_plural` for count!=1. The non-en bare-key values were
left as the original plural form by the previous commit so the `_plural`
siblings could be copied from them; this commit flips the bare values to
the singular form per language.
Languages where singular and plural noun forms coincide (Finnish, Swedish,
Turkish) are unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Apply suggestions from code review
Co-authored-by: Olzhas Askar <olzhas.askar@gmail.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Olzhas Askar <olzhas.askar@gmail.com>
GitOrigin-RevId: 628513ca792c2dcce023247e52b7320e2741cc54
* move Review Toggle into the toolbar
* cleaning up and adding a comment
* adding the cursor styling
* adding isolation on writefull toolbar to adjust z-index of writefull toolbar
* fixing the dark mode colours for review dropdown trigger
* Fix review mode switcher dark mode styles
GitOrigin-RevId: 36847e0debdc4dce5f96492261d25e7cc46b2e96
The standalone-HTML export produced a non-self-contained file (no slide
CSS/JS, math or images when opened away from the server) because Quarto's
--metadata/-M flag uses KEY:VALUE (colon), not KEY=VALUE. '-M
embed-resources=true' silently registered a bogus key and left
embed-resources unset. Switch to colon syntax and also embed MathJax
(self-contained-math:true) so equations render offline.
For the slide PDF, add --disable-dev-shm-usage (the usual cause of
Chromium crashing inside a container with a small /dev/shm), and have the
export controller return the compile log as text/plain on failure so a
failed PDF export shows the real decktape/Chromium error instead of an
HTML page the browser saves as 'pdf.htm'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In RevealJS mode the download button becomes a 2-choice menu:
- Standalone HTML: a one-off compile with embed-resources (chalkboard and other
runtime-only plugins are dropped, since they don't survive self-containment),
yielding a single portable .html.
- Slide PDF: render the deck, then print it with decktape (headless Chromium)
to a faithful one-slide-per-page PDF.
Implementation:
- Dockerfile-base: install decktape + headless Chromium (open-source; deps via
playwright install-deps for Ubuntu-Noble correctness). Base-only change.
- QuartoRunner honours options.exportMode ('html-standalone' | 'pdf-slides');
exportMode is threaded web ClsiManager -> CLSI RequestParser -> CompileManager
-> runner.
- New GET /project/:id/presentation-export/:format compiles in the matching
export mode and streams the result as a download (PresentationExportController,
reusing ClsiManager.getOutputFileStream).
- pdf-hybrid-download-button shows the dropdown when the output is output.html;
PDF/LaTeX projects keep the single download button.
- i18n: download_as_standalone_html / download_as_pdf_slides (en + fr +
extracted-translations.json).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Deployment: set OVERLEAF_SITE_LANGUAGE=fr so the UI defaults to French.
- fr.json: add French translations for the Verso strings — blank_/example_
{quarto,latex,typst}_project, share_compiled_presentation(_info),
presentation_link_{members,private,public}, reset_link, and preview (which
was missing from fr.json). Other untranslated keys keep falling back to
English via the translations-loader.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The frontend bundles only the locale keys listed in
frontend/extracted-translations.json (a custom webpack translations-loader
filters en.json to that set, normally regenerated by i18next-scanner). Every
key added by hand to en.json without also adding it here renders as its raw
key — which is why "blank_quarto_project", "share_compiled_presentation", etc.
showed up literally in the New-project menu and Share dialog.
Add all introduced keys to extracted-translations.json: blank_/example_
{quarto,latex,typst}_project, share_compiled_presentation(_info),
presentation_link_{members,private,public}, reset_link.
Also enable anonymous read-AND-write share links (edit without an account) via
OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING; read-only links already worked
through OVERLEAF_ALLOW_PUBLIC_ACCESS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a project-members-only link tier and independent link rotation.
- Three tokens per project instead of two: publicToken (anyone), loginToken
(any logged-in user), memberToken (only users who can read the project).
serve() resolves the token to its tier and enforces accordingly — 'member'
requires AuthorizationManager.canUserReadProject.
- New POST /project/:id/publish-presentation/regenerate { tier } rotates a
single tier's token (invalidating only that old link), leaving the snapshot
and the other links intact.
- Share dialog now shows three links (members / logged-in / anyone), each with
its own Copy and Reset buttons; Publish refreshes, Unpublish removes all.
Preview button opens the logged-in-users link.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The web service installs a site-wide login gate (router.mjs: webRouter.all('*',
requireGlobalLogin)) whenever Settings.allowPublicAccess is false — which it was,
since OVERLEAF_ALLOW_PUBLIC_ACCESS wasn't set. That gate bounced every anonymous
request to /login, breaking both Overleaf's own link-sharing and the public
presentation links (the dynamic token routes can't be in the exact-match
global whitelist, so there's no per-path exemption — allowPublicAccess is the
intended knob).
Set OVERLEAF_ALLOW_PUBLIC_ACCESS=true on the verso Deployment. Per-project and
per-route authorization still applies, and private presentation links still
require a login (enforced in the serve handler), so only genuinely public
content is reachable anonymously.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the single token + visibility toggle with two stable tokens per project
pointing at the same snapshot:
- publicToken → anyone with the link
- privateToken → any logged-in Verso user
This fixes both reported issues: changing visibility no longer mutates a link
(there's no toggle — both links always exist), and a public link can never
become private by accident. It also fixes public links redirecting to login:
access is now decided purely by which token was used (public token = open),
not a per-record flag.
- Model: storageId (snapshot dir) + publicToken + privateToken; drop token/
visibility.
- Manager.publish: mints both tokens once and reuses them on re-publish; serve
resolves a token to its record and treats the public token as open.
- Controller: returns { publicUrl, privateUrl }.
- Share dialog: shows the private and public links side by side, each with its
own copy button; Publish refreshes, Unpublish removes. Preview button opens
the private link.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A deck served at /p/:token (no trailing slash) made the browser resolve its
relative asset references (main_files/... CSS+JS) against /p/, 404ing them —
so the deck rendered as unstyled HTML with no reveal.js. Publish links now end
in a slash, and the bare /p/:token URL 301-redirects to /p/:token/, so relative
assets resolve under /p/:token/ and load correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The default published-presentations folder resolved to the app dir
(/overleaf/services/web/data/published), which isn't writable by the runtime
user → EACCES on publish. Point it at the Overleaf data volume in the
production config (Path.join(DATA_DIR, 'published') = /var/lib/overleaf/data/
published), alongside compiles/output, where the app user can write (and which
persists when a volume is mounted). Overridable via PUBLISHED_PRESENTATIONS_PATH.
CompileManager.compile debounces compiles via a Redis key set on every compile
(_checkIfRecentlyCompiled), returning {status:'too-recently-compiled',
outputFiles:[]} when the editor has just auto-compiled. Publishing called
compile() and then required output.html, so it threw "did not produce an HTML
presentation" — which is why Preview/Publish errored whenever the deck was
freshly compiled.
- CompileManager.compile: honour options.bypassRecentCompileCheck to skip the
debounce (still runs the normal autocompile-limit guards).
- PublishedPresentationManager: publish with bypassRecentCompileCheck, and put
the compile status in the error message for diagnosis.
- Controller: catch publish errors, log them, and return the message so the
Share dialog can show what went wrong instead of a generic error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wires the two entry points to the publishing backend:
- Share dialog: a "Share compiled presentation" section (owner only) with a
public / logged-in-users-only choice, Publish/Unpublish, and a copyable link.
- Top-right toolbar: a "Preview" button that publishes a private (logged-in-
users-only) link in one click and opens the standalone deck in a new tab
(opened synchronously to dodge popup blockers).
Both talk to /project/:id/publish-presentation. Reuses existing i18n
(publish/unpublish/copy/preview); adds share_compiled_presentation(_info) and
presentation_link_public/private.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the engine + API for publishing a project's compiled HTML/RevealJS deck as
a stable, standalone snapshot served at /p/:token, independent of the editor.
- PublishedPresentation model: one per project { token, visibility, buildId },
re-publishing keeps the same token so shared links stay stable.
- Manager.publish: compiles the project, then copies the HTML deck + its _files
assets + referenced media (now included thanks to the OutputFileFinder fix)
into a persistent snapshot dir (Settings.path.publishedPresentationsFolder,
override with PUBLISHED_PRESENTATIONS_PATH). Logs/aux are excluded.
- Routes: GET/POST/DELETE /project/:id/publish-presentation (owner/reader) for
status/publish/unpublish; public GET /p/:token(/*) serves the deck full-page.
Visibility is enforced in the handler: 'public' = anonymous, 'private' = any
logged-in Verso user. CSP is dropped on these responses so reveal.js renders.
Frontend entry points (share-modal section + top-right Preview button) follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OutputFileFinder excluded all incoming project resources from the output set,
and OutputCacheManager only copies outputs into the served build dir. For PDF
that's fine (media is embedded), but for HTML/RevealJS the browser fetches
images/videos/fonts from the output path at runtime — so a deck's referenced
image (a project input file) was never served and rendered broken in the
preview.
When the compile produced output.html, keep media inputs (img/video/audio/font
extensions) in the output set so they're served alongside the deck. PDF/LaTeX
compiles are unaffected. This also makes referenced media land in output.zip,
which the upcoming presentation-publishing feature relies on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Since we dropped --embed-resources (so RevealJS plugins like chalkboard work),
pandoc no longer tries to fetch referenced media for HTML output, so a missing
image or video produces no compile-time warning — it only renders broken in the
browser. PDF/Typst output is unaffected because Typst hard-errors on a missing
image.
After an HTML render, QuartoRunner now scans output.html for local media
references (img/video/audio/iframe src, poster, RevealJS data-background-*) and
appends a `[WARNING] Missing resource: …` line to output.log for any that don't
exist on disk. External URLs, data URIs, anchors and Quarto's own generated
<basename>_files assets are ignored. The [WARNING] prefix is recognised by the
Quarto/Typst log parser, so these show up in the Warnings tab.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Project.compiler defaults to settings.defaultLatexCompiler ('quarto' in this
fork), so every .tex project carried compiler='quarto'. Since the CLSI runner
is chosen by file extension, a .tex root still goes to LatexRunner, whose
_buildLatexCommand threw `unknown compiler: quarto` — surfacing as an opaque
HTTP 500 with no compile log.
- LatexRunner: fall back to pdfLaTeX when the compiler isn't a known TeX engine
instead of throwing. Universal safety net (covers existing projects, uploads
and GitHub imports already saved with compiler='quarto').
- ProjectCreationHandler: store a sensible compiler per flavour at creation via
a shared _flavourConfig helper — blank/example LaTeX → 'pdflatex',
Typst → 'typst', Quarto → 'quarto' — so the compiler dropdown reflects the
engine and LatexRunner receives a valid one directly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A project whose root file is a .typ file now compiles straight to PDF with
Typst, as a third engine beside Quarto (.qmd) and latexmk (.tex). Dispatch
stays purely extension-based.
CLSI:
- New TypstRunner.js: runs `quarto typst compile <main>.typ output.pdf` (reuses
the Typst bundled in Quarto, so no extra binary / Docker change). stderr is
merged into output.log.
- CompileManager: _isTypstFile + a TypstRunner branch in _getRunner, and
TypstRunner added to the isRunning check and stopCompile kill list.
- RequestParser: 'typst' added to VALID_COMPILERS.
web:
- settings.defaults: 'typ' added to validRootDocExtensions and the text
extensions (so .typ opens in the editor); 'typst' added to safeCompilers.
- output-files: the Quarto/Typst log parser (which already understands Typst
`error:`/`warning:` + `┌─ file:line:col` diagnostics) now also handles .typ
compiles, so their errors/warnings populate the log tabs.
Polish:
- New-project menu: "Blank Typst project" + "Example Typst project" in both the
main and welcome dropdowns, backed by createBasicProject/createExampleProject
flavour 'typst', a new mainbasic.typ template and an example-project-typst
presentation (math, an image, a table, lists).
- Compiler dropdown gains a "Typst" option (cosmetic; dispatch is by extension).
README updated: three compilers side by side, with a Writing-a-Typst-document
section.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the generic "Blank project" / "Example project" entries with four
flavour-specific ones in both the New-project dropdown and the welcome-screen
dropdown:
- Blank Quarto project -> empty main.qmd (format: typst)
- Blank LaTeX project -> empty main.tex
- Example Quarto project -> a Reveal.js presentation showcasing images, math,
a table, code and incremental lists (new template
project_files/example-project-quarto/)
- Example LaTeX project -> the existing LaTeX example
Backend: ProjectController.newProject now dispatches the `template` value
(blank_quarto/blank_latex/example_quarto/example_latex, plus the legacy
'example'/'none') to createBasicProject(flavour) / createExampleProject(flavour).
_createRootDoc takes a root-doc name so each flavour gets the right extension —
this also fixes the LaTeX example, whose root doc was wrongly created as
main.qmd, back to main.tex (matching the acceptance test). Signatures stay
backward compatible (flavour defaults: blank=quarto, example=latex).
Also refresh the README: Verso now runs Quarto and LaTeX side by side
(engine chosen by root-file extension), not Quarto instead of LaTeX.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The $accent knob caught primary buttons, but several places still
referenced the green ramp directly as a brand-accent colour (rather than
genuine success semantics). Repoint those at the --bg-accent-* tokens so
they too follow the single $accent knob:
- navbar Sign in / Register ("primary" + subdued/link hover) buttons
- file-tree selected-item highlight and drag background (IDE redesign,
light and dark)
- document-outline highlighted item (IDE redesign, light and dark)
- the Visual/Code editor-switcher button mixin
- web/content hyperlinks (--link-web*), e.g. on the project dashboard;
dark-theme variants point at the blue ramp to stay readable on dark
Genuine success/positive greens (notification success icon,
$content-positive, beta badges, etc.) are deliberately left green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two build-speed changes to the Gitea Actions deploy workflow.
(#1) Build the base image only when it changes. The base layers' only
repo input is server-ce/Dockerfile-base, so the prepare step hashes that
file and the base is tagged verso-base:base-<hash>; the app builds FROM
that exact tag. If a base with the current hash already exists in the
registry, the heavy base build (apt ~111s, TeX Live ~51s, Quarto, plus
its ~49s export/push) is skipped entirely — which is every commit that
doesn't touch Dockerfile-base.
(#2) Import/export a registry-backed layer cache (verso-cache:base and
verso-cache:app, mode=max) on both builds. Unchanged layers are reused
instead of rebuilt: yarn install is skipped when package.json is
unchanged, and only the web compile re-runs on a frontend source change.
No new cluster resources — the cache lives as extra tags in the same
in-cluster registry.
First run after this is still a full build (populates the caches and the
hash-tagged base); subsequent commits should be substantially faster.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Quarto compiles (.qmd/.md/.Rmd, dispatched to QuartoRunner) write
Typst/Pandoc/Quarto diagnostics to output.log that the LaTeX log parser
does not understand, so the Errors/Warnings tabs stayed empty. Add a
dedicated quarto-log-parser that recognises Typst `error:`/`warning:`
(+ `┌─ file:line:col`), Pandoc `[WARNING]`/`[ERROR]`, Quarto CLI/Deno
`ERROR:`/`WARNING:`, and knitr `Quitting from lines`. handleLogFiles now
routes to it when the root file is a Quarto file (mirrors CLSI dispatch),
otherwise the LaTeX path is unchanged.
Also decouple the UI accent from the green ramp. The framework already
funnels every primary/accent surface (primary buttons, Bootstrap
$primary/$success, --btn-primary-background) through the --bg-accent-*
tokens; those just happened to point at Overleaf green. Introduce a
single $accent knob in foundations/colors.scss (with auto-derived
hover/tint shades) and repoint the accent tokens at it, defaulting to
the Verso/Quarto blue. Re-skinning the whole UI is now a one-line edit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two HTML/RevealJS preview fixes:
1. Stop passing --embed-resources to quarto render. A self-contained
single-file HTML breaks reveal.js plugins that load/store resources at
runtime (chalkboard, multiplex) and is slow to transfer. Quarto now
emits the HTML plus a sibling "<basename>_files/" asset dir referenced
by relative paths; both are served from the same .../output/ path
(nginx output/(.+) and web :file(.*) both capture slashes), so the
relative links resolve. The renamed output.html still points at the
unchanged "<basename>_files" dir. This also fixes the slow-load issue,
since assets now load on demand instead of one giant inlined file.
2. On a failed compile that follows a successful one, the previous deck
stayed in the iframe, making the failure look like a success. We now
clear pdfFile when a non-success status carries a stale output.html.
The last-good-PDF-beside-the-error behaviour is preserved for PDF
output (only output.html is dropped).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two fixes to the Markdown/Quarto file outline:
1. The last frontmatter line (e.g. `format: typst`) appeared as a
heading. The Lezer Markdown grammar has no frontmatter support, so it
reads the closing `---` of the YAML block as a Setext underline and
promotes the line above it to a heading. Detect the leading
`---`...`---`/`...` block and skip any heading inside it.
2. Pandoc/Quarto attribute blocks were shown in titles, e.g.
`## Slide {.smaller auto-animate="true"}`. Strip a trailing `{...}`
from the extracted title.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The outline entries showed up at the right lines and jumped correctly,
but their titles were blank. The text-extraction walked the heading
node's children and collected non-HeaderMark child text — but in the
Lezer Markdown grammar a heading has NO child node for its text; the
only children are the HeaderMark nodes. The title text lives in the
gaps between marks, so the walk collected nothing.
Slice the whole heading's source instead and strip the markers:
leading/trailing '#'s for ATX headings and the '==='/'---' underline
for Setext headings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reverts the heavy multi-collection texlive install back toward the
original upstream-Overleaf approach: install-tl with scheme-basic
(~300 MB) plus latexmk and texcount via tlmgr, no docfiles/srcfiles.
This restores the fast, small base image we had before LaTeX support
was added in full.
Tradeoff: documents needing tikz/beamer/siunitx/extra fonts won't
compile out of the box for now — those should stay in Quarto/Typst
until the project is mature enough to justify a full TeX Live.
Made deliberately easy to reverse: a header comment documents that
switching scheme-basic -> scheme-full (one line) restores the complete
toolchain, or individual packages can be appended to the tlmgr list.
Uses TEXDIR=/usr/local/texlive (unversioned) so PATH stays stable
across TeX Live releases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous install expanded texlive-full (minus -doc/-lang-), pulling in
essentially every CTAN package plus inkscape's large GTK GUI tree — ~20 min
and several GB. Replace it with a curated set of meta-packages that covers
the vast majority of documents: latex base/recommended/extra, recommended
fonts, plain-generic, science (math/physics), xetex, luatex, bibtex-extra,
extra-utils (texcount), plus latexmk/biber/chktex/pygments.
Smaller and faster to build. Documents needing an omitted package can have
the relevant texlive-* collection added back. Drops inkscape (only used for
auto SVG->PDF conversion) to avoid its heavy GUI dependency chain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cluster nodes' containerd can only pull from registry.alocoq.fr, not
the in-cluster service name. Keep pushing via the in-cluster address (to
bypass the Traefik upload-timeout), but reference registry.alocoq.fr/verso
in the test Deployment and the rolling update. Both addresses front the
same registry storage, so the pushed image resolves at the public name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous approach created a verso-buildkitd-config ConfigMap, but the
workflow's RBAC does not permit creating new cluster resources. Write the
buildkitd.toml (marking the in-cluster registry as http/insecure) directly
inside the buildkit container at runtime via printf, and drop the configMap
volume/mount. No new k8s resources are created.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The TeX Live layer (~3.5 GB) failed to push to registry.alocoq.fr:
Traefik severed the upload mid-stream ("client disconnected during blob
PUT ... unexpected EOF"), buildkit retried at the wrong offset, and the
registry returned "blob upload invalid".
Push to the in-cluster registry Service (registry.git.svc.cluster.local:5000)
instead, so the upload never traverses Traefik. Changes:
- buildctl outputs use registry.insecure=true (registry is plain HTTP)
- add a verso-buildkitd-config ConfigMap with buildkitd.toml marking the
registry http/insecure, so the second build can pull the base image back
- the verso Deployment and rolling update reference the in-cluster image
NOTE: the cluster nodes' containerd must also treat
registry.git.svc.cluster.local:5000 as an insecure registry, otherwise
the kubelet image pull for the test deployment will fail. That is node-
level config outside this repo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The base image build failed with "E: Unable to locate package texcount".
texcount ships inside texlive-extra-utils, not as its own apt package.
Replace the bogus texcount entry with texlive-extra-utils (which provides
both texcount and latexmk). latexmk is kept explicit for clarity.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both limits that gate uploads are bumped in tandem so they don't conflict:
- settings.defaults.js maxUploadSize: 50 MB → 500 MB (app-level check)
- nginx.conf.template client_max_body_size: 50m → 500m (proxy body limit)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verso now compiles both .tex (latexmk) and .qmd (Quarto) projects,
dispatching by the root file's extension rather than replacing one with
the other. LaTeX and Quarto projects can coexist on the same server.
CompileManager: re-import LatexRunner and add a _getRunner() dispatcher
that returns a uniform {run, isRunning, kill} interface. .qmd/.md/.Rmd
→ QuartoRunner; everything else (.tex/.ltx/.Rtex/.Rnw) → LatexRunner.
stopCompile now checks/kills both runners since it has no root path.
compiler-setting.tsx: restore the LaTeX engine choices (pdfLaTeX, LaTeX,
XeLaTeX, LuaLaTeX) alongside Quarto. The dropdown still controls which
TeX engine latexmk uses; actual engine dispatch is by file extension.
Dockerfile-base: reinstall TeX Live alongside Quarto (texlive-full minus
-doc/-lang- packages, plus xetex/luatex/biber/latexmk/texcount/chktex/
synctex). Restore TEXMFVAR for a writable LuaTeX cache. This brings back
a large image, which is the accepted cost of full LaTeX+Quarto support.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RevealJS presentations are served as (currently embed-resources) HTML that
went over the wire uncompressed, because gzip_types only listed text/plain.
This made the HTML preview slow to load for heavy decks.
Add text/html, text/css, application/javascript, application/json and
image/svg+xml to gzip_types so the text-based portion of the output is
compressed. Already-compressed formats (pdf, png/jpeg/webp, woff/woff2)
are intentionally excluded to avoid wasting CPU. Also set gzip_min_length
1024 so tiny responses aren't compressed needlessly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dockerfile-base: after Quarto is installed, run 'quarto add --no-prompt'
for a curated set of extensions into /opt/quarto-extensions/. Quarto
writes _extensions/<author>/<name>/ in the working dir, giving us a
clean shared store. Extensions included:
- igorlima/charged-ieee — IEEE paper format (Typst)
- quarto-ext/fontawesome — Font Awesome icons
- quarto-ext/attribution — attribution footer on RevealJS slides
- quarto-ext/pointer — laser pointer for presentations
- quarto-ext/drop — drop-down overlay for RevealJS
Adding more: one extra '&& quarto add --no-prompt <author>/<repo>' line.
QuartoRunner: before quarto render, merge /opt/quarto-extensions/_extensions/
into the compile dir's _extensions/ with 'cp -rn' (no-clobber). This
makes all pre-installed extensions available to every project without
any user action. Project-uploaded _extensions/ files take precedence
since cp -n never overwrites existing files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace Overleaf's original README with a Verso-specific one covering:
the project's purpose (collaborative Quarto editor), output formats
(typst/PDF and revealjs/HTML), quick-start Docker instructions, service
architecture overview, a minimal .qmd example, key env vars, and a
clear description of what differs from upstream Overleaf.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
collectOutputPdfSize() only calls stat() and sets .size on output.pdf.
All other output files (including output.html) keep size: undefined.
The previous check required file.size > 0 for both PDF and HTML, so
undefined > 0 always evaluated false for output.html, making every
RevealJS compile report 'failure' even when the file was produced.
Fix: require size > 0 only for output.pdf; accept output.html
regardless of size (it is always non-empty if Quarto succeeded).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LocalCommandRunner.replace() uses String.replace() which only substitutes
the FIRST occurrence of '$COMPILE_DIR' in the shell script string. The mv
commands had two more occurrences that stayed as literal '$COMPILE_DIR',
which the shell expanded to '', making 'mv /main.pdf /output.pdf' fail
silently. The file was produced (Quarto logged 'Output created: main.pdf')
but never renamed to output.pdf, so the pipeline reported failure.
Fix: mv uses relative filenames since the shell CWD is already the compile
directory (set by LocalCommandRunner via the spawnCwd option).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
clsi-nginx.conf: the types{} block was overriding all nginx defaults,
leaving HTML/CSS/JS/fonts as application/octet-stream. Add the full
set of web MIME types so RevealJS assets are served correctly. Also
needed for X-Content-Type-Options: nosniff to pass.
CompileController.js: success was hardcoded to require output.pdf.
Also accept output.html so a RevealJS compile is reported as
'success' rather than 'failure'.
QuartoRunner.js: remove hardcoded --to typst --output output.pdf.
Instead run `quarto render` without --to/--output so the YAML
frontmatter decides the format (typst → PDF, revealjs → HTML, etc.).
Pass --embed-resources so HTML output is self-contained (flag is
silently ignored by the typst backend). After render, rename
main.pdf → output.pdf or main.html → output.html so the pipeline
finds the standard canonical filename.
output-files.ts: handleOutputFiles now falls back to output.html when
output.pdf is absent. Download URL uses outputFile.path instead of
the hardcoded 'output.pdf' string.
pdf-viewer.tsx: when pdfUrl contains output.html, bypass PDF.js
entirely and render a sandboxed iframe (allow-scripts for RevealJS
interactivity, allow-presentation for fullscreen).
Usage: set `format: revealjs` in the .qmd YAML frontmatter to get
an HTML presentation preview; set `format: typst` for PDF.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
outline.ts: export NestingLevel so it can be used outside the file.
markdown/document-outline.ts: new enterMarkdownNode function that walks
the Lezer Markdown syntax tree and extracts ATXHeading1-6 and
SetextHeading1-2 nodes, mapping them to the same NestingLevel enum
used by the LaTeX outline (Section→SubSection→SubSubSection…).
Wrapped in makeProjectionStateField for incremental updates.
markdown/index.ts: register markdownDocumentOutline as a CodeMirror
extension in the Markdown LanguageSupport so the StateField is active
whenever a .qmd file is open.
codemirror-outline.tsx: fall back to markdownDocumentOutline when the
LaTeX documentOutline StateField is not present in the editor state
(i.e. when the active language is Markdown, not LaTeX).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous compile logs confirm Quarto handles --to typst --output
output.pdf correctly: pandoc produces main.typ, typst compiles it to
main.pdf, then Quarto renames to output.pdf. The mv-based approach was
unnecessary and incorrect.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--to typst combined with --output output.pdf caused Quarto to write a
Typst source file (.typ content) named output.pdf instead of invoking
the typst compiler, producing a text file that the PDF viewer could not
render (hence 'markdown not rendered' — it was literally showing the raw
.typ markup). Fix: let Quarto name the PDF after the input file
(main.qmd → main.pdf) and rename to output.pdf with mv afterwards.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Quarto resolves its cache dir as $HOME/.cache/quarto. The process runs
as www-data (home=/var/www) but that directory is root-owned, so Quarto
crashed immediately with PermissionDenied on mkdir. Pre-create the cache
dir and chown it to www-data at image build time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LocalCommandRunner: attach captured stdout to the error object when
exit code is 1, so callers can read Quarto's output even on failure.
QuartoRunner: stop propagating plain 'exited' errors from Quarto up
to CompileManager. A Quarto exit-code-1 is a compile failure, not a
server error — CLSI already detects failure by the absence of
output.pdf and returns status='failure' (HTTP 200). Previously it
fell through to the generic error handler (HTTP 500), which caused
the frontend to show "Server Error" instead of the log panel.
Only true process-level errors (terminated, timedout) are propagated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dockerfile-base: remove TeX Live (no longer needed), install Quarto
1.6.39 which bundles Typst for PDF output. This was the root cause
of all compile failures — the server-ce monolith never had Quarto.
QuartoRunner: run quarto via /bin/sh so stderr is merged into stdout
with 2>&1; write combined output to output.log (not output.stdout)
so the PDF-preview log panel picks it up and shows raw output.
Also write the log on error so failures are always visible.
CompileManager: guard DraftModeManager behind an isLatexFile check —
injecting LaTeX preamble commands into a .qmd file corrupts it and
causes a guaranteed compile failure when draft mode is requested.
ProjectCreationHandler + mainbasic.qmd: new projects now create
main.qmd with a minimal Quarto/Typst frontmatter instead of the
LaTeX main.tex; _createRootDoc names the file main.qmd accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- compiler-setting.tsx: replace hardcoded LaTeX compiler list with a
single Quarto option; drop now-unused getMeta/lodash imports
- project-settings.ts: add 'quarto' to ProjectCompiler union type
- ClsiManager: detect main.qmd as a default root document (preferred
over main.tex); replace hasMainFile boolean with detectedMainFile
so we know which filename to use
- settings.defaults.js: add 'qmd' to validRootDocExtensions so .qmd
files appear as selectable root documents in the UI
- ProjectRootDocManager: sort main.qmd before main.tex in the root
doc candidate list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add QuartoRunner.js: runs `quarto render --to typst --output output.pdf`,
using Typst (bundled with Quarto >= 1.4) so no separate LaTeX install needed
- Swap LatexRunner for QuartoRunner in CompileManager; remove latexmk-specific
stats, fdb metrics, and performance sampling that no longer apply
- Add 'quarto' to VALID_COMPILERS in RequestParser and set it as the default;
change default rootResourcePath from main.tex to main.qmd
- Add 'quarto' to safeCompilers and set it as the default in web settings
- Replace with-texlive Dockerfile stage with with-quarto (Quarto deb install);
add Quarto to the default final stage as well
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
<img src="doc/screenshot.png" alt="A screenshot of a project being edited in Overleaf Community Edition">
<p align="center">
Figure 1: A screenshot of a project being edited in Overleaf Community Edition.
</p>
**A collaborative, real-time editor for LaTeX, Quarto and Typst — self-hosted.**
## Community Edition
---
[Overleaf](https://www.overleaf.com) is an open-source online real-time collaborative LaTeX editor. We run a hosted version at [www.overleaf.com](https://www.overleaf.com), but you can also run your own local version, and contribute to the development of Overleaf.
## What is Verso?
Verso is a fork of [Overleaf](https://github.com/overleaf/overleaf) that extends its
collaborative editing infrastructure to support [Quarto](https://quarto.org) and
[Typst](https://typst.app) projects alongside LaTeX. Think of it as Overleaf, but
not limited to LaTeX.
### Verso vs Overleaf
[Overleaf](https://www.overleaf.com) is the gold standard for collaborative LaTeX
editing. Verso keeps everything that makes Overleaf great — real-time co-editing,
operational-transformation history, auth, project management, file storage — and
adds:
- **Quarto and Typst compilers** running alongside TeX Live, dispatched
automatically from the root file's extension (`.qmd` → Quarto, `.typ` → Typst,
`.tex` → `latexmk`).
- **Language-aware editor** for Quarto and Typst (syntax highlighting, completions,
document outline) — not just LaTeX.
- **Publish & share compiled output** (`/p/:token` with tiered access links) — a
feature absent from Overleaf Community Edition.
- **Lumière theme** — a redesigned project dashboard and editor chrome with a
card-based grid, thumbnails, and a teal gradient identity.
- **Full i18n** — French, German, Italian, and Spanish UI translations on top of
Overleaf's English base.
- Completely **free and self-hosted**; no Overleaf subscription required.
### Verso vs Quarto
[Quarto](https://quarto.org) is a command-line tool: you install it locally, write
`.qmd` files in any text editor, and run `quarto render` in a terminal. It is
excellent for solo authors with full control over their environment.
Verso wraps Quarto in a collaborative web editor:
- **No local install** — Quarto, Typst, TeX Live and Python run on the server.
- **Real-time collaboration** — multiple people edit the same `.qmd` simultaneously
with live cursors and conflict-free merging.
- **Not just Quarto** — LaTeX and Typst projects live in the same workspace, under
the same auth and history system.
- **Publish in one click** — RevealJS decks and PDFs are served at a stable link
without leaving the browser.
Verso is not a replacement for Quarto's CLI — it is a platform that makes Quarto
accessible as a shared, always-on service.
### Verso vs Typst.app
[Typst.app](https://typst.app) is a cloud-hosted web editor for Typst. It is
polished and fast, but it is a proprietary SaaS product and only supports Typst.
Verso differs in that:
- It is **self-hosted** and open-source (AGPL v3) — you control your data.
- It supports **three languages** (Typst, LaTeX, Quarto) in one instance.
- Real-time collaboration is powered by **operational transformation** (the same
engine as Overleaf), not CRDTs, which means it handles concurrent edits
gracefully for long documents.
- It ships with a full **project history** and version-restore workflow.
If you only need Typst and want a lighter, Typst-focused alternative, have a look
at **[Collabst](https://github.com/herluf-ba/collabst)** — an open-source,
self-hosted collaborative Typst editor that is independent of the Overleaf
codebase and shows a lot of promise.
---
## Features
- **Real-time collaboration** — multiple editors, live cursors, full project history and version restore.
- **Three compilers, auto-dispatched** by root file extension:
| Root file | Compiler | Typical output |
|-----------|----------|----------------|
| `.qmd` | Quarto | PDF (via Typst or LaTeX), HTML, or RevealJS |
| `.tex` | `latexmk` / TeX Live | PDF |
| `.typ` | Typst | PDF |
- **Language-aware editor for all three** — syntax highlighting, completions, and a document outline panel for LaTeX, Quarto and Typst.
- **Format badge** on the project dashboard; compiler dropdown greys out inapplicable engines.
- **Publish & share** — compile and snapshot to `/p/:token` with three independent access tiers (project members / any logged-in user / public). HTML/RevealJS decks are served live; PDFs are embedded inline. A **Present** toolbar button links directly to the published deck.
- **RevealJS thumbnails** — the first slide of a presentation is rendered as a preview card in the project list.
- **Quarto Python cells** — optional per-project virtual environment built from `requirements.txt`, so Python code chunks execute during render.
- **Visual formatting toolbar** — bold, italic, headings and inline code shortcuts for Quarto (`.qmd`) and Typst (`.typ`) files, in addition to Overleaf's existing LaTeX toolbar.
- **Lumière theme** — card-based project dashboard with PDF/slide thumbnails, a teal gradient identity, dark editor chrome, and an XS compact list view.
- **i18n** — French, German, Italian and Spanish UI translations.
- **Auto-compile** — preview refreshes automatically after you stop typing.
---
## Releases
### Alpha 1
The initial public release. Established Verso as an Overleaf fork with first-class
multi-language support:
- Quarto (`.qmd`) and Typst (`.typ`) compilers running alongside TeX Live,
dispatched automatically by root file extension — no per-project configuration.
- Language-aware editor for Quarto: Markdown highlighting, code-chunk completions
(`{python}`, `{r}`, `{julia}`, `{ojs}`…), callout and fenced-div completions,
- **Visual editors for Quarto and Typst** — bold, italic, headings and inline code
shortcuts in the toolbar for `.qmd` and `.typ` files.
- **Top/bottom split view** — new editor layout that stacks the source editor
above the PDF preview vertically, in addition to the existing side-by-side mode.
- **Bidirectional format export** — LaTeX projects can be converted to Typst and
Typst projects to LaTeX via pandoc. Available from the File menu in the editor.
- **Mobile layout** — project dashboard, search bar, footer and editor all
adapted for phone screen sizes.
---
## Known issues
- **Large file upload timeouts** — uploads of large files on slow connections
can time out at the proxy layer. A streaming response fix is pending.
---
## Security model — trusted environments only
> [!CAUTION]
> Overleaf Community Edition is intended for use in environments where **all** users are trusted. Community Edition is **not** appropriate for scenarios where isolation of users is required due to Sandbox Compiles not being available. When not using Sandboxed Compiles, users have full read and write access to the `sharelatex` container resources (filesystem, network, environment variables) when running LaTeX compiles.
> Verso is designed for **closed groups of trusted users** (a lab, a class, a
> small team). All three compilers can execute arbitrary code on the server:
>
> - LaTeX with shell-escape enabled can run system commands.
> - Quarto Python cells execute Python code directly.
> - Typst's scripting layer is sandboxed by design, but runs server-side.
>
> There is **no per-project sandbox or resource isolation** beyond what the
> operating system provides. Exposing Verso to the public internet with open
> registration is not recommended. If you need to host a collaborative
> LaTeX editor for untrusted users or at scale, look at
> which include proper sandboxing and enterprise access controls.
For more information on Sandbox Compiles check out our [documentation](https://docs.overleaf.com/on-premises/configuration/overleaf-toolkit/server-pro-only-configuration/sandboxed-compiles).
---
## Enterprise
## Quick start
If you want help installing and maintaining Overleaf in your lab or workplace, we offer an officially supported version called [Overleaf Server Pro](https://www.overleaf.com/for/enterprises). It also includes more features for security (SSO with LDAP or SAML), administration and collaboration (e.g. tracked changes). [Find out more!](https://www.overleaf.com/for/enterprises)
### With Docker
## Keeping up to date
```bash
docker run -d \
-p 80:80 \
-v ~/verso_data:/var/lib/overleaf \
--name verso \
registry.alocoq.fr/verso:latest
```
Sign up to the [mailing list](https://mailchi.mp/overleaf.com/community-edition-and-server-pro) to get updates on Overleaf releases and development.
Open `http://localhost`, then visit `/launchpad` on first run to create the admin
account.
## Installation
### Build from source
We have detailed installation instructions in the [Overleaf Toolkit](https://github.com/overleaf/toolkit/).
```bash
cd server-ce
make build-base # base OS image: system deps, Quarto, Typst, TeX Live
make build-community # application image: Node services + compiled frontend
```
## Upgrading
| File | Purpose |
|------|---------|
| `server-ce/Dockerfile-base` | Base image — system deps, Quarto (with Typst) and TeX Live |
| `server-ce/Dockerfile` | App image — Node services and the compiled React frontend |
If you are upgrading from a previous version of Overleaf, please see the [Release Notes section on the Wiki](https://github.com/overleaf/overleaf/wiki#release-notes) for all of the versions between your current version and the version you are upgrading to.
---
## Overleaf Docker Image
## Architecture
This repo contains two dockerfiles, [`Dockerfile-base`](server-ce/Dockerfile-base), which builds the
`sharelatex/sharelatex-base` image, and [`Dockerfile`](server-ce/Dockerfile) which builds the
`sharelatex/sharelatex` (or "community") image.
Verso is a microservices monorepo (Yarn workspaces). All services run inside a
single container managed by `runit`, with `nginx` as the front router.
The Base image generally contains the basic dependencies like `wget`, plus `texlive`.
We split this out because it's a pretty heavy set of
dependencies, and it's nice to not have to rebuild all of that every time.
// Return a PassThrough stream with a minimal interface. It will buffer until the caller starts reading. It will emit errors from the source stream (Stream.pipeline passes errors along).
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.