mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-04 18:07:17 +00:00
Add Symbol Palette component, data and stories (#4027)
GitOrigin-RevId: b00128bc087e2ebe9911fa19b7e62fd4bb492226
This commit is contained in:
parent
bb88af80cf
commit
0360d01aeb
24 changed files with 1650 additions and 4 deletions
|
@ -19,6 +19,11 @@
|
|||
"cannot_invite_non_user": "",
|
||||
"cannot_invite_self": "",
|
||||
"cannot_verify_user_not_robot": "",
|
||||
"category_arrows": "",
|
||||
"category_greek": "",
|
||||
"category_misc": "",
|
||||
"category_operators": "",
|
||||
"category_relations": "",
|
||||
"change_or_cancel-cancel": "",
|
||||
"change_or_cancel-change": "",
|
||||
"change_or_cancel-or": "",
|
||||
|
@ -185,6 +190,7 @@
|
|||
"no_new_commits_in_github": "",
|
||||
"no_other_projects_found": "",
|
||||
"no_preview_available": "",
|
||||
"no_symbols_found": "",
|
||||
"normal": "",
|
||||
"off": "",
|
||||
"ok": "",
|
||||
|
@ -240,6 +246,7 @@
|
|||
"revoke": "",
|
||||
"revoke_invite": "",
|
||||
"run_syntax_check_now": "",
|
||||
"search": "",
|
||||
"select_a_file": "",
|
||||
"select_a_project": "",
|
||||
"select_an_output_file": "",
|
||||
|
|
92
services/web/frontend/fonts/STIXTwoMath/LICENSE.txt
Normal file
92
services/web/frontend/fonts/STIXTwoMath/LICENSE.txt
Normal file
|
@ -0,0 +1,92 @@
|
|||
Copyright 2001-2021 The STIX Fonts Project Authors (https://github.com/stipub/stixfonts), with Reserved Font Name "TM Math". STIX Fonts™ is a trademark of The Institute of Electrical and Electronics Engineers, Inc.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
Binary file not shown.
5
services/web/frontend/fonts/stix-two-math.css
Normal file
5
services/web/frontend/fonts/stix-two-math.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
@font-face {
|
||||
font-family: "Stix Two Math";
|
||||
src: url("./STIXTwoMath/STIXTwoMath-Regular.woff2")
|
||||
format("woff2");
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react'
|
||||
import { TabPanels, TabPanel } from '@reach/tabs'
|
||||
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,
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// not searching: show the symbols grouped by category
|
||||
if (!filteredSymbols) {
|
||||
return (
|
||||
<TabPanels>
|
||||
{categories.map(category => (
|
||||
<TabPanel key={category.id} tabIndex={-1}>
|
||||
<SymbolPaletteItems
|
||||
items={categorisedSymbols[category.id]}
|
||||
handleSelect={handleSelect}
|
||||
focusInput={focusInput}
|
||||
/>
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
)
|
||||
}
|
||||
|
||||
// searching with no matches: show a message
|
||||
if (!filteredSymbols.length) {
|
||||
return <div className="symbol-palette-empty">{t('no_symbols_found')}</div>
|
||||
}
|
||||
|
||||
// searching with matches: show the matched symbols
|
||||
return (
|
||||
<SymbolPaletteItems
|
||||
items={filteredSymbols}
|
||||
handleSelect={handleSelect}
|
||||
focusInput={focusInput}
|
||||
/>
|
||||
)
|
||||
}
|
||||
SymbolPaletteBody.propTypes = {
|
||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
categorisedSymbols: PropTypes.object,
|
||||
filteredSymbols: PropTypes.arrayOf(PropTypes.object),
|
||||
handleSelect: PropTypes.func.isRequired,
|
||||
focusInput: PropTypes.func.isRequired,
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import { Tabs } from '@reach/tabs'
|
||||
import React, { 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 '@reach/tabs/styles.css'
|
||||
import SymbolPaletteBody from './symbol-palette-body'
|
||||
import SymbolPaletteTabs from './symbol-palette-tabs'
|
||||
|
||||
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])
|
||||
|
||||
// group the symbols by category
|
||||
const categorisedSymbols = useMemo(
|
||||
() => buildCategorisedSymbols(categories),
|
||||
[categories]
|
||||
)
|
||||
|
||||
// select symbols which match the input
|
||||
const filteredSymbols = useMemo(() => {
|
||||
if (input === '') {
|
||||
return null
|
||||
}
|
||||
|
||||
return matchSorter(symbols, input, {
|
||||
keys: ['command', 'description'],
|
||||
threshold: matchSorter.rankings.CONTAINS,
|
||||
})
|
||||
}, [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 (
|
||||
<Tabs>
|
||||
<div className="symbol-palette">
|
||||
<div className="symbol-palette-header">
|
||||
<SymbolPaletteTabs
|
||||
categories={categories}
|
||||
disabled={input.length > 0}
|
||||
/>
|
||||
<SymbolPaletteSearch setInput={setInput} inputRef={inputRef} />
|
||||
</div>
|
||||
<div className="symbol-palette-body">
|
||||
<SymbolPaletteBody
|
||||
categories={categories}
|
||||
categorisedSymbols={categorisedSymbols}
|
||||
filteredSymbols={filteredSymbols}
|
||||
handleSelect={handleSelect}
|
||||
focusInput={focusInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
SymbolPaletteContent.propTypes = {
|
||||
handleSelect: PropTypes.func.isRequired,
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import React, { useEffect, useRef } from 'react'
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default function SymbolPaletteItem({
|
||||
focused,
|
||||
handleSelect,
|
||||
handleKeyDown,
|
||||
symbol,
|
||||
}) {
|
||||
const buttonRef = useRef(null)
|
||||
|
||||
// call focus() on this item when appropriate
|
||||
useEffect(() => {
|
||||
if (
|
||||
focused &&
|
||||
buttonRef.current &&
|
||||
document.activeElement?.closest('.symbol-palette-items')
|
||||
) {
|
||||
buttonRef.current.focus()
|
||||
}
|
||||
}, [focused])
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
placement="top"
|
||||
trigger={['hover', 'focus']}
|
||||
overlay={
|
||||
<Tooltip id={`tooltip-symbol-${symbol.codepoint}`}>
|
||||
<div className="symbol-palette-item-description">
|
||||
{symbol.description}
|
||||
</div>
|
||||
<div className="symbol-palette-item-command" aria-hidden="true">
|
||||
{symbol.command}
|
||||
</div>
|
||||
{symbol.notes && (
|
||||
<div className="symbol-palette-item-notes">{symbol.notes}</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<button
|
||||
key={symbol.codepoint}
|
||||
className="symbol-palette-item"
|
||||
onClick={() => handleSelect(symbol)}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={focused ? 0 : -1}
|
||||
ref={buttonRef}
|
||||
role="option"
|
||||
aria-selected={focused}
|
||||
>
|
||||
<span aria-hidden="true">{symbol.character}</span>
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
)
|
||||
}
|
||||
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,
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import SymbolPaletteItem from './symbol-palette-item'
|
||||
|
||||
export default function SymbolPaletteItems({
|
||||
items,
|
||||
handleSelect,
|
||||
focusInput,
|
||||
}) {
|
||||
const [focusedIndex, setFocusedIndex] = useState(0)
|
||||
|
||||
// reset the focused item when the list of items changes
|
||||
useEffect(() => {
|
||||
setFocusedIndex(0)
|
||||
}, [items])
|
||||
|
||||
// navigate through items with left and right arrows
|
||||
const handleKeyDown = useCallback(
|
||||
event => {
|
||||
if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (event.key) {
|
||||
// focus previous item
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowUp':
|
||||
setFocusedIndex(index => (index > 0 ? index - 1 : items.length - 1))
|
||||
break
|
||||
|
||||
// focus next item
|
||||
case 'ArrowRight':
|
||||
case 'ArrowDown':
|
||||
setFocusedIndex(index => (index < items.length - 1 ? index + 1 : 0))
|
||||
break
|
||||
|
||||
// focus first item
|
||||
case 'Home':
|
||||
setFocusedIndex(0)
|
||||
break
|
||||
|
||||
// focus last item
|
||||
case 'End':
|
||||
setFocusedIndex(items.length - 1)
|
||||
break
|
||||
|
||||
// allow the default action
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
break
|
||||
|
||||
// any other key returns focus to the input
|
||||
default:
|
||||
focusInput()
|
||||
break
|
||||
}
|
||||
},
|
||||
[focusInput, items.length]
|
||||
)
|
||||
|
||||
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}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
SymbolPaletteItems.propTypes = {
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
codepoint: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
handleSelect: PropTypes.func.isRequired,
|
||||
focusInput: PropTypes.func.isRequired,
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PropTypes from 'prop-types'
|
||||
import useDebounce from '../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()
|
||||
|
||||
return (
|
||||
<input
|
||||
className="symbol-palette-search"
|
||||
type="search"
|
||||
ref={inputRef}
|
||||
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,
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react'
|
||||
import { TabList, Tab } from '@reach/tabs'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default function SymbolPaletteTabs({ categories, disabled }) {
|
||||
return (
|
||||
<TabList aria-label="Symbol Categories">
|
||||
{categories.map(category => (
|
||||
<Tab key={category.id} disabled={disabled}>
|
||||
{category.label}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
)
|
||||
}
|
||||
SymbolPaletteTabs.propTypes = {
|
||||
categories: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import SymbolPaletteContent from './symbol-palette-content'
|
||||
|
||||
export default function SymbolPalette({ show, handleSelect }) {
|
||||
if (!show) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <SymbolPaletteContent handleSelect={handleSelect} />
|
||||
}
|
||||
SymbolPalette.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
handleSelect: PropTypes.func.isRequired,
|
||||
}
|
|
@ -0,0 +1,821 @@
|
|||
[
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\alpha",
|
||||
"codepoint": "U+1D6FC",
|
||||
"description": "lowercase Greek letter alpha",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\beta",
|
||||
"codepoint": "U+1D6FD",
|
||||
"description": "lowercase Greek letter beta",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\gamma",
|
||||
"codepoint": "U+1D6FE",
|
||||
"description": "lowercase Greek letter gamma",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\delta",
|
||||
"codepoint": "U+1D6FF",
|
||||
"description": "lowercase Greek letter delta",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\varepsilon",
|
||||
"codepoint": "U+1D700",
|
||||
"description": "lowercase Greek letter epsilon, varepsilon",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\epsilon",
|
||||
"codepoint": "U+1D716",
|
||||
"description": "lowercase Greek letter epsilon lunate",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\zeta",
|
||||
"codepoint": "U+1D701",
|
||||
"description": "lowercase Greek letter zeta",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\eta",
|
||||
"codepoint": "U+1D702",
|
||||
"description": "lowercase Greek letter eta",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\vartheta",
|
||||
"codepoint": "U+1D717",
|
||||
"description": "lowercase Greek letter curly theta, vartheta",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\theta",
|
||||
"codepoint": "U+1D703",
|
||||
"description": "lowercase Greek letter theta",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\iota",
|
||||
"codepoint": "U+1D704",
|
||||
"description": "lowercase Greek letter iota",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\kappa",
|
||||
"codepoint": "U+1D705",
|
||||
"description": "lowercase Greek letter kappa",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\lambda",
|
||||
"codepoint": "U+1D706",
|
||||
"description": "lowercase Greek letter lambda",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\mu",
|
||||
"codepoint": "U+1D707",
|
||||
"description": "lowercase Greek letter mu",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\nu",
|
||||
"codepoint": "U+1D708",
|
||||
"description": "lowercase Greek letter nu",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\xi",
|
||||
"codepoint": "U+1D709",
|
||||
"description": "lowercase Greek letter xi",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\pi",
|
||||
"codepoint": "U+1D70B",
|
||||
"description": "lowercase Greek letter pi",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\varrho",
|
||||
"codepoint": "U+1D71A",
|
||||
"description": "lowercase Greek letter varrho",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\rho",
|
||||
"codepoint": "U+1D70C",
|
||||
"description": "lowercase Greek letter rho",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\sigma",
|
||||
"codepoint": "U+1D70E",
|
||||
"description": "lowercase Greek letter sigma",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\varsigma",
|
||||
"codepoint": "U+1D70D",
|
||||
"description": "lowercase Greek letter final sigma, varsigma",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\tau",
|
||||
"codepoint": "U+1D70F",
|
||||
"description": "lowercase Greek letter tau",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\upsilon",
|
||||
"codepoint": "U+1D710",
|
||||
"description": "lowercase Greek letter upsilon",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\phi",
|
||||
"codepoint": "U+1D719",
|
||||
"description": "lowercase Greek letter phi",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\varphi",
|
||||
"codepoint": "U+1D711",
|
||||
"description": "lowercase Greek letter varphi",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\chi",
|
||||
"codepoint": "U+1D712",
|
||||
"description": "lowercase Greek letter chi",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\psi",
|
||||
"codepoint": "U+1D713",
|
||||
"description": "lowercase Greek letter psi",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\omega",
|
||||
"codepoint": "U+1D714",
|
||||
"description": "lowercase Greek letter omega",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\Gamma",
|
||||
"codepoint": "U+00393",
|
||||
"description": "uppercase Greek letter Gamma",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\Delta",
|
||||
"codepoint": "U+00394",
|
||||
"description": "uppercase Greek letter Delta",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\Theta",
|
||||
"codepoint": "U+00398",
|
||||
"description": "uppercase Greek letter Theta",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\Lambda",
|
||||
"codepoint": "U+0039B",
|
||||
"description": "uppercase Greek letter Lambda",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\Xi",
|
||||
"codepoint": "U+0039E",
|
||||
"description": "uppercase Greek letter Xi",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\Pi",
|
||||
"codepoint": "U+003A0",
|
||||
"description": "uppercase Greek letter Pi",
|
||||
"notes": "Use \\prod for the product."
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\Sigma",
|
||||
"codepoint": "U+003A3",
|
||||
"description": "uppercase Greek letter Sigma",
|
||||
"notes": "Use \\sum for the sum."
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\Upsilon",
|
||||
"codepoint": "U+003A5",
|
||||
"description": "uppercase Greek letter Upsilon",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\Phi",
|
||||
"codepoint": "U+003A6",
|
||||
"description": "uppercase Greek letter Phi",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\Psi",
|
||||
"codepoint": "U+003A8",
|
||||
"description": "uppercase Greek letter Psi",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Greek",
|
||||
"command": "\\Omega",
|
||||
"codepoint": "U+003A9",
|
||||
"description": "uppercase Greek letter Omega",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Relations",
|
||||
"command": "\\neq",
|
||||
"codepoint": "U+02260",
|
||||
"description": "not equal",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Relations",
|
||||
"command": "\\leq",
|
||||
"codepoint": "U+02264",
|
||||
"description": "less than or equal",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Relations",
|
||||
"command": "\\geq",
|
||||
"codepoint": "U+02265",
|
||||
"description": "greater than or equal",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Relations",
|
||||
"command": "\\ll",
|
||||
"codepoint": "U+0226A",
|
||||
"description": "much less than",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Relations",
|
||||
"command": "\\gg",
|
||||
"codepoint": "U+0226B",
|
||||
"description": "much greater than",
|
||||
"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",
|
||||
"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",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Arrows",
|
||||
"command": "\\rightarrow",
|
||||
"codepoint": "U+02192",
|
||||
"description": "rightward arrow",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Arrows",
|
||||
"command": "\\leftrightarrow",
|
||||
"codepoint": "U+02194",
|
||||
"description": "left and right arrow",
|
||||
"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",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Arrows",
|
||||
"command": "\\Rightarrow",
|
||||
"codepoint": "U+021D2",
|
||||
"description": "implies",
|
||||
"notes": ""
|
||||
},
|
||||
{
|
||||
"category": "Arrows",
|
||||
"command": "\\Leftrightarrow",
|
||||
"codepoint": "U+021D4",
|
||||
"description": "left and right double arrow",
|
||||
"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": "trangle",
|
||||
"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": ""
|
||||
}
|
||||
]
|
|
@ -0,0 +1,17 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
export default function useDebounce(value, delay = 0) {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
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
|
||||
}
|
18
services/web/frontend/stories/symbol-palette.stories.js
Normal file
18
services/web/frontend/stories/symbol-palette.stories.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import React from 'react'
|
||||
|
||||
import SymbolPalette from '../js/features/symbol-palette/components/symbol-palette'
|
||||
|
||||
export const Interactive = args => {
|
||||
return <SymbolPalette {...args} />
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Symbol Palette',
|
||||
component: SymbolPalette,
|
||||
args: {
|
||||
show: true,
|
||||
},
|
||||
argTypes: {
|
||||
handleSelect: { action: 'handleSelect' },
|
||||
},
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
@import './editor/publish-modal.less';
|
||||
@import './editor/outline.less';
|
||||
@import './editor/logs.less';
|
||||
@import './editor/symbol-palette.less';
|
||||
|
||||
@ui-layout-toggler-def-height: 50px;
|
||||
@ui-resizer-size: 7px;
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
.symbol-palette {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: @symbol-palette-bg;
|
||||
color: @symbol-palette-color;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.symbol-palette-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-family: @font-family-sans-serif;
|
||||
font-size: 16px;
|
||||
background: @symbol-palette-header-background;
|
||||
}
|
||||
|
||||
.symbol-palette-header [data-reach-tab] {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.symbol-palette-header [data-reach-tab][data-selected] {
|
||||
background: @symbol-palette-selected-tab-bg;
|
||||
}
|
||||
|
||||
.symbol-palette-body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.symbol-palette-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.symbol-palette-item {
|
||||
font-family: 'Stix Two Math', serif;
|
||||
font-size: 24px;
|
||||
line-height: 42px;
|
||||
height: 42px;
|
||||
width: 42px;
|
||||
margin: 5px;
|
||||
color: @symbol-palette-item-color;
|
||||
background: @symbol-palette-item-bg;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.symbol-palette-item-command {
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.symbol-palette-item-notes {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.symbol-palette-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.symbol-palette-search {
|
||||
color: @symbol-palette-search-color;
|
||||
border-radius: 21px;
|
||||
border: none;
|
||||
padding: 2px 10px;
|
||||
margin: 5px;
|
||||
line-height: 1;
|
||||
}
|
|
@ -127,3 +127,12 @@
|
|||
@chat-new-message-textarea-bg: #fff;
|
||||
@chat-new-message-textarea-color: @ol-blue-gray-6;
|
||||
@chat-new-message-border-color: @ol-blue-gray-1;
|
||||
|
||||
// Symbol Palette
|
||||
@symbol-palette-bg: #fff;
|
||||
@symbol-palette-color: @ol-blue-gray-3;
|
||||
@symbol-palette-header-background: #fff;
|
||||
@symbol-palette-item-bg: @gray-lighter;
|
||||
@symbol-palette-item-color: @ol-blue-gray-5;
|
||||
@symbol-palette-search-color: @ol-blue-gray-4;
|
||||
@symbol-palette-selected-tab-bg: @gray-lighter;
|
||||
|
|
|
@ -1112,3 +1112,12 @@
|
|||
|
||||
// Input suggestions
|
||||
@input-suggestion-v-offset: 4px;
|
||||
|
||||
// Symbol Palette
|
||||
@symbol-palette-bg: @ol-blue-gray-4;
|
||||
@symbol-palette-color: #fff;
|
||||
@symbol-palette-header-background: @ol-blue-gray-5;
|
||||
@symbol-palette-item-bg: @ol-blue-gray-5;
|
||||
@symbol-palette-item-color: #fff;
|
||||
@symbol-palette-search-color: @ol-blue-gray-4;
|
||||
@symbol-palette-selected-tab-bg: @ol-blue-gray-4;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
@import (less) '../fonts/lato.css';
|
||||
@import (less) '../fonts/merriweather.css';
|
||||
@import (less) '../fonts/source-code-pro.css';
|
||||
@import (less) '../fonts/stix-two-math.css';
|
||||
|
||||
@is-overleaf-light: false;
|
||||
@show-rich-text: true;
|
||||
|
|
|
@ -1429,6 +1429,13 @@
|
|||
"hotkey_toggle_review_panel": "Toggle review panel",
|
||||
"hotkey_toggle_track_changes": "Toggle track changes",
|
||||
"hotkey_add_a_comment": "Add a comment",
|
||||
"category_greek": "Greek",
|
||||
"category_arrows": "Arrows",
|
||||
"category_operators": "Operators",
|
||||
"category_relations": "Relations",
|
||||
"category_misc": "Misc",
|
||||
"no_symbols_found": "No symbols found",
|
||||
"search": "Search",
|
||||
"also": "Also",
|
||||
"add_email": "Add Email"
|
||||
}
|
||||
|
|
80
services/web/package-lock.json
generated
80
services/web/package-lock.json
generated
|
@ -4419,6 +4419,38 @@
|
|||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
|
||||
},
|
||||
"@reach/auto-id": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.15.0.tgz",
|
||||
"integrity": "sha512-iACaCcZeqAhDf4OOwJpmHHS/LaRj9z3Ip8JmlhpCrFWV2YOIiiZk42amlBZX6CKH66Md+eriYZQk3TyAjk6Oxg==",
|
||||
"requires": {
|
||||
"@reach/utils": "0.15.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
|
||||
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@reach/descendants": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@reach/descendants/-/descendants-0.15.0.tgz",
|
||||
"integrity": "sha512-MGs+7sGjx07sm29Wc2d/Ya72qwatNVHofnYwwHMT6LImv99kE5TQMCgkMSDeRwqQxHURmcjb4I7Tab+ahvCGiw==",
|
||||
"requires": {
|
||||
"@reach/utils": "0.15.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
|
||||
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@reach/router": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@reach/router/-/router-1.3.4.tgz",
|
||||
|
@ -4431,6 +4463,41 @@
|
|||
"react-lifecycles-compat": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"@reach/tabs": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@reach/tabs/-/tabs-0.15.0.tgz",
|
||||
"integrity": "sha512-W3FZw0DMGChwIRDBZYCw66NdiGkyapYs6xjOqQV2Ph7DMaMQ0lYck5bn40EtVi3O5x+8cAKmV+ubbHVUh+WSig==",
|
||||
"requires": {
|
||||
"@reach/auto-id": "0.15.0",
|
||||
"@reach/descendants": "0.15.0",
|
||||
"@reach/utils": "0.15.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
|
||||
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@reach/utils": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.15.0.tgz",
|
||||
"integrity": "sha512-JHHN7T5ucFiuQbqkgv8ECbRWKfRiJxrO/xHR3fHf+f2C7mVs/KkJHhYtovS1iEapR4silygX9PY0+QUmHPOTYw==",
|
||||
"requires": {
|
||||
"tiny-warning": "^1.0.3",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
|
||||
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@react-dnd/asap": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz",
|
||||
|
@ -18220,7 +18287,7 @@
|
|||
"functional-red-black-tree": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
|
||||
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
|
||||
"integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
|
||||
"dev": true
|
||||
},
|
||||
"functions-have-names": {
|
||||
|
@ -23121,7 +23188,7 @@
|
|||
"microtime-nodejs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/microtime-nodejs/-/microtime-nodejs-1.0.0.tgz",
|
||||
"integrity": "sha1-iFlASvLipGKhXJzWvyxORo2r2+g="
|
||||
"integrity": "sha512-SthP/4JW6HUIZfgM0nadNtwKm/WMH0+z1i4RsPDnud+UasjoABzSkCk3eMhIRzipgwPhkdAYpTI69X4II4j1pA=="
|
||||
},
|
||||
"miller-rabin": {
|
||||
"version": "4.0.1",
|
||||
|
@ -24128,7 +24195,7 @@
|
|||
"natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
"integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"dev": true
|
||||
},
|
||||
"ncp": {
|
||||
|
@ -29783,7 +29850,7 @@
|
|||
"require-like": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz",
|
||||
"integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==",
|
||||
"integrity": "sha1-rW8wwTvs15cBDEaK+ndcDAprR/o=",
|
||||
"dev": true
|
||||
},
|
||||
"require-main-filename": {
|
||||
|
@ -33480,6 +33547,11 @@
|
|||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
|
||||
},
|
||||
"tlds": {
|
||||
"version": "1.210.0",
|
||||
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.210.0.tgz",
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
"@pollyjs/adapter-node-http": "^4.2.1",
|
||||
"@pollyjs/core": "^4.2.1",
|
||||
"@pollyjs/persister-fs": "^4.2.1",
|
||||
"@reach/tabs": "^0.15.0",
|
||||
"@sentry/browser": "^6.3.5",
|
||||
"@uppy/core": "^1.15.0",
|
||||
"@uppy/dashboard": "^1.11.0",
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import React from 'react'
|
||||
import { screen, render, fireEvent, waitFor } from '@testing-library/react'
|
||||
import SymbolPalette from '../../../../../frontend/js/features/symbol-palette/components/symbol-palette'
|
||||
|
||||
// eslint-disable-next-line mocha/no-skipped-tests
|
||||
describe.skip('symbol palette', function () {
|
||||
// let clock
|
||||
//
|
||||
// beforeEach(function () {
|
||||
// clock = sinon.useFakeTimers({
|
||||
// toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'],
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// afterEach(function () {
|
||||
// clock.restore()
|
||||
// })
|
||||
|
||||
it('handles keyboard interaction', async function () {
|
||||
this.timeout(10000)
|
||||
|
||||
const handleSelect = sinon.stub()
|
||||
|
||||
const { container } = render(
|
||||
<SymbolPalette show handleSelect={handleSelect} />
|
||||
)
|
||||
|
||||
// check the number of tabs
|
||||
const tabs = await screen.findAllByRole('tab')
|
||||
expect(tabs).to.have.length(5)
|
||||
|
||||
let selectedTab
|
||||
let symbols
|
||||
|
||||
// the first tab should be selected
|
||||
selectedTab = await screen.getByRole('tab', { selected: true })
|
||||
expect(selectedTab.textContent).to.equal('Greek')
|
||||
symbols = await screen.findAllByRole('option')
|
||||
expect(symbols).to.have.length(39)
|
||||
|
||||
// click to select the third tab
|
||||
tabs[2].click()
|
||||
selectedTab = await screen.getByRole('tab', { selected: true })
|
||||
expect(selectedTab.textContent).to.equal('Operators')
|
||||
symbols = await screen.findAllByRole('option')
|
||||
expect(symbols).to.have.length(20)
|
||||
|
||||
// press the left arrow to select the second tab
|
||||
fireEvent.keyDown(selectedTab, { key: 'ArrowLeft' })
|
||||
selectedTab = await screen.getByRole('tab', { selected: true })
|
||||
expect(selectedTab.textContent).to.equal('Arrows')
|
||||
symbols = await screen.findAllByRole('option')
|
||||
expect(symbols).to.have.length(16)
|
||||
|
||||
// select the search input
|
||||
const input = await screen.getByRole('searchbox')
|
||||
input.click()
|
||||
|
||||
// type in the search input
|
||||
fireEvent.change(input, { target: { value: 'pi' } })
|
||||
|
||||
// make sure all scheduled microtasks have executed
|
||||
// clock.runAll()
|
||||
|
||||
// wait for the symbols to be filtered
|
||||
await waitFor(async () => {
|
||||
symbols = await screen.findAllByRole('option')
|
||||
console.log(symbols.length)
|
||||
expect(symbols).to.have.length(2)
|
||||
})
|
||||
|
||||
// press Tab to select the symbols
|
||||
fireEvent.keyDown(container, { key: 'Tab' })
|
||||
|
||||
// get the selected symbol
|
||||
let selectedSymbol
|
||||
|
||||
selectedSymbol = await screen.getByRole('option', { selected: true })
|
||||
expect(selectedSymbol.textContent).to.equal('𝜋')
|
||||
|
||||
// move to the next symbol
|
||||
fireEvent.keyDown(selectedSymbol, { key: 'ArrowRight' })
|
||||
|
||||
// wait for the symbol to be selected
|
||||
selectedSymbol = await screen.getByRole('option', { selected: true })
|
||||
expect(selectedSymbol.textContent).to.equal('Π')
|
||||
|
||||
// click on the selected symbol
|
||||
selectedSymbol.click()
|
||||
|
||||
expect(handleSelect).to.have.been.calledWith({
|
||||
category: 'Greek',
|
||||
character: 'Π',
|
||||
codepoint: 'U+003A0',
|
||||
command: '\\Pi',
|
||||
description: 'uppercase Greek letter Pi',
|
||||
notes: 'Use \\prod for the product.',
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue