Compare commits

...

6 commits

13 changed files with 1453 additions and 5 deletions

View file

@ -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: {},

View file

@ -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 (
<div className="symbol-palette-panels">
{filteredSymbols.length ? (
<SymbolPaletteItems
items={filteredSymbols}
handleSelect={handleSelect}
focusInput={focusInput}
/>
) : (
<div className="symbol-palette-empty">{t('no_symbols_found')}</div>
)}
{categories.map(category => (
<div
key={category.id}
role="tabpanel"
className="symbol-palette-panel"
id={`symbol-palette-panel-${category.id}`}
aria-labelledby={`symbol-palette-tab-${category.id}`}
hidden
/>
))}
</div>
)
}
// not searching: show the symbols grouped by category
return (
<div className="symbol-palette-panels">
{categories.map((category) => (
<div
key={category.id}
id={`symbol-palette-panel-${category.id}`}
className="symbol-palette-panel"
role="tabpanel"
aria-labelledby={`symbol-palette-tab-${category.id}`}
hidden={category.id !== activeCategoryId}
>
<SymbolPaletteItems
items={categorisedSymbols[category.id]}
handleSelect={handleSelect}
focusInput={focusInput}
/>
</div>
))}
</div>
)
}
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,
}

View file

@ -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 (
<div className="symbol-palette-close-button-outer">
<button
type="button"
className="btn-close symbol-palette-close-button"
onClick={handleClick}
aria-label={t('close')}
>
</button>
</div>
)
}
SymbolPaletteCloseButton.propTypes = {
focusInput: PropTypes.func,
}

View file

@ -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 (
<div className="symbol-palette-container">
<div className="symbol-palette">
<div className="symbol-palette-header-outer">
<div className="symbol-palette-header">
<SymbolPaletteTabs
categories={categories}
activeCategoryId={activeCategoryId}
setActiveCategoryId={setActiveCategoryId}
/>
<div className="symbol-palette-header-group">
<SymbolPaletteSearch setInput={setInput} inputRef={inputRef} />
</div>
</div>
<div className="symbol-palette-header-group">
<SymbolPaletteCloseButton />
</div>
</div>
<div className="symbol-palette-body">
<SymbolPaletteBody
categories={categories}
categorisedSymbols={categorisedSymbols}
filteredSymbols={filteredSymbols}
handleSelect={handleSelect}
focusInput={focusInput}
activeCategoryId={activeCategoryId}
/>
</div>
</div>
</div>
)
}
SymbolPaletteContent.propTypes = {
handleSelect: PropTypes.func.isRequired,
}

View file

@ -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 (
<OLTooltip
id={`symbol-${symbol.codepoint}`}
description={
<div>
<div className="symbol-palette-item-description">
{symbol.description}
</div>
<div className="symbol-palette-item-command">
{symbol.command}
</div>
{symbol.notes && (
<div className="symbol-palette-item-notes">
{symbol.notes}
</div>
)}
</div>
}
overlayProps={{ placement: 'top', trigger: ['hover', 'focus'] }}
>
<button
key={symbol.codepoint}
className="symbol-palette-item"
onClick={() => handleSelect(symbol)}
onKeyDown={handleKeyDown}
tabIndex={focused ? 0 : -1}
ref={buttonRef}
role="option"
aria-label={symbol.description}
aria-selected={focused ? 'true' : 'false'}
>
{symbol.character}
</button>
</OLTooltip>
)
})
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

View file

@ -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 (
<div className="symbol-palette-items" role="listbox" aria-label="Symbols">
{items.map((symbol, index) => (
<SymbolPaletteItem
key={symbol.codepoint}
symbol={symbol}
handleSelect={symbol => {
handleSelect(symbol)
setFocusedIndex(index)
}}
handleKeyDown={handleKeyDown}
focused={index === focusedIndex}
ref={el => {
itemRefs.current[index] = el
}}
/>
))}
</div>
)
}
SymbolPaletteItems.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
codepoint: PropTypes.string.isRequired,
})
).isRequired,
handleSelect: PropTypes.func.isRequired,
focusInput: PropTypes.func.isRequired,
}

View file

@ -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 (
<OLFormControl
className="symbol-palette-search"
type="search"
ref={inputRefCallback}
id="symbol-palette-input"
aria-label="Search"
value={localInput}
placeholder={t('search') + '…'}
onChange={event => {
setLocalInput(event.target.value)
}}
/>
)
}
SymbolPaletteSearch.propTypes = {
setInput: PropTypes.func.isRequired,
inputRef: PropTypes.object.isRequired,
}

View file

@ -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 (
<div
role="tablist"
aria-label="Symbol Categories"
className="symbol-palette-tab-list"
tabIndex={0}
>
{categories.map((category, index) => {
const selected = activeCategoryId === category.id
return (
<button
key={category.id}
role="tab"
type="button"
className="symbol-palette-tab"
id={`symbol-palette-tab-${category.id}`}
aria-controls={`symbol-palette-panel-${category.id}`}
aria-selected={selected}
tabIndex={selected ? 0 : -1}
ref={(el) => (buttonRefs.current[index] = el)}
onClick={() => setActiveCategoryId(category.id)}
onKeyDown={(e) => handleKeyDown(e, index)}
>
{category.label}
</button>
)
})}
</div>
)
}
SymbolPaletteTabs.propTypes = {
categories: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
})).isRequired,
activeCategoryId: PropTypes.string.isRequired,
setActiveCategoryId: PropTypes.func.isRequired,
}

View file

@ -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 <SymbolPaletteContent handleSelect={handleSelect} />
}

View file

@ -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": ""
}
]

View file

@ -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
}

View file

@ -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;
}

View file

@ -0,0 +1,2 @@
import logger from '@overleaf/logger'
logger.debug({}, 'Enable Symbol Palette')