Merge pull request #35056 from overleaf/mj-command-palette-improvements

[web] Command palette improvements

GitOrigin-RevId: 3d6e11697ed4726b7ecada1744d24a15291ad646
This commit is contained in:
Mathias Jakobsen
2026-09-17 11:25:26 +00:00
committed by Copybot
parent 4ef7324263
commit 912fb2dcd8
14 changed files with 339 additions and 102 deletions
@@ -310,6 +310,7 @@
"close": "",
"close_dialog": "",
"close_other_tabs": "",
"close_tab": "",
"close_tabs_to_the_right": "",
"clsi_maintenance": "",
"clsi_unavailable": "",
@@ -680,6 +681,10 @@
"features_like_track_changes": "",
"feedback": "",
"figure": "",
"figure_from_another_project": "",
"figure_from_computer": "",
"figure_from_project_files": "",
"figure_from_url": "",
"file": "",
"file_action_created": "",
"file_action_deleted": "",
@@ -976,25 +981,15 @@
"inr_discount_modal_info": "",
"inr_discount_modal_title": "",
"insert": "",
"insert_citation": "",
"insert_column_left": "",
"insert_column_right": "",
"insert_cross_reference": "",
"insert_display_math": "",
"insert_figure": "",
"insert_figure_from_another_project": "",
"insert_figure_from_computer": "",
"insert_figure_from_project_files": "",
"insert_figure_from_url": "",
"insert_from_another_project": "",
"insert_from_project_files": "",
"insert_from_url": "",
"insert_image": "",
"insert_inline_math": "",
"insert_link": "",
"insert_row_above": "",
"insert_row_below": "",
"insert_table": "",
"insert_x_columns_left": "",
"insert_x_columns_right": "",
"insert_x_rows_above": "",
@@ -1055,6 +1050,8 @@
"join_x_managed_enterprise_group": "",
"joined_team": "",
"joining": "",
"jump_to_line_x": "",
"jump_to_line_x_column_y": "",
"jump_to_location_in_pdf": "",
"justify": "",
"kb_suggestions_enquiry": "",
@@ -1444,6 +1441,7 @@
"pdf_compile_in_progress_error": "",
"pdf_compile_rate_limit_hit": "",
"pdf_compile_try_again": "",
"pdf_in_different_tab": "",
"pdf_only": "",
"pdf_preview": "",
"pdf_preview_error": "",
@@ -2145,6 +2143,7 @@
"to_pull_results_directly_from_your_reference_manager_enable_one_of_the_available_reference_manager_integrations": "",
"to_use_text_wrapping_in_your_table_make_sure_you_include_the_array_package": "",
"toggle_compile_options_menu": "",
"toggle_focus_mode": "",
"toggle_group": "",
"toggle_personal_library": "",
"toggle_unknown_group": "",
@@ -111,17 +111,17 @@ const CommandPaletteBody: FC<CommandPaletteBodyProps> = ({ show, onHide }) => {
>
<button
role="menuitem"
onMouseEnter={() => setSelectedIndex(index)}
onMouseMove={() => setSelectedIndex(index)}
onClick={() => runResult(result)}
>
<div className="command-palette-result-title">
{result.title}
{result.description && (
<span className="command-palette-result-description">
{result.description}
</span>
)}
</div>
{result.description && (
<div className="command-palette-result-description">
{result.description}
</div>
)}
</button>
</li>
))}
@@ -1,7 +1,7 @@
import useDebounce from '@/shared/hooks/use-debounce'
import useCommandPaletteSources from './use-command-palette-sources'
import { useEffect, useState } from 'react'
import { CommandPaletteSearchResult } from '../types'
import { CommandPaletteSearchResult, CommandPaletteSource } from '../types'
const useCommandPaletteResults = (query: string) => {
const debouncedQuery = useDebounce(query, 25)
@@ -9,17 +9,37 @@ const useCommandPaletteResults = (query: string) => {
const [results, setResults] = useState<CommandPaletteSearchResult[]>([])
useEffect(() => {
if (debouncedQuery) {
const res = sources.map(source => source.search(debouncedQuery))
setResults(res.flat().sort((a, b) => b.score - a.score))
} else {
const res = sources
.filter(s => 'defaults' in s)
.map(source => source.defaults!())
setResults(res.flat().sort((a, b) => b.score - a.score))
}
const res = getSourcesMatchingQuery(debouncedQuery, sources).map(
({ source, query }) =>
query ? source.search(query) : (source.defaults?.() ?? [])
)
setResults(res.flat().sort((a, b) => b.score - a.score))
}, [debouncedQuery, sources])
return results
}
const getSourcesMatchingQuery = (
query: string,
sources: CommandPaletteSource[]
): { query: string; source: CommandPaletteSource }[] => {
// No query matches all sources
if (!query) return sources.map(source => ({ source, query }))
// If the query matches a prefix, return all the sources with that prefix
const matchingSources = sources.filter(
source => source.prefix && query.startsWith(source.prefix)
)
if (matchingSources.length > 0) {
return matchingSources.map(source => ({
source,
query: query.slice(source.prefix!.length).trim(),
}))
}
// otherwise return all sources that don't require a prefix match
return sources
.filter(source => !source.prefixRequired)
.map(source => ({ source, query: query.trim() }))
}
export default useCommandPaletteResults
@@ -2,13 +2,16 @@ import { useMemo } from 'react'
import { CommandPaletteSource } from '../types'
import useCommandRegistrySource from './use-command-registry-source'
import useFileTreeCommandSource from './use-file-tree-command-source'
import { useJumpToLineCommandSource } from './use-jump-to-line-command-source'
const useCommandPaletteSources = (): CommandPaletteSource[] => {
const fileTreeSource = useFileTreeCommandSource()
const commandRegistrySource = useCommandRegistrySource()
const jumpToLineSource = useJumpToLineCommandSource()
const sources = useMemo(
() => [fileTreeSource, commandRegistrySource],
[fileTreeSource, commandRegistrySource]
() => [fileTreeSource, commandRegistrySource, jumpToLineSource],
[fileTreeSource, commandRegistrySource, jumpToLineSource]
)
return sources
}
@@ -5,42 +5,85 @@ import {
Command,
useCommandRegistry,
} from '@/features/ide-react/context/command-registry-context'
import { useTranslation } from 'react-i18next'
const ENABLED_COMMANDS: string[] = [
'new_file',
'new_folder',
'upload_file',
'open-settings',
'show_version_history',
'word_count',
'view-pdf-presentation-mode',
'comment',
'compile',
'stop-compile',
'recompile-from-scratch',
'synctex-sync-to-pdf',
'synctex-sync-to-code',
'insert-inline-math',
'insert-display-math',
'insert-figure-from-computer',
'insert-figure-from-project-files',
'insert-figure-from-another-project',
'insert-figure-from-url',
'insert-table',
'insert-citation',
'insert-link',
'insert-cross-reference',
]
type CommandGroup = {
label: string
actions: string[]
}
const useCommandRegistrySource = (): CommandPaletteSource => {
const { t } = useTranslation()
const elements: (CommandGroup | string)[] = useMemo(
() => [
{
label: t('file'),
actions: ['new_file', 'new_folder', 'upload_file'],
},
{
label: t('view'),
actions: [
'change-layout-side-by-side',
'change-layout-editor-only',
'change-layout-pdf-only',
'change-layout-detached-pdf',
'change-layout-focus-mode',
],
},
{
label: t('insert'),
actions: [
'insert-inline-math',
'insert-display-math',
'insert-figure-from-computer',
'insert-figure-from-project-files',
'insert-figure-from-another-project',
'insert-figure-from-url',
'insert-table',
'insert-citation',
'insert-link',
'insert-cross-reference',
],
},
'open-settings',
'show_version_history',
'word_count',
'view-pdf-presentation-mode',
'comment',
'compile',
'stop-compile',
'recompile-from-scratch',
'synctex-sync-to-pdf',
'synctex-sync-to-code',
'close-tab',
'close-other-tabs',
],
[t]
)
const { registry } = useCommandRegistry()
const commands = useMemo(() => {
const enabled = new Set(ENABLED_COMMANDS)
return [...registry.values()].filter(
c => enabled.has(c.id) && !c.disabled && c.handler
)
}, [registry])
const ret: Command[] = []
for (const element of elements) {
if (typeof element === 'string') {
const command = registry.get(element)
if (command && !command.disabled && command.handler) {
ret.push(command)
}
} else {
for (const action of element.actions) {
const command = registry.get(action)
if (command && !command.disabled && command.handler) {
ret.push({
...command,
label: `${element.label}: ${command.label}`,
})
}
}
}
}
return ret
}, [registry, elements])
const defaults = useCallback((): CommandPaletteSearchResult[] => {
return commands.map(command => ({
@@ -53,7 +96,7 @@ const useCommandRegistrySource = (): CommandPaletteSource => {
const index = useMemo(() => {
const miniSearch = new MiniSearch<Command>({
fields: ['label'],
storeFields: ['id'],
storeFields: ['id', 'label'],
idField: 'id',
})
miniSearch.addAll(commands)
@@ -63,17 +106,18 @@ const useCommandRegistrySource = (): CommandPaletteSource => {
return useMemo<CommandPaletteSource>(
() => ({
id: 'command-registry',
prefix: '>',
search(query) {
const results = index.search(query, {
prefix: true,
fuzzy: term => (term.length > 3 ? 0.2 : false),
})
return results.flatMap(({ id, score }) => {
return results.flatMap(({ id, label, score }) => {
const command = registry.get(id)
if (!command?.handler) return []
return [
{
title: command.label,
title: label,
onSelect: () => command.handler!({ location: 'command-palette' }),
score,
},
@@ -9,9 +9,12 @@ import { useEditorManagerContext } from '@/features/ide-react/context/editor-man
type FlatFileTree = { path: string; name: string; id: string }[]
const FILE_POSITION_REGEX = /:(\d+)(?:,(\d+))?$/
const useFileTreeCommandSource = (): CommandPaletteSource => {
const { fileTreeData } = useFileTreeData()
const { openDocWithId, openFileWithId } = useEditorManagerContext()
const flatFileTree = useMemo(
() => flattenFileTree(fileTreeData),
[fileTreeData]
@@ -22,7 +25,11 @@ const useFileTreeCommandSource = (): CommandPaletteSource => {
)
const onSelect = useCallback(
async (id: string) => {
async (
id: string,
line: number | undefined,
column: number | undefined
) => {
if (!fileTreeData) {
return
}
@@ -31,7 +38,10 @@ const useFileTreeCommandSource = (): CommandPaletteSource => {
return
}
if (file.type === 'doc') {
await openDocWithId(file.entity._id)
await openDocWithId(file.entity._id, {
gotoLine: line,
gotoColumn: column,
})
} else if (file.type === 'fileRef') {
openFileWithId(file.entity._id)
} else {
@@ -49,8 +59,9 @@ const useFileTreeCommandSource = (): CommandPaletteSource => {
.slice(0, 10)
.map(({ path, name, id }) => ({
title: name,
description: path === name ? undefined : path,
onSelect: () => onSelect(id),
description:
path === name ? undefined : path.split('/').slice(0, -1).join('/'),
onSelect: () => onSelect(id, undefined, undefined),
score: 1,
}))
@@ -64,14 +75,27 @@ const useFileTreeCommandSource = (): CommandPaletteSource => {
if (!index) {
return []
}
const fileLocationMatch = query.match(FILE_POSITION_REGEX)
let line: number | undefined = undefined
let column: number | undefined = undefined
if (fileLocationMatch) {
line = parseInt(fileLocationMatch[1], 10)
column = fileLocationMatch[2]
? parseInt(fileLocationMatch[2], 10)
: undefined
query = query.slice(0, fileLocationMatch.index)
}
const result = index.search(query, {
prefix: true,
fuzzy: term => (term.length > 3 ? 0.2 : false),
})
return result.map(({ path, name, id, score }) => ({
title: name,
description: path === name ? undefined : path,
onSelect: () => onSelect(id),
description:
path === name ? undefined : path.split('/').slice(0, -1).join('/'),
onSelect: () => onSelect(id, line, column),
score,
}))
},
@@ -0,0 +1,43 @@
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
import { CommandPaletteSource } from '../types'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
const JUMP_REGEX = /^(\d+)(?:,(\d+))?$/
export const useJumpToLineCommandSource = (): CommandPaletteSource => {
const { jumpToLine } = useEditorManagerContext()
const { t } = useTranslation()
return useMemo(
() => ({
id: 'jump-to-line-source',
prefix: ':',
prefixRequired: true,
search: (query: string) => {
const match = query.match(JUMP_REGEX)
if (!match) {
return []
}
const line = parseInt(match[1], 10)
const column = match[2] ? parseInt(match[2], 10) : undefined
return [
{
onSelect: () => jumpToLine({ gotoLine: line, gotoColumn: column }),
title:
column != null
? t('jump_to_line_x_column_y', {
line,
column,
})
: t('jump_to_line_x', {
line,
}),
score: 1,
},
]
},
}),
[jumpToLine, t]
)
}
@@ -9,4 +9,6 @@ export type CommandPaletteSource = {
id: string
search(query: string): CommandPaletteSearchResult[]
defaults?(): CommandPaletteSearchResult[]
prefix?: string
prefixRequired?: boolean
}
@@ -1,4 +1,4 @@
import { useRef, useEffect } from 'react'
import { useRef, useEffect, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useFileTreeCreateName } from '../../contexts/file-tree-create-name'
import {
@@ -36,6 +36,7 @@ export default function FileTreeCreateNameInput({
// the value is stored in a context provider, so it's available elsewhere in the form
const { name, setName, touchedName, validName } = useFileTreeCreateName()
const nameFocusedOnce = useRef(false)
// focus the first part of the filename if needed
const inputRef = useRef<HTMLInputElement>(null)
@@ -45,15 +46,24 @@ export default function FileTreeCreateNameInput({
window.requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
inputRef.current.setSelectionRange(
0,
inputRef.current.value.lastIndexOf('.')
)
}
})
}
}, [focusName])
const onFocus = useCallback(() => {
if (nameFocusedOnce.current) return
if (!focusName) return
window.requestAnimationFrame(() => {
if (!inputRef.current) return
nameFocusedOnce.current = true
inputRef.current.setSelectionRange(
0,
inputRef.current.value.lastIndexOf('.')
)
})
}, [focusName])
return (
<OLFormGroup controlId="new-doc-name" className={classes.formGroup}>
<OLFormLabel>{label || t('file_name')}</OLFormLabel>
@@ -65,6 +75,7 @@ export default function FileTreeCreateNameInput({
onChange={event => setName(event.target.value)}
ref={inputRef}
disabled={inFlight}
onFocus={onFocus}
/>
{touchedName && !validName && (
<div className="notification-list">
@@ -8,6 +8,10 @@ import {
import ChangeLayoutOptions from './change-layout-options'
import MaterialIcon from '@/shared/components/material-icon'
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import { useCommandProvider } from '../../hooks/use-command-provider'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
import { useLayoutContext } from '@/shared/context/layout-context'
export default function ChangeLayoutButton() {
const { t } = useTranslation()
@@ -17,6 +21,71 @@ export default function ChangeLayoutButton() {
'ide-redesign-toolbar-button-icon'
)
const {
detachIsLinked,
detachRole,
handleChangeLayout,
handleDetach,
focusMode,
setFocusMode,
} = useLayoutContext()
const { sendEvent } = useEditorAnalytics()
const focusModeEnabled = useFeatureFlag('focus-mode')
const detachable = 'BroadcastChannel' in window
const waitingForDetachedLink = !detachIsLinked && detachRole === 'detacher'
useCommandProvider(
() => [
{
id: 'change-layout-side-by-side',
handler: () => handleChangeLayout('sideBySide'),
label: t('split_view'),
},
{
id: 'change-layout-editor-only',
handler: () => handleChangeLayout('flat', 'editor'),
label: t('editor_only'),
},
{
id: 'change-layout-pdf-only',
handler: () => handleChangeLayout('flat', 'pdf'),
label: t('pdf_only'),
},
{
id: 'change-layout-detached-pdf',
handler: () => handleDetach(),
disabled: !detachable || waitingForDetachedLink,
label: t('pdf_in_different_tab'),
},
...(focusModeEnabled
? [
{
id: 'change-layout-focus-mode',
handler: () => {
setFocusMode(!focusMode)
sendEvent('focus-mode-toggle', { focusMode: !focusMode })
},
label: t('toggle_focus_mode'),
},
]
: []),
],
[
handleChangeLayout,
t,
detachable,
handleDetach,
waitingForDetachedLink,
focusModeEnabled,
focusMode,
setFocusMode,
sendEvent,
]
)
return (
<div className="ide-redesign-toolbar-button-container">
<OLDropdown className="toolbar-item layout-dropdown" align="end">
@@ -10,6 +10,8 @@ import { useCallback, useMemo, useState } from 'react'
import { throttle } from 'lodash'
import { debugConsole } from '@/utils/debugging'
import classNames from 'classnames'
import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider'
import { useTranslation } from 'react-i18next'
export const TabsContainer = () => {
const {
@@ -20,9 +22,11 @@ export const TabsContainer = () => {
makeTabPermanent,
setContextMenuTarget,
setHeaderSlot,
closeOtherTabs,
} = useTabsContext()
const { openEntity } = useFileTreeOpenContext()
const [hovered, setHovered] = useState<boolean>(false)
const { t } = useTranslation()
const openContextMenu = useCallback(
(coords: { top: number; left: number }, tabId: string) => {
@@ -86,6 +90,32 @@ export const TabsContainer = () => {
}
}, [])
useCommandProvider(() => {
if (tabs.length === 0) {
return
}
if (!openEntity?.entity._id) {
return
}
return [
{
id: 'close-tab',
label: t('close_tab'),
handler: () => {
closeTab(openEntity.entity._id)
},
},
{
id: 'close-other-tabs',
label: t('close_other_tabs'),
handler: () => {
closeOtherTabs(openEntity.entity._id)
},
},
]
}, [tabs, t, openEntity, closeTab, closeOtherTabs])
return (
<div className="editor-tabs-container">
<div className="review-panel-header-slot" ref={setHeaderSlot} />
@@ -102,7 +102,7 @@ export const useToolbarMenuBarEditorCommands = () => {
{
id: 'insert-inline-math',
menuLabel: t('inline_math'),
label: t('insert_inline_math'),
label: t('inline_math'),
handler: () => {
commands.wrapInInlineMath(view)
view.focus()
@@ -111,7 +111,7 @@ export const useToolbarMenuBarEditorCommands = () => {
{
id: 'insert-display-math',
menuLabel: t('display_math'),
label: t('insert_display_math'),
label: t('display_math'),
handler: () => {
commands.wrapInDisplayMath(view)
view.focus()
@@ -119,7 +119,7 @@ export const useToolbarMenuBarEditorCommands = () => {
},
{
menuLabel: t('upload_from_computer'),
label: t('insert_figure_from_computer'),
label: t('figure_from_computer'),
id: 'insert-figure-from-computer',
handler: () => {
openFigureModal(FigureModalSource.FILE_UPLOAD)
@@ -127,7 +127,7 @@ export const useToolbarMenuBarEditorCommands = () => {
},
{
menuLabel: t('from_project_files'),
label: t('insert_figure_from_project_files'),
label: t('figure_from_project_files'),
id: 'insert-figure-from-project-files',
handler: () => {
openFigureModal(FigureModalSource.FILE_TREE)
@@ -149,7 +149,7 @@ export const useToolbarMenuBarEditorCommands = () => {
return [
{
menuLabel: t('from_another_project'),
label: t('insert_figure_from_another_project'),
label: t('figure_from_another_project'),
id: 'insert-figure-from-another-project',
handler: () => {
openFigureModal(FigureModalSource.OTHER_PROJECT)
@@ -179,7 +179,7 @@ export const useToolbarMenuBarEditorCommands = () => {
return [
{
menuLabel: t('from_url'),
label: t('insert_figure_from_url'),
label: t('figure_from_url'),
id: 'insert-figure-from-url',
handler: () => {
openFigureModal(FigureModalSource.FROM_URL)
@@ -206,8 +206,7 @@ export const useToolbarMenuBarEditorCommands = () => {
return [
{
id: 'insert-table',
menuLabel: t('table'),
label: t('insert_table'),
label: t('table'),
handler: () => {
commands.insertTable(view, 3, 3)
view.focus()
@@ -215,8 +214,7 @@ export const useToolbarMenuBarEditorCommands = () => {
},
{
id: 'insert-citation',
menuLabel: t('citation'),
label: t('insert_citation'),
label: t('citation'),
handler: () => {
commands.insertCite(view)
view.focus()
@@ -224,8 +222,7 @@ export const useToolbarMenuBarEditorCommands = () => {
},
{
id: 'insert-link',
menuLabel: t('link'),
label: t('insert_link'),
label: t('link'),
handler: () => {
commands.wrapInHref(view)
view.focus()
@@ -233,8 +230,7 @@ export const useToolbarMenuBarEditorCommands = () => {
},
{
id: 'insert-cross-reference',
menuLabel: t('cross_reference'),
label: t('insert_cross_reference'),
label: t('cross_reference'),
handler: () => {
commands.insertRef(view)
view.focus()
@@ -83,22 +83,19 @@
background-color: var(--command-palette-selected-bg);
color: var(--command-palette-selected-color);
}
&.command-palette-result-selected .command-palette-result-description {
color: var(--command-palette-selected-color);
}
}
.command-palette-result-title {
color: var(--content-primary-themed);
font-size: var(--font-size-03);
font-size: var(--font-size-02);
font-weight: 500;
line-height: 1.2;
}
.command-palette-result-description {
font-size: var(--font-size-02);
color: var(--content-secondary-themed);
margin-left: var(--spacing-02);
color: var(--content-placeholder-themed);
line-height: 1.2;
}
}
+9 -10
View File
@@ -402,6 +402,7 @@
"close": "Close",
"close_dialog": "Close dialog",
"close_other_tabs": "Close other tabs",
"close_tab": "Close tab",
"close_tabs_to_the_right": "Close tabs to the right",
"clsi_maintenance": "The compile servers are down for maintenance, and will be back shortly.",
"clsi_unavailable": "Sorry, the compile server for your project was temporarily unavailable. Please try again in a few moments.",
@@ -888,6 +889,10 @@
"fedramp_li_saas": "FedRAMP® Li-SaaS authorized solution",
"feedback": "Feedback",
"figure": "Figure",
"figure_from_another_project": "Figure from another project",
"figure_from_computer": "Figure from your computer",
"figure_from_project_files": "Figure from project files",
"figure_from_url": "Figure from URL",
"file": "File",
"file_action_created": "Created",
"file_action_deleted": "Deleted",
@@ -1244,25 +1249,15 @@
"inr_discount_modal_title": "70% off all Overleaf premium plans for users in India",
"inr_discount_offer_plans_page_banner": "__flag__ <b>Great news!</b> We’ve applied a <b>70% discount</b> to premium plans for our users in India. Check out the new lower prices below.",
"insert": "Insert",
"insert_citation": "Insert citation",
"insert_column_left": "Insert column left",
"insert_column_right": "Insert column right",
"insert_cross_reference": "Insert cross reference",
"insert_display_math": "Insert display math",
"insert_figure": "Insert figure",
"insert_figure_from_another_project": "Insert figure from another project",
"insert_figure_from_computer": "Insert figure from your computer",
"insert_figure_from_project_files": "Insert figure from project files",
"insert_figure_from_url": "Insert figure from URL",
"insert_from_another_project": "Insert from another project",
"insert_from_project_files": "Insert from project files",
"insert_from_url": "Insert from URL",
"insert_image": "Insert image",
"insert_inline_math": "Insert inline math",
"insert_link": "Insert link",
"insert_row_above": "Insert row above",
"insert_row_below": "Insert row below",
"insert_table": "Insert table",
"insert_x_columns_left": "Insert __columns__ columns left",
"insert_x_columns_right": "Insert __columns__ columns right",
"insert_x_rows_above": "Insert __rows__ rows above",
@@ -1357,6 +1352,8 @@
"joined_team": "You have joined the group subscription managed by __inviterName__",
"joining": "Joining",
"july": "July",
"jump_to_line_x": "Jump to line __line__",
"jump_to_line_x_column_y": "Jump to line __line__, column __column__",
"jump_to_location_in_pdf": "Jump to location in PDF",
"june": "June",
"justify": "Justify",
@@ -1894,6 +1891,7 @@
"pdf_compile_in_progress_error": "A previous compile is still running. Please wait a minute and try compiling again.",
"pdf_compile_rate_limit_hit": "Compile rate limit hit",
"pdf_compile_try_again": "Please wait for your other compile to finish before trying again.",
"pdf_in_different_tab": "PDF in different tab",
"pdf_only": "PDF only",
"pdf_preview": "PDF preview",
"pdf_preview_error": "There was a problem displaying the compilation results for this project.",
@@ -2758,6 +2756,7 @@
"to_pull_results_directly_from_your_reference_manager_enable_one_of_the_available_reference_manager_integrations": "To pull results directly from your reference manager, <0>enable one of the available reference manager integrations</0>.",
"to_use_text_wrapping_in_your_table_make_sure_you_include_the_array_package": "<0>Please note:</0> To use text wrapping in your table, make sure you include the <1>array</1> package in your document preamble:",
"toggle_compile_options_menu": "Toggle compile options menu",
"toggle_focus_mode": "Toggle focus mode",
"toggle_unknown_group": "Toggle unknown group",
"token": "Token",
"token_access_failure": "Cannot grant access; contact the project owner for help",