diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js
index a7ff970ef0..5c7a9e4ab1 100644
--- a/services/web/config/settings.defaults.js
+++ b/services/web/config/settings.defaults.js
@@ -971,7 +971,7 @@ module.exports = {
pdfPreviewPromotions: [],
diagnosticActions: [],
sourceEditorCompletionSources: [],
- sourceEditorSymbolPalette: [],
+ sourceEditorSymbolPalette: ['@/features/symbol-palette/components/symbol-palette'],
sourceEditorToolbarComponents: [],
mainEditorLayoutModals: [],
langFeedbackLinkingWidgets: [],
@@ -1005,6 +1005,7 @@ module.exports = {
'launchpad',
'server-ce-scripts',
'user-activate',
+ 'symbol-palette',
],
viewIncludes: {},
diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-body.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-body.js
new file mode 100644
index 0000000000..3f9eb7fc5f
--- /dev/null
+++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-body.js
@@ -0,0 +1,76 @@
+import { useTranslation } from 'react-i18next'
+import PropTypes from 'prop-types'
+import SymbolPaletteItems from './symbol-palette-items'
+
+export default function SymbolPaletteBody({
+ categories,
+ categorisedSymbols,
+ filteredSymbols,
+ handleSelect,
+ focusInput,
+ activeCategoryId,
+}) {
+ const { t } = useTranslation()
+
+ // searching with matches: show the matched symbols
+ // searching with no matches: show a message
+ // note: include empty tab panels so that aria-controls on tabs can still reference the panel ids
+ if (filteredSymbols) {
+ return (
+
+ {filteredSymbols.length ? (
+
+ ) : (
+
{t('no_symbols_found')}
+ )}
+
+ {categories.map(category => (
+
+ ))}
+
+ )
+ }
+
+ // not searching: show the symbols grouped by category
+ return (
+
+ {categories.map((category) => (
+
+
+
+ ))}
+
+ )
+
+
+}
+SymbolPaletteBody.propTypes = {
+ categories: PropTypes.arrayOf(PropTypes.object).isRequired,
+ categorisedSymbols: PropTypes.object,
+ filteredSymbols: PropTypes.arrayOf(PropTypes.object),
+ handleSelect: PropTypes.func.isRequired,
+ focusInput: PropTypes.func.isRequired,
+ activeCategoryId: PropTypes.string.isRequired,
+}
diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-close-button.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-close-button.js
new file mode 100644
index 0000000000..0a2d3c02fc
--- /dev/null
+++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-close-button.js
@@ -0,0 +1,29 @@
+import { useEditorContext } from '../../../shared/context/editor-context'
+import { useTranslation } from 'react-i18next'
+import PropTypes from 'prop-types'
+
+export default function SymbolPaletteCloseButton() {
+ const { toggleSymbolPalette } = useEditorContext()
+ const { t } = useTranslation()
+
+ const handleClick = () => {
+ toggleSymbolPalette()
+ window.dispatchEvent(new CustomEvent('editor:focus'))
+ }
+
+ return (
+
+
+
+ )
+}
+
+SymbolPaletteCloseButton.propTypes = {
+ focusInput: PropTypes.func,
+}
diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-content.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-content.js
new file mode 100644
index 0000000000..b395cac53d
--- /dev/null
+++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-content.js
@@ -0,0 +1,94 @@
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import PropTypes from 'prop-types'
+import { matchSorter } from 'match-sorter'
+import symbols from '../data/symbols.json'
+import { buildCategorisedSymbols, createCategories } from '../utils/categories'
+import SymbolPaletteSearch from './symbol-palette-search'
+import SymbolPaletteBody from './symbol-palette-body'
+import SymbolPaletteTabs from './symbol-palette-tabs'
+import SymbolPaletteCloseButton from './symbol-palette-close-button'
+
+export default function SymbolPaletteContent({ handleSelect }) {
+ const [input, setInput] = useState('')
+
+ const { t } = useTranslation()
+
+ // build the list of categories with translated labels
+ const categories = useMemo(() => createCategories(t), [t])
+ const [activeCategoryId, setActiveCategoryId] = useState(categories[0]?.id)
+
+ // group the symbols by category
+ const categorisedSymbols = useMemo(
+ () => buildCategorisedSymbols(categories),
+ [categories]
+ )
+
+ // select symbols which match the input
+ const filteredSymbols = useMemo(() => {
+ if (input === '') {
+ return null
+ }
+
+ const words = input.trim().split(/\s+/)
+
+ return words.reduceRight(
+ (symbols, word) =>
+ matchSorter(symbols, word, {
+ keys: ['command', 'description', 'character', 'aliases'],
+ threshold: matchSorter.rankings.CONTAINS,
+ }),
+ symbols
+ )
+ }, [input])
+
+ const inputRef = useRef(null)
+
+ // allow the input to be focused
+ const focusInput = useCallback(() => {
+ if (inputRef.current) {
+ inputRef.current.focus()
+ }
+ }, [])
+
+ // focus the input when the symbol palette is opened
+ useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus()
+ }
+ }, [])
+ return (
+
+ )
+}
+SymbolPaletteContent.propTypes = {
+ handleSelect: PropTypes.func.isRequired,
+}
diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-item.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-item.js
new file mode 100644
index 0000000000..400da81b1e
--- /dev/null
+++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-item.js
@@ -0,0 +1,84 @@
+import { useEffect, useRef, forwardRef } from 'react'
+import PropTypes from 'prop-types'
+import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
+
+const SymbolPaletteItem = forwardRef(function ({
+ focused,
+ handleSelect,
+ handleKeyDown,
+ symbol,
+}, ref) {
+ const buttonRef = useRef(null)
+
+ // Forward internal ref to parent
+ useEffect(() => {
+ if (ref) {
+ if (typeof ref === 'function') {
+ ref(buttonRef.current)
+ } else {
+ ref.current = buttonRef.current
+ }
+ }
+ }, [ref])
+
+ // Focus the item when it becomes focused
+ useEffect(() => {
+ if (
+ focused &&
+ buttonRef.current &&
+ document.activeElement?.closest('.symbol-palette-items')
+ ) {
+ buttonRef.current.focus()
+ }
+ }, [focused])
+
+ return (
+
+
+ {symbol.description}
+
+
+ {symbol.command}
+
+ {symbol.notes && (
+
+ {symbol.notes}
+
+ )}
+
+ }
+ overlayProps={{ placement: 'top', trigger: ['hover', 'focus'] }}
+ >
+
+
+ )
+})
+
+SymbolPaletteItem.propTypes = {
+ symbol: PropTypes.shape({
+ codepoint: PropTypes.string.isRequired,
+ description: PropTypes.string.isRequired,
+ command: PropTypes.string.isRequired,
+ character: PropTypes.string.isRequired,
+ notes: PropTypes.string,
+ }),
+ handleKeyDown: PropTypes.func.isRequired,
+ handleSelect: PropTypes.func.isRequired,
+ focused: PropTypes.bool,
+}
+export default SymbolPaletteItem
diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-items.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-items.js
new file mode 100644
index 0000000000..8d95439f5a
--- /dev/null
+++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-items.js
@@ -0,0 +1,118 @@
+import { useCallback, useEffect, useRef, useState } from 'react'
+import PropTypes from 'prop-types'
+import { useEditorContext } from '../../../shared/context/editor-context'
+import SymbolPaletteItem from './symbol-palette-item'
+
+export default function SymbolPaletteItems({
+ items,
+ handleSelect,
+ focusInput,
+}) {
+ const [focusedIndex, setFocusedIndex] = useState(0)
+ const itemRefs = useRef([])
+
+ useEffect(() => {
+ itemRefs.current = items.map((_, i) => itemRefs.current[i] || null)
+ setFocusedIndex(0)
+ }, [items])
+
+ const getItemRects = () => {
+ return itemRefs.current.map(ref => ref?.getBoundingClientRect?.() ?? null)
+ }
+ const { toggleSymbolPalette } = useEditorContext()
+
+ const handleKeyDown = useCallback(
+ event => {
+ if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return
+
+ const rects = getItemRects()
+ const currentRect = rects[focusedIndex]
+ if (!currentRect) return
+
+ let newIndex = focusedIndex
+
+ switch (event.key) {
+ case 'ArrowLeft':
+ newIndex = focusedIndex > 0 ? focusedIndex - 1 : items.length - 1
+ break
+ case 'ArrowRight':
+ newIndex = focusedIndex < items.length - 1 ? focusedIndex + 1 : 0
+ break
+ case 'ArrowUp':
+ case 'ArrowDown': {
+ const direction = event.key === 'ArrowUp' ? -1 : 1
+ const candidates = rects
+ .map((rect, i) => ({ rect, i }))
+ .filter(({ rect }, i) =>
+ i !== focusedIndex &&
+ rect &&
+ Math.abs(rect.x - currentRect.x) < currentRect.width * 0.8 &&
+ (direction === -1 ? rect.y < currentRect.y : rect.y > currentRect.y)
+ )
+
+ if (candidates.length > 0) {
+ const closest = candidates.reduce((a, b) =>
+ Math.abs(b.rect.y - currentRect.y) < Math.abs(a.rect.y - currentRect.y) ? b : a
+ )
+ newIndex = closest.i
+ }
+ break
+ }
+ case 'Home':
+ newIndex = 0
+ break
+ case 'End':
+ newIndex = items.length - 1
+ break
+ case 'Enter':
+ case ' ':
+ handleSelect(items[focusedIndex])
+ toggleSymbolPalette()
+ break
+ case 'Escape':
+ toggleSymbolPalette()
+ window.dispatchEvent(new CustomEvent('editor:focus'))
+ break
+
+ default:
+ focusInput()
+ return
+ }
+
+ event.preventDefault()
+ setFocusedIndex(newIndex)
+ },
+ [focusedIndex, items, focusInput, handleSelect]
+ )
+
+ return (
+
+ {items.map((symbol, index) => (
+ {
+ handleSelect(symbol)
+ setFocusedIndex(index)
+ }}
+ handleKeyDown={handleKeyDown}
+ focused={index === focusedIndex}
+ ref={el => {
+ itemRefs.current[index] = el
+ }}
+ />
+ ))}
+
+ )
+}
+
+SymbolPaletteItems.propTypes = {
+ items: PropTypes.arrayOf(
+ PropTypes.shape({
+ codepoint: PropTypes.string.isRequired,
+ })
+ ).isRequired,
+ handleSelect: PropTypes.func.isRequired,
+ focusInput: PropTypes.func.isRequired,
+}
+
diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-search.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-search.js
new file mode 100644
index 0000000000..75d01f0119
--- /dev/null
+++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-search.js
@@ -0,0 +1,45 @@
+import { useCallback, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import PropTypes from 'prop-types'
+import OLFormControl from '@/features/ui/components/ol/ol-form-control'
+import useDebounce from '../../../shared/hooks/use-debounce'
+
+export default function SymbolPaletteSearch({ setInput, inputRef }) {
+ const [localInput, setLocalInput] = useState('')
+
+ // debounce the search input until a typing delay
+ const debouncedLocalInput = useDebounce(localInput, 250)
+
+ useEffect(() => {
+ setInput(debouncedLocalInput)
+ }, [debouncedLocalInput, setInput])
+
+ const { t } = useTranslation()
+
+ const inputRefCallback = useCallback(
+ element => {
+ inputRef.current = element
+ },
+ [inputRef]
+ )
+
+ return (
+ {
+ setLocalInput(event.target.value)
+ }}
+ />
+ )
+}
+
+SymbolPaletteSearch.propTypes = {
+ setInput: PropTypes.func.isRequired,
+ inputRef: PropTypes.object.isRequired,
+}
diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette-tabs.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-tabs.js
new file mode 100644
index 0000000000..80f0421f27
--- /dev/null
+++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette-tabs.js
@@ -0,0 +1,76 @@
+import PropTypes from 'prop-types'
+import { useState, useRef } from 'react'
+
+
+export default function SymbolPaletteTabs({
+ categories,
+ activeCategoryId,
+ setActiveCategoryId,
+}) {
+
+ const buttonRefs = useRef([])
+ const focusTab = (index) => {
+ setActiveCategoryId(categories[index].id)
+ buttonRefs.current[index]?.focus()
+ }
+
+ const handleKeyDown = (e, index) => {
+ switch (e.key) {
+ case 'ArrowRight':
+ focusTab((index + 1) % categories.length)
+ break
+ case 'ArrowLeft':
+ focusTab((index - 1 + categories.length) % categories.length)
+ break
+ case 'Home':
+ case 'PageUp':
+ focusTab(0)
+ break
+ case 'End':
+ case 'PageDown':
+ focusTab(categories.length - 1)
+ break
+ default:
+ break
+ }
+ }
+
+ return (
+
+ {categories.map((category, index) => {
+ const selected = activeCategoryId === category.id
+ return (
+
+ )
+ })}
+
+ )
+}
+
+SymbolPaletteTabs.propTypes = {
+ categories: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ })).isRequired,
+ activeCategoryId: PropTypes.string.isRequired,
+ setActiveCategoryId: PropTypes.func.isRequired,
+}
diff --git a/services/web/frontend/js/features/symbol-palette/components/symbol-palette.js b/services/web/frontend/js/features/symbol-palette/components/symbol-palette.js
new file mode 100644
index 0000000000..2f1cc5e8c8
--- /dev/null
+++ b/services/web/frontend/js/features/symbol-palette/components/symbol-palette.js
@@ -0,0 +1,8 @@
+import SymbolPaletteContent from './symbol-palette-content'
+
+export default function SymbolPalette() {
+ const handleSelect = (symbol) => {
+ window.dispatchEvent(new CustomEvent('editor:insert-symbol', { detail: symbol }))
+ }
+ return
+}
diff --git a/services/web/frontend/js/features/symbol-palette/data/symbols.json b/services/web/frontend/js/features/symbol-palette/data/symbols.json
new file mode 100644
index 0000000000..af160b3eed
--- /dev/null
+++ b/services/web/frontend/js/features/symbol-palette/data/symbols.json
@@ -0,0 +1,872 @@
+[
+ {
+ "category": "Greek",
+ "command": "\\alpha",
+ "codepoint": "U+1D6FC",
+ "description": "Lowercase Greek letter alpha",
+ "aliases": ["a", "α"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\beta",
+ "codepoint": "U+1D6FD",
+ "description": "Lowercase Greek letter beta",
+ "aliases": ["b", "β"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\gamma",
+ "codepoint": "U+1D6FE",
+ "description": "Lowercase Greek letter gamma",
+ "aliases": ["γ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\delta",
+ "codepoint": "U+1D6FF",
+ "description": "Lowercase Greek letter delta",
+ "aliases": ["δ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\varepsilon",
+ "codepoint": "U+1D700",
+ "description": "Lowercase Greek letter epsilon, varepsilon",
+ "aliases": ["ε"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\epsilon",
+ "codepoint": "U+1D716",
+ "description": "Lowercase Greek letter epsilon lunate",
+ "aliases": ["ε"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\zeta",
+ "codepoint": "U+1D701",
+ "description": "Lowercase Greek letter zeta",
+ "aliases": ["ζ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\eta",
+ "codepoint": "U+1D702",
+ "description": "Lowercase Greek letter eta",
+ "aliases": ["η"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\vartheta",
+ "codepoint": "U+1D717",
+ "description": "Lowercase Greek letter curly theta, vartheta",
+ "aliases": ["θ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\theta",
+ "codepoint": "U+1D703",
+ "description": "Lowercase Greek letter theta",
+ "aliases": ["θ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\iota",
+ "codepoint": "U+1D704",
+ "description": "Lowercase Greek letter iota",
+ "aliases": ["ι"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\kappa",
+ "codepoint": "U+1D705",
+ "description": "Lowercase Greek letter kappa",
+ "aliases": ["κ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\lambda",
+ "codepoint": "U+1D706",
+ "description": "Lowercase Greek letter lambda",
+ "aliases": ["λ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\mu",
+ "codepoint": "U+1D707",
+ "description": "Lowercase Greek letter mu",
+ "aliases": ["μ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\nu",
+ "codepoint": "U+1D708",
+ "description": "Lowercase Greek letter nu",
+ "aliases": ["ν"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\xi",
+ "codepoint": "U+1D709",
+ "description": "Lowercase Greek letter xi",
+ "aliases": ["ξ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\pi",
+ "codepoint": "U+1D70B",
+ "description": "Lowercase Greek letter pi",
+ "aliases": ["π"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\varrho",
+ "codepoint": "U+1D71A",
+ "description": "Lowercase Greek letter rho, varrho",
+ "aliases": ["ρ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\rho",
+ "codepoint": "U+1D70C",
+ "description": "Lowercase Greek letter rho",
+ "aliases": ["ρ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\sigma",
+ "codepoint": "U+1D70E",
+ "description": "Lowercase Greek letter sigma",
+ "aliases": ["σ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\varsigma",
+ "codepoint": "U+1D70D",
+ "description": "Lowercase Greek letter final sigma, varsigma",
+ "aliases": ["ς"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\tau",
+ "codepoint": "U+1D70F",
+ "description": "Lowercase Greek letter tau",
+ "aliases": ["τ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\upsilon",
+ "codepoint": "U+1D710",
+ "description": "Lowercase Greek letter upsilon",
+ "aliases": ["υ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\phi",
+ "codepoint": "U+1D719",
+ "description": "Lowercase Greek letter phi",
+ "aliases": ["φ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\varphi",
+ "codepoint": "U+1D711",
+ "description": "Lowercase Greek letter phi, varphi",
+ "aliases": ["φ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\chi",
+ "codepoint": "U+1D712",
+ "description": "Lowercase Greek letter chi",
+ "aliases": ["χ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\psi",
+ "codepoint": "U+1D713",
+ "description": "Lowercase Greek letter psi",
+ "aliases": ["ψ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\omega",
+ "codepoint": "U+1D714",
+ "description": "Lowercase Greek letter omega",
+ "aliases": ["ω"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\Gamma",
+ "codepoint": "U+00393",
+ "description": "Uppercase Greek letter Gamma",
+ "aliases": ["Γ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\Delta",
+ "codepoint": "U+00394",
+ "description": "Uppercase Greek letter Delta",
+ "aliases": ["Δ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\Theta",
+ "codepoint": "U+00398",
+ "description": "Uppercase Greek letter Theta",
+ "aliases": ["Θ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\Lambda",
+ "codepoint": "U+0039B",
+ "description": "Uppercase Greek letter Lambda",
+ "aliases": ["Λ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\Xi",
+ "codepoint": "U+0039E",
+ "description": "Uppercase Greek letter Xi",
+ "aliases": ["Ξ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\Pi",
+ "codepoint": "U+003A0",
+ "description": "Uppercase Greek letter Pi",
+ "aliases": ["Π"],
+ "notes": "Use \\prod for the product."
+ },
+ {
+ "category": "Greek",
+ "command": "\\Sigma",
+ "codepoint": "U+003A3",
+ "description": "Uppercase Greek letter Sigma",
+ "aliases": ["Σ"],
+ "notes": "Use \\sum for the sum."
+ },
+ {
+ "category": "Greek",
+ "command": "\\Upsilon",
+ "codepoint": "U+003A5",
+ "description": "Uppercase Greek letter Upsilon",
+ "aliases": ["Υ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\Phi",
+ "codepoint": "U+003A6",
+ "description": "Uppercase Greek letter Phi",
+ "aliases": ["Φ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\Psi",
+ "codepoint": "U+003A8",
+ "description": "Uppercase Greek letter Psi",
+ "aliases": ["Ψ"],
+ "notes": ""
+ },
+ {
+ "category": "Greek",
+ "command": "\\Omega",
+ "codepoint": "U+003A9",
+ "description": "Uppercase Greek letter Omega",
+ "aliases": ["Ω"],
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\neq",
+ "codepoint": "U+02260",
+ "description": "Not equal",
+ "aliases": ["!="],
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\leq",
+ "codepoint": "U+02264",
+ "description": "Less than or equal",
+ "aliases": ["<="],
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\geq",
+ "codepoint": "U+02265",
+ "description": "Greater than or equal",
+ "aliases": [">="],
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\ll",
+ "codepoint": "U+0226A",
+ "description": "Much less than",
+ "aliases": ["<<"],
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\gg",
+ "codepoint": "U+0226B",
+ "description": "Much greater than",
+ "aliases": [">>"],
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\prec",
+ "codepoint": "U+0227A",
+ "description": "Precedes",
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\succ",
+ "codepoint": "U+0227B",
+ "description": "Succeeds",
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\in",
+ "codepoint": "U+02208",
+ "description": "Set membership",
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\notin",
+ "codepoint": "U+02209",
+ "description": "Negated set membership",
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\ni",
+ "codepoint": "U+0220B",
+ "description": "Contains",
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\subset",
+ "codepoint": "U+02282",
+ "description": "Subset",
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\subseteq",
+ "codepoint": "U+02286",
+ "description": "Subset or equals",
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\supset",
+ "codepoint": "U+02283",
+ "description": "Superset",
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\simeq",
+ "codepoint": "U+02243",
+ "description": "Similar",
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\approx",
+ "codepoint": "U+02248",
+ "description": "Approximate",
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\equiv",
+ "codepoint": "U+02261",
+ "description": "Identical with",
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\cong",
+ "codepoint": "U+02245",
+ "description": "Congruent with",
+ "notes": ""
+ },
+ {
+ "category": "Relations",
+ "command": "\\mid",
+ "codepoint": "U+02223",
+ "description": "Mid, divides, vertical bar, modulus, absolute value",
+ "notes": "Use \\lvert...\\rvert for the absolute value."
+ },
+ {
+ "category": "Relations",
+ "command": "\\nmid",
+ "codepoint": "U+02224",
+ "description": "Negated mid, not divides",
+ "notes": "Requires \\usepackage{amssymb}."
+ },
+ {
+ "category": "Relations",
+ "command": "\\parallel",
+ "codepoint": "U+02225",
+ "description": "Parallel, double vertical bar, norm",
+ "notes": "Use \\lVert...\\rVert for the norm."
+ },
+ {
+ "category": "Relations",
+ "command": "\\perp",
+ "codepoint": "U+027C2",
+ "description": "Perpendicular",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\times",
+ "codepoint": "U+000D7",
+ "description": "Cross product, multiplication",
+ "aliases": ["x"],
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\div",
+ "codepoint": "U+000F7",
+ "description": "Division",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\cap",
+ "codepoint": "U+02229",
+ "description": "Intersection",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\cup",
+ "codepoint": "U+0222A",
+ "description": "Union",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\cdot",
+ "codepoint": "U+022C5",
+ "description": "Dot product, multiplication",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\cdots",
+ "codepoint": "U+022EF",
+ "description": "Centered dots",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\bullet",
+ "codepoint": "U+02219",
+ "description": "Bullet",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\circ",
+ "codepoint": "U+025E6",
+ "description": "Circle",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\wedge",
+ "codepoint": "U+02227",
+ "description": "Wedge, logical and",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\vee",
+ "codepoint": "U+02228",
+ "description": "Vee, logical or",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\setminus",
+ "codepoint": "U+0005C",
+ "description": "Set minus, backslash",
+ "notes": "Use \\backslash for a backslash."
+ },
+ {
+ "category": "Operators",
+ "command": "\\oplus",
+ "codepoint": "U+02295",
+ "description": "Plus sign in circle",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\otimes",
+ "codepoint": "U+02297",
+ "description": "Multiply sign in circle",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\sum",
+ "codepoint": "U+02211",
+ "description": "Summation operator",
+ "notes": "Use \\Sigma for the letter Sigma."
+ },
+ {
+ "category": "Operators",
+ "command": "\\prod",
+ "codepoint": "U+0220F",
+ "description": "Product operator",
+ "notes": "Use \\Pi for the letter Pi."
+ },
+ {
+ "category": "Operators",
+ "command": "\\bigcap",
+ "codepoint": "U+022C2",
+ "description": "Intersection operator",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\bigcup",
+ "codepoint": "U+022C3",
+ "description": "Union operator",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\int",
+ "codepoint": "U+0222B",
+ "description": "Integral operator",
+ "notes": ""
+ },
+ {
+ "category": "Operators",
+ "command": "\\iint",
+ "codepoint": "U+0222C",
+ "description": "Double integral operator",
+ "notes": "Requires \\usepackage{amsmath}."
+ },
+ {
+ "category": "Operators",
+ "command": "\\iiint",
+ "codepoint": "U+0222D",
+ "description": "Triple integral operator",
+ "notes": "Requires \\usepackage{amsmath}."
+ },
+ {
+ "category": "Arrows",
+ "command": "\\leftarrow",
+ "codepoint": "U+02190",
+ "description": "Leftward arrow",
+ "aliases": ["<-"],
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\rightarrow",
+ "codepoint": "U+02192",
+ "description": "Rightward arrow",
+ "aliases": ["->"],
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\leftrightarrow",
+ "codepoint": "U+02194",
+ "description": "Left and right arrow",
+ "aliases": ["<->"],
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\uparrow",
+ "codepoint": "U+02191",
+ "description": "Upward arrow",
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\downarrow",
+ "codepoint": "U+02193",
+ "description": "Downward arrow",
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\Leftarrow",
+ "codepoint": "U+021D0",
+ "description": "Is implied by",
+ "aliases": ["<="],
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\Rightarrow",
+ "codepoint": "U+021D2",
+ "description": "Implies",
+ "aliases": ["=>"],
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\Leftrightarrow",
+ "codepoint": "U+021D4",
+ "description": "Left and right double arrow",
+ "aliases": ["<=>"],
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\mapsto",
+ "codepoint": "U+021A6",
+ "description": "Maps to, rightward",
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\nearrow",
+ "codepoint": "U+02197",
+ "description": "NE pointing arrow",
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\searrow",
+ "codepoint": "U+02198",
+ "description": "SE pointing arrow",
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\rightleftharpoons",
+ "codepoint": "U+021CC",
+ "description": "Right harpoon over left",
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\leftharpoonup",
+ "codepoint": "U+021BC",
+ "description": "Left harpoon up",
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\rightharpoonup",
+ "codepoint": "U+021C0",
+ "description": "Right harpoon up",
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\leftharpoondown",
+ "codepoint": "U+021BD",
+ "description": "Left harpoon down",
+ "notes": ""
+ },
+ {
+ "category": "Arrows",
+ "command": "\\rightharpoondown",
+ "codepoint": "U+021C1",
+ "description": "Right harpoon down",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\infty",
+ "codepoint": "U+0221E",
+ "description": "Infinity",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\partial",
+ "codepoint": "U+1D715",
+ "description": "Partial differential",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\nabla",
+ "codepoint": "U+02207",
+ "description": "Nabla, del, hamilton operator",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\varnothing",
+ "codepoint": "U+02300",
+ "description": "Empty set",
+ "notes": "Requires \\usepackage{amssymb}."
+ },
+ {
+ "category": "Misc",
+ "command": "\\forall",
+ "codepoint": "U+02200",
+ "description": "For all",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\exists",
+ "codepoint": "U+02203",
+ "description": "There exists",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\neg",
+ "codepoint": "U+000AC",
+ "description": "Not sign",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\Re",
+ "codepoint": "U+0211C",
+ "description": "Real part",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\Im",
+ "codepoint": "U+02111",
+ "description": "Imaginary part",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\Box",
+ "codepoint": "U+025A1",
+ "description": "Square",
+ "notes": "Requires \\usepackage{amssymb}."
+ },
+ {
+ "category": "Misc",
+ "command": "\\triangle",
+ "codepoint": "U+025B3",
+ "description": "Triangle",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\aleph",
+ "codepoint": "U+02135",
+ "description": "Hebrew letter aleph",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\wp",
+ "codepoint": "U+02118",
+ "description": "Weierstrass letter p",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\#",
+ "codepoint": "U+00023",
+ "description": "Number sign, hashtag",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\$",
+ "codepoint": "U+00024",
+ "description": "Dollar sign",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\%",
+ "codepoint": "U+00025",
+ "description": "Percent sign",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\&",
+ "codepoint": "U+00026",
+ "description": "Et sign, and, ampersand",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\{",
+ "codepoint": "U+0007B",
+ "description": "Left curly brace",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\}",
+ "codepoint": "U+0007D",
+ "description": "Right curly brace",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\langle",
+ "codepoint": "U+027E8",
+ "description": "Left angle bracket, bra",
+ "notes": ""
+ },
+ {
+ "category": "Misc",
+ "command": "\\rangle",
+ "codepoint": "U+027E9",
+ "description": "Right angle bracket, ket",
+ "notes": ""
+ }
+]
diff --git a/services/web/frontend/js/features/symbol-palette/utils/categories.js b/services/web/frontend/js/features/symbol-palette/utils/categories.js
new file mode 100644
index 0000000000..872534771f
--- /dev/null
+++ b/services/web/frontend/js/features/symbol-palette/utils/categories.js
@@ -0,0 +1,44 @@
+import symbols from '../data/symbols.json'
+export function createCategories(t) {
+ return [
+ {
+ id: 'Greek',
+ label: t('category_greek'),
+ },
+ {
+ id: 'Arrows',
+ label: t('category_arrows'),
+ },
+ {
+ id: 'Operators',
+ label: t('category_operators'),
+ },
+ {
+ id: 'Relations',
+ label: t('category_relations'),
+ },
+ {
+ id: 'Misc',
+ label: t('category_misc'),
+ },
+ ]
+}
+
+export function buildCategorisedSymbols(categories) {
+ const output = {}
+
+ for (const category of categories) {
+ output[category.id] = []
+ }
+
+ for (const item of symbols) {
+ if (item.category in output) {
+ item.character = String.fromCodePoint(
+ parseInt(item.codepoint.replace(/^U\+0*/, ''), 16)
+ )
+ output[item.category].push(item)
+ }
+ }
+
+ return output
+}
diff --git a/services/web/frontend/stylesheets/bootstrap-5/modules/symbol-palette.scss b/services/web/frontend/stylesheets/bootstrap-5/modules/symbol-palette.scss
index dd6d5f64f8..3ff65c9743 100644
--- a/services/web/frontend/stylesheets/bootstrap-5/modules/symbol-palette.scss
+++ b/services/web/frontend/stylesheets/bootstrap-5/modules/symbol-palette.scss
@@ -154,19 +154,18 @@
.symbol-palette-close-button-outer {
display: flex;
+ align-items: center;
+ margin-right: var(--spacing-05);
}
.symbol-palette-close-button {
--bs-btn-close-color: var(--symbol-palette-color);
[data-theme='default'] & {
+ --bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23ffffff'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e");
filter: var(--bs-btn-close-white-filter);
}
- margin-top: var(--spacing-04);
- margin-left: var(--spacing-05);
- margin-right: var(--spacing-03);
-
.symbol-palette-unavailable & {
visibility: hidden;
}
diff --git a/services/web/modules/symbol-palette/index.mjs b/services/web/modules/symbol-palette/index.mjs
new file mode 100644
index 0000000000..3a412c2eec
--- /dev/null
+++ b/services/web/modules/symbol-palette/index.mjs
@@ -0,0 +1,2 @@
+import logger from '@overleaf/logger'
+logger.debug({}, 'Enable Symbol Palette')