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')