Refactoring: apply prettier on save

This commit is contained in:
IRBorisov 2023-12-28 14:04:44 +03:00
parent 87d3152e6c
commit 40cb8b4ce8
222 changed files with 7652 additions and 7666 deletions

260
.vscode/settings.json vendored
View File

@ -1,137 +1,133 @@
{ {
"search.exclude": { "search.exclude": {
".mypy_cache/": true, ".mypy_cache/": true,
".pytest_cache/": true ".pytest_cache/": true
}, },
"python.testing.unittestArgs": [ "python.testing.unittestArgs": ["-v", "-s", "./tests", "-p", "test*.py"],
"-v", "python.testing.pytestEnabled": false,
"-s", "python.testing.unittestEnabled": true,
"./tests", "eslint.workingDirectories": [
"-p",
"test*.py"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true,
"eslint.workingDirectories": [
{
"mode": "auto"
}
],
"python.analysis.typeCheckingMode": "off",
"python.analysis.diagnosticSeverityOverrides": {
// "reportOptionalMemberAccess": "none"
},
"python.analysis.ignore": ["**/tests/**", "**/node_modules/**", "**/venv/**"],
"python.analysis.packageIndexDepths": [
{ {
"name": "django", "mode": "auto"
"depth": 5
} }
], ],
"colorize.include": [".tsx", ".jsx", ".ts", ".js"], "python.analysis.typeCheckingMode": "off",
"colorize.languages": [ "python.analysis.diagnosticSeverityOverrides": {
"typescript", // "reportOptionalMemberAccess": "none"
"javascript", },
"css", "python.analysis.ignore": ["**/tests/**", "**/node_modules/**", "**/venv/**"],
"typescriptreact", "python.analysis.packageIndexDepths": [
"javascriptreact" {
], "name": "django",
"cSpell.words": [ "depth": 5
"ablt", }
"accs", ],
"actv", "colorize.include": [".tsx", ".jsx", ".ts", ".js"],
"ADJF", "colorize.languages": [
"ADJS", "typescript",
"ADVB", "javascript",
"Analyse", "css",
"Backquote", "typescriptreact",
"BIGPR", "javascriptreact"
"cctext", ],
"CIHT", "cSpell.words": [
"clsx", "ablt",
"codemirror", "accs",
"Constituenta", "actv",
"csrftoken", "ADJF",
"cstlist", "ADJS",
"csttype", "ADVB",
"datv", "Analyse",
"Debool", "Backquote",
"Decart", "BIGPR",
"EMPTYSET", "cctext",
"exteor", "CIHT",
"femn", "clsx",
"filterset", "codemirror",
"forceatlas", "Constituenta",
"futr", "csrftoken",
"Grammeme", "cstlist",
"Grammemes", "csttype",
"GRND", "datv",
"impr", "Debool",
"inan", "Decart",
"indc", "Downvote",
"INFN", "EMPTYSET",
"Infr", "exteor",
"INTJ", "femn",
"Keymap", "filterset",
"lezer", "forceatlas",
"Litr", "futr",
"loct", "Grammeme",
"moprho", "Grammemes",
"nomn", "GRND",
"nooverlap", "impr",
"NPRO", "inan",
"NUMR", "indc",
"perfectivity", "INFN",
"ponomarev", "Infr",
"PRCL", "INTJ",
"PRTF", "Keymap",
"PRTS", "lezer",
"pssv", "Litr",
"pyconcept", "loct",
"pymorphy", "moprho",
"Quantor", "nomn",
"razdel", "nooverlap",
"reagraph", "NPRO",
"Reindex", "NUMR",
"rsedit", "perfectivity",
"rseditor", "ponomarev",
"rsform", "PRCL",
"rsforms", "PRTF",
"rslang", "PRTS",
"rstemplates", "pssv",
"SIDELIST", "pyconcept",
"signup", "pymorphy",
"Slng", "Quantor",
"SMALLPR", "razdel",
"tanstack", "reagraph",
"toastify", "Reindex",
"tooltipic", "rsedit",
"Viewset", "rseditor",
"viewsets", "rsform",
"wordform", "rsforms",
"Wordforms", "rslang",
"Булеан", "rstemplates",
"Бурбаки", "SIDELIST",
"Десинглетон", "signup",
"компаратив", "Slng",
"конституент", "SMALLPR",
"Конституента", "tanstack",
"конституенту", "toastify",
"конституенты", "tooltipic",
"неинтерпретируемый", "Upvote",
"неитерируемого", "Viewset",
"пересинтез", "viewsets",
"Родоструктурная", "wordform",
"Родоструктурное", "Wordforms",
"Синглетон", "Булеан",
"твор", "Бурбаки",
"Терминологизация", "Десинглетон",
"Цермелло", "компаратив",
"ЦИВТ", "конституент",
"Экстеор", "Конституента",
"Экстеора", "конституенту",
"Экстеоре" "конституенты",
], "неинтерпретируемый",
"cSpell.language": "en,ru", "неитерируемого",
"cSpell.ignorePaths": ["node_modules/**", "*.json"] "пересинтез",
} "Родоструктурная",
"Родоструктурное",
"Синглетон",
"твор",
"Терминологизация",
"Цермелло",
"ЦИВТ",
"Экстеор",
"Экстеора",
"Экстеоре"
],
"cSpell.language": "en,ru",
"cSpell.ignorePaths": ["node_modules/**", "*.json"]
}

View File

@ -0,0 +1,13 @@
{
"semi": true,
"useTabs": false,
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "none",
"arrowParens": "avoid",
"singleQuote": true,
"jsxSingleQuote": true,
"quoteProps": "consistent",
"bracketSameLine": false,
"bracketSpacing": true
}

View File

@ -20,35 +20,33 @@ import { globalIDs } from './utils/constants';
function Root() { function Root() {
const { viewportHeight, mainHeight, showScroll } = useConceptTheme(); const { viewportHeight, mainHeight, showScroll } = useConceptTheme();
return ( return (
<NavigationState> <NavigationState>
<div className='min-w-[30rem] clr-app antialiased'> <div className='min-w-[30rem] clr-app antialiased'>
<ConceptToaster
className='mt-[4rem] text-sm' //
autoClose={3000}
draggable={false}
pauseOnFocusLoss={false}
/>
<ConceptToaster <Navigation />
className='mt-[4rem] text-sm'
autoClose={3000}
draggable={false}
pauseOnFocusLoss={false}
/>
<Navigation />
<div id={globalIDs.main_scroll} <div
className='overscroll-none min-w-fit overflow-y-auto' id={globalIDs.main_scroll}
style={{ className='overflow-y-auto overscroll-none min-w-fit'
maxHeight: viewportHeight, style={{
overflowY: showScroll ? 'scroll': 'auto' maxHeight: viewportHeight,
}} overflowY: showScroll ? 'scroll' : 'auto'
> }}
<main >
className='flex flex-col items-center' <main className='flex flex-col items-center' style={{ minHeight: mainHeight }}>
style={{minHeight: mainHeight}} <Outlet />
> </main>
<Outlet /> <Footer />
</main> </div>
<Footer /> </div>
</div> </NavigationState>
</div> );
</NavigationState>);
} }
const router = createBrowserRouter([ const router = createBrowserRouter([
@ -59,48 +57,46 @@ const router = createBrowserRouter([
children: [ children: [
{ {
path: '', path: '',
element: <HomePage />, element: <HomePage />
}, },
{ {
path: 'login', path: 'login',
element: <LoginPage />, element: <LoginPage />
}, },
{ {
path: 'signup', path: 'signup',
element: <RegisterPage />, element: <RegisterPage />
}, },
{ {
path: 'restore-password', path: 'restore-password',
element: <RestorePasswordPage />, element: <RestorePasswordPage />
}, },
{ {
path: 'profile', path: 'profile',
element: <UserProfilePage />, element: <UserProfilePage />
}, },
{ {
path: 'manuals', path: 'manuals',
element: <ManualsPage />, element: <ManualsPage />
}, },
{ {
path: 'library', path: 'library',
element: <LibraryPage />, element: <LibraryPage />
}, },
{ {
path: 'library/create', path: 'library/create',
element: <CreateRSFormPage />, element: <CreateRSFormPage />
}, },
{ {
path: 'rsforms/:id', path: 'rsforms/:id',
element: <RSFormPage />, element: <RSFormPage />
}, }
] ]
}, }
]); ]);
function App () { function App() {
return ( return <RouterProvider router={router} />;
<RouterProvider router={router} />
);
} }
export default App; export default App;

View File

@ -11,13 +11,10 @@ import { UsersState } from '@/context/UsersContext';
import ErrorFallback from './components/ErrorFallback'; import ErrorFallback from './components/ErrorFallback';
pdfjs.GlobalWorkerOptions.workerSrc = new URL( pdfjs.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.js', import.meta.url).toString();
'pdfjs-dist/build/pdf.worker.min.js',
import.meta.url,
).toString();
const resetState = () => { const resetState = () => {
console.log('Resetting state after error fallback') console.log('Resetting state after error fallback');
}; };
const logError = (error: Error, info: { componentStack?: string | null | undefined }) => { const logError = (error: Error, info: { componentStack?: string | null | undefined }) => {
@ -27,6 +24,7 @@ const logError = (error: Error, info: { componentStack?: string | null | undefin
} }
}; };
// prettier-ignore
function GlobalProviders({ children }: { children: React.ReactNode }) { function GlobalProviders({ children }: { children: React.ReactNode }) {
return ( return (
<ErrorBoundary <ErrorBoundary
@ -50,4 +48,4 @@ function GlobalProviders({ children }: { children: React.ReactNode }) {
</ErrorBoundary>); </ErrorBoundary>);
} }
export default GlobalProviders; export default GlobalProviders;

View File

@ -4,47 +4,54 @@ import { globalIDs } from '@/utils/constants';
import { CProps } from '../props'; import { CProps } from '../props';
interface ButtonProps interface ButtonProps extends CProps.Control, CProps.Colors, CProps.Button {
extends CProps.Control, CProps.Colors, CProps.Button { text?: string;
text?: string icon?: React.ReactNode;
icon?: React.ReactNode
dense?: boolean dense?: boolean;
loading?: boolean loading?: boolean;
} }
function Button({ function Button({
text, icon, title, text,
loading, dense, disabled, noBorder, noOutline, icon,
title,
loading,
dense,
disabled,
noBorder,
noOutline,
colors = 'clr-btn-default', colors = 'clr-btn-default',
className, className,
...restProps ...restProps
}: ButtonProps) { }: ButtonProps) {
return ( return (
<button type='button' <button
disabled={disabled ?? loading} type='button'
className={clsx( disabled={disabled ?? loading}
'inline-flex gap-2 items-center justify-center', className={clsx(
'select-none disabled:cursor-not-allowed', 'inline-flex gap-2 items-center justify-center',
{ 'select-none disabled:cursor-not-allowed',
'border rounded': !noBorder, {
'px-1': dense, 'border rounded': !noBorder,
'px-3 py-2': !dense, 'px-1': dense,
'cursor-progress': loading, 'px-3 py-2': !dense,
'cursor-pointer': !loading, 'cursor-progress': loading,
'outline-none': noOutline, 'cursor-pointer': !loading,
'clr-outline': !noOutline, 'outline-none': noOutline,
}, 'clr-outline': !noOutline
className, },
colors className,
)} colors
data-tooltip-id={title ? globalIDs.tooltip : undefined} )}
data-tooltip-content={title} data-tooltip-id={title ? globalIDs.tooltip : undefined}
{...restProps} data-tooltip-content={title}
> {...restProps}
{icon ? icon : null} >
{text ? <span className='font-semibold'>{text}</span> : null} {icon ? icon : null}
</button>); {text ? <span className='font-semibold'>{text}</span> : null}
</button>
);
} }
export default Button; export default Button;

View File

@ -7,21 +7,16 @@ import { CheckboxCheckedIcon } from '../Icons';
import { CProps } from '../props'; import { CProps } from '../props';
import Label from './Label'; import Label from './Label';
export interface CheckboxProps export interface CheckboxProps extends Omit<CProps.Button, 'value' | 'onClick'> {
extends Omit<CProps.Button, 'value' | 'onClick'> { label?: string;
label?: string disabled?: boolean;
disabled?: boolean
value: boolean value: boolean;
setValue?: (newValue: boolean) => void setValue?: (newValue: boolean) => void;
} }
function Checkbox({ function Checkbox({ id, disabled, label, title, className, value, setValue, ...restProps }: CheckboxProps) {
id, disabled, label, title, const cursor = useMemo(() => {
className, value, setValue, ...restProps
}: CheckboxProps) {
const cursor = useMemo(
() => {
if (disabled) { if (disabled) {
return 'cursor-not-allowed'; return 'cursor-not-allowed';
} else if (setValue) { } else if (setValue) {
@ -40,32 +35,31 @@ function Checkbox({
} }
return ( return (
<button type='button' id={id} <button
className={clsx( type='button'
'flex items-center gap-2', id={id}
'outline-none', className={clsx('flex items-center gap-2', 'outline-none', 'text-start', cursor, className)}
'text-start', disabled={disabled}
cursor, onClick={handleClick}
className data-tooltip-id={title ? globalIDs.tooltip : undefined}
)} data-tooltip-content={title}
disabled={disabled} {...restProps}
onClick={handleClick} >
data-tooltip-id={title ? (globalIDs.tooltip) : undefined} <div
data-tooltip-content={title} className={clsx('max-w-[1rem] min-w-[1rem] h-4', 'border rounded-sm', {
{...restProps} 'clr-primary': value !== false,
> 'clr-app': value === false
<div className={clsx( })}
'max-w-[1rem] min-w-[1rem] h-4', >
'border rounded-sm', {value ? (
{ <div className='mt-[1px] ml-[1px]'>
'clr-primary': value !== false, <CheckboxCheckedIcon />
'clr-app': value === false </div>
} ) : null}
)}> </div>
{value ? <div className='mt-[1px] ml-[1px]'><CheckboxCheckedIcon /></div> : null} <Label className={cursor} text={label} htmlFor={id} />
</div> </button>
<Label className={cursor} text={label} htmlFor={id} /> );
</button>);
} }
export default Checkbox; export default Checkbox;

View File

@ -5,18 +5,14 @@ import { ThreeDots } from 'react-loader-spinner';
import { useConceptTheme } from '@/context/ThemeContext'; import { useConceptTheme } from '@/context/ThemeContext';
interface ConceptLoaderProps { interface ConceptLoaderProps {
size?: number size?: number;
} }
export function ConceptLoader({size=10}: ConceptLoaderProps) { export function ConceptLoader({ size = 10 }: ConceptLoaderProps) {
const {colors} = useConceptTheme(); const { colors } = useConceptTheme();
return ( return (
<div className='flex justify-center'> <div className='flex justify-center'>
<ThreeDots <ThreeDots color={colors.bgSelected} height={size * 10} width={size * 10} radius={size} />
color={colors.bgSelected} </div>
height={size*10} );
width={size*10} }
radius={size}
/>
</div>);
}

View File

@ -4,30 +4,28 @@ import { CProps } from '../props';
import Overlay from './Overlay'; import Overlay from './Overlay';
import TextInput from './TextInput'; import TextInput from './TextInput';
interface ConceptSearchProps interface ConceptSearchProps extends CProps.Styling {
extends CProps.Styling { value: string;
value: string onChange?: (newValue: string) => void;
onChange?: (newValue: string) => void noBorder?: boolean;
noBorder?: boolean
} }
function ConceptSearch({ value, onChange, noBorder, ...restProps }: ConceptSearchProps) { function ConceptSearch({ value, onChange, noBorder, ...restProps }: ConceptSearchProps) {
return ( return (
<div {...restProps}> <div {...restProps}>
<Overlay <Overlay position='top-[-0.125rem] left-3 translate-y-1/2' className='pointer-events-none clr-text-controls'>
position='top-[-0.125rem] left-3 translate-y-1/2' <BiSearchAlt2 size='1.25rem' />
className='pointer-events-none clr-text-controls' </Overlay>
> <TextInput
<BiSearchAlt2 size='1.25rem' /> noOutline
</Overlay> placeholder='Поиск'
<TextInput noOutline className='pl-10'
placeholder='Поиск' noBorder={noBorder}
className='pl-10' value={value}
noBorder={noBorder} onChange={event => (onChange ? onChange(event.target.value) : undefined)}
value={value} />
onChange={event => (onChange ? onChange(event.target.value) : undefined)} </div>
/> );
</div>);
} }
export default ConceptSearch; export default ConceptSearch;

View File

@ -4,33 +4,30 @@ import { Tab } from 'react-tabs';
import { globalIDs } from '@/utils/constants'; import { globalIDs } from '@/utils/constants';
interface ConceptTabProps interface ConceptTabProps extends Omit<TabProps, 'children'> {
extends Omit<TabProps, 'children'> { label?: string;
label?: string
} }
function ConceptTab({ function ConceptTab({ label, title, className, ...otherProps }: ConceptTabProps) {
label, title, className,
...otherProps
}: ConceptTabProps) {
return ( return (
<Tab <Tab
className={clsx( className={clsx(
'min-w-[6rem]', 'min-w-[6rem]',
'px-2 py-1 flex justify-center', 'px-2 py-1 flex justify-center',
'clr-tab', 'clr-tab',
'text-sm whitespace-nowrap small-caps font-semibold', 'text-sm whitespace-nowrap small-caps font-semibold',
'select-none hover:cursor-pointer', 'select-none hover:cursor-pointer',
className className
)} )}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined} data-tooltip-id={title ? globalIDs.tooltip : undefined}
data-tooltip-content={title} data-tooltip-content={title}
{...otherProps} {...otherProps}
> >
{label} {label}
</Tab>); </Tab>
);
} }
ConceptTab.tabsRole = 'Tab'; ConceptTab.tabsRole = 'Tab';
export default ConceptTab; export default ConceptTab;

View File

@ -7,16 +7,16 @@ import { ITooltip, Tooltip } from 'react-tooltip';
import { useConceptTheme } from '@/context/ThemeContext'; import { useConceptTheme } from '@/context/ThemeContext';
interface ConceptTooltipProps interface ConceptTooltipProps extends Omit<ITooltip, 'variant'> {
extends Omit<ITooltip, 'variant'> { layer?: string;
layer?: string text?: string;
text?: string
} }
function ConceptTooltip({ function ConceptTooltip({
text, children, text,
layer='z-tooltip', children,
place='bottom', layer = 'z-tooltip',
place = 'bottom',
className, className,
style, style,
...restProps ...restProps
@ -26,25 +26,22 @@ function ConceptTooltip({
return null; return null;
} }
return createPortal( return createPortal(
(<Tooltip <Tooltip
delayShow={1000} delayShow={1000}
delayHide={100} delayHide={100}
opacity={0.97} opacity={0.97}
className={clsx( className={clsx('overflow-hidden', 'border shadow-md', layer, className)}
'overflow-hidden', classNameArrow={layer}
'border shadow-md', style={{ ...{ paddingTop: '2px', paddingBottom: '2px' }, ...style }}
layer, variant={darkMode ? 'dark' : 'light'}
className place={place}
)} {...restProps}
classNameArrow={layer} >
style={{...{ paddingTop: '2px', paddingBottom: '2px'}, ...style}} {text ? text : null}
variant={(darkMode ? 'dark' : 'light')} {children as ReactNode}
place={place} </Tooltip>,
{...restProps} document.body
> );
{text ? text : null}
{children as ReactNode}
</Tooltip>), document.body);
} }
export default ConceptTooltip; export default ConceptTooltip;

View File

@ -1,19 +1,19 @@
import clsx from 'clsx'; import clsx from 'clsx';
interface DividerProps { interface DividerProps {
vertical?: boolean vertical?: boolean;
margins?: string margins?: string;
} }
function Divider({ vertical, margins = 'mx-2' }: DividerProps) { function Divider({ vertical, margins = 'mx-2' }: DividerProps) {
return ( return (
<div className={clsx( <div
margins, className={clsx(margins, {
{ 'border-x': vertical,
'border-x': vertical, 'border-y': !vertical
'border-y': !vertical })}
} />
)}/>); );
} }
export default Divider; export default Divider;

View File

@ -5,43 +5,38 @@ import { animateDropdown } from '@/utils/animations';
import { CProps } from '../props'; import { CProps } from '../props';
interface DropdownProps interface DropdownProps extends CProps.Styling {
extends CProps.Styling { stretchLeft?: boolean;
stretchLeft?: boolean isOpen: boolean;
isOpen: boolean children: React.ReactNode;
children: React.ReactNode
} }
function Dropdown({ function Dropdown({ isOpen, stretchLeft, className, children, ...restProps }: DropdownProps) {
isOpen, stretchLeft,
className,
children,
...restProps
}: DropdownProps) {
return ( return (
<div className='relative'> <div className='relative'>
<motion.div <motion.div
className={clsx( className={clsx(
'z-modal-tooltip', 'z-modal-tooltip',
'absolute mt-3', 'absolute mt-3',
'flex flex-col', 'flex flex-col',
'border rounded-md shadow-lg', 'border rounded-md shadow-lg',
'text-sm', 'text-sm',
'clr-input', 'clr-input',
{ {
'right-0': stretchLeft, 'right-0': stretchLeft,
'left-0': !stretchLeft 'left-0': !stretchLeft
}, },
className className
)} )}
initial={false} initial={false}
animate={isOpen ? 'open' : 'closed'} animate={isOpen ? 'open' : 'closed'}
variants={animateDropdown} variants={animateDropdown}
{...restProps} {...restProps}
> >
{children} {children}
</motion.div> </motion.div>
</div>); </div>
);
} }
export default Dropdown; export default Dropdown;

View File

@ -6,44 +6,39 @@ import { globalIDs } from '@/utils/constants';
import { CProps } from '../props'; import { CProps } from '../props';
interface DropdownButtonProps interface DropdownButtonProps extends CProps.AnimatedButton {
extends CProps.AnimatedButton { text?: string;
text?: string icon?: React.ReactNode;
icon?: React.ReactNode
children?: React.ReactNode children?: React.ReactNode;
} }
function DropdownButton({ function DropdownButton({ text, icon, className, title, onClick, children, ...restProps }: DropdownButtonProps) {
text, icon,
className, title,
onClick,
children,
...restProps
}: DropdownButtonProps) {
return ( return (
<motion.button type='button' <motion.button
onClick={onClick} type='button'
className={clsx( onClick={onClick}
'px-3 py-1 inline-flex items-center gap-2', className={clsx(
'text-left text-sm overflow-ellipsis whitespace-nowrap', 'px-3 py-1 inline-flex items-center gap-2',
'disabled:clr-text-controls', 'text-left text-sm overflow-ellipsis whitespace-nowrap',
{ 'disabled:clr-text-controls',
'clr-hover': onClick, {
'cursor-pointer disabled:cursor-not-allowed': onClick, 'clr-hover': onClick,
'cursor-default': !onClick 'cursor-pointer disabled:cursor-not-allowed': onClick,
}, 'cursor-default': !onClick
className },
)} className
variants={animateDropdownItem} )}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined} variants={animateDropdownItem}
data-tooltip-content={title} data-tooltip-id={title ? globalIDs.tooltip : undefined}
{...restProps} data-tooltip-content={title}
> {...restProps}
{children ? children : null} >
{!children && icon ? icon : null} {children ? children : null}
{!children && text ? <span>{text}</span> : null} {!children && icon ? icon : null}
</motion.button>); {!children && text ? <span>{text}</span> : null}
</motion.button>
);
} }
export default DropdownButton; export default DropdownButton;

View File

@ -6,31 +6,28 @@ import { animateDropdownItem } from '@/utils/animations';
import Checkbox from './Checkbox'; import Checkbox from './Checkbox';
interface DropdownCheckboxProps { interface DropdownCheckboxProps {
value: boolean value: boolean;
label?: string label?: string;
title?: string title?: string;
disabled?: boolean disabled?: boolean;
setValue?: (newValue: boolean) => void setValue?: (newValue: boolean) => void;
} }
function DropdownCheckbox({ title, setValue, disabled, ...restProps }: DropdownCheckboxProps) { function DropdownCheckbox({ title, setValue, disabled, ...restProps }: DropdownCheckboxProps) {
return ( return (
<motion.div <motion.div
variants={animateDropdownItem} variants={animateDropdownItem}
title={title} title={title}
className={clsx( className={clsx(
'px-3 py-1', 'px-3 py-1',
'text-left overflow-ellipsis whitespace-nowrap', 'text-left overflow-ellipsis whitespace-nowrap',
'disabled:clr-text-controls', 'disabled:clr-text-controls',
!!setValue && !disabled && 'clr-hover' !!setValue && !disabled && 'clr-hover'
)} )}
> >
<Checkbox <Checkbox disabled={disabled} setValue={setValue} {...restProps} />
disabled={disabled} </motion.div>
setValue={setValue} );
{...restProps}
/>
</motion.div>);
} }
export default DropdownCheckbox; export default DropdownCheckbox;

View File

@ -1,35 +1,37 @@
interface EmbedYoutubeProps { interface EmbedYoutubeProps {
videoID: string videoID: string;
pxHeight: number pxHeight: number;
pxWidth?: number pxWidth?: number;
} }
function EmbedYoutube({ videoID, pxHeight, pxWidth }: EmbedYoutubeProps) { function EmbedYoutube({ videoID, pxHeight, pxWidth }: EmbedYoutubeProps) {
if (!pxWidth) { if (!pxWidth) {
pxWidth = pxHeight * 16 / 9; pxWidth = (pxHeight * 16) / 9;
} }
return ( return (
<div <div
className='relative' className='relative'
style={{
height: 0,
paddingBottom: `${pxHeight}px`,
paddingLeft: `${pxWidth}px`
}}
>
<iframe allowFullScreen
title='Встроенное видео Youtube'
allow='accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
className='absolute top-0 left-0 border'
style={{ style={{
minHeight: `${pxHeight}px`, height: 0,
minWidth: `${pxWidth}px` paddingBottom: `${pxHeight}px`,
paddingLeft: `${pxWidth}px`
}} }}
width={`${pxWidth}px`} >
height={`${pxHeight}px`} <iframe
src={`https://www.youtube.com/embed/${videoID}`} allowFullScreen
/> title='Встроенное видео Youtube'
</div>); allow='accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture'
className='absolute top-0 left-0 border'
style={{
minHeight: `${pxHeight}px`,
minWidth: `${pxWidth}px`
}}
width={`${pxWidth}px`}
height={`${pxHeight}px`}
src={`https://www.youtube.com/embed/${videoID}`}
/>
</div>
);
} }
export default EmbedYoutube; export default EmbedYoutube;

View File

@ -8,20 +8,14 @@ import { CProps } from '../props';
import Button from './Button'; import Button from './Button';
import Label from './Label'; import Label from './Label';
interface FileInputProps interface FileInputProps extends Omit<CProps.Input, 'accept' | 'type'> {
extends Omit<CProps.Input, 'accept' | 'type'> { label: string;
label: string
acceptType?: string;
acceptType?: string onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
} }
function FileInput({ function FileInput({ label, acceptType, title, className, style, onChange, ...restProps }: FileInputProps) {
label, acceptType, title,
className, style,
onChange,
...restProps
}: FileInputProps) {
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
const [fileName, setFileName] = useState(''); const [fileName, setFileName] = useState('');
@ -41,29 +35,19 @@ function FileInput({
}; };
return ( return (
<div <div className={clsx('py-2', 'flex flex-col gap-2 items-center', className)} style={style}>
className={clsx( <input
'py-2', type='file'
'flex flex-col gap-2 items-center', ref={inputRef}
className style={{ display: 'none' }}
)} accept={acceptType}
style={style} onChange={handleFileChange}
> {...restProps}
<input type='file' />
ref={inputRef} <Button text={label} icon={<BiUpload size='1.5rem' />} onClick={handleUploadClick} title={title} />
style={{ display: 'none' }} <Label text={fileName} />
accept={acceptType} </div>
onChange={handleFileChange} );
{...restProps}
/>
<Button
text={label}
icon={<BiUpload size='1.5rem' />}
onClick={handleUploadClick}
title={title}
/>
<Label text={fileName} />
</div>);
} }
export default FileInput; export default FileInput;

View File

@ -4,23 +4,14 @@ import { classnames } from '@/utils/constants';
import { CProps } from '../props'; import { CProps } from '../props';
export interface FlexColumnProps export interface FlexColumnProps extends CProps.Div {}
extends CProps.Div {}
function FlexColumn({ function FlexColumn({ className, children, ...restProps }: FlexColumnProps) {
className, children,
...restProps
}: FlexColumnProps) {
return ( return (
<div <div className={clsx(classnames.flex_col, className)} {...restProps}>
className={clsx( {children}
classnames.flex_col, </div>
className );
)}
{...restProps}
>
{children}
</div>);
} }
export default FlexColumn; export default FlexColumn;

View File

@ -3,10 +3,6 @@
import { GraphCanvas as GraphUI } from 'reagraph'; import { GraphCanvas as GraphUI } from 'reagraph';
export { export { type GraphEdge, type GraphNode, type GraphCanvasRef, type LayoutTypes, Sphere, useSelection } from 'reagraph';
type GraphEdge, type GraphNode, type GraphCanvasRef,
type LayoutTypes,
Sphere, useSelection
} from 'reagraph';
export default GraphUI; export default GraphUI;

View File

@ -2,9 +2,8 @@ import clsx from 'clsx';
import { CProps } from '../props'; import { CProps } from '../props';
interface LabelProps interface LabelProps extends CProps.Label {
extends CProps.Label { text?: string;
text?: string
} }
function Label({ text, className, ...restProps }: LabelProps) { function Label({ text, className, ...restProps }: LabelProps) {
@ -12,15 +11,10 @@ function Label({ text, className, ...restProps }: LabelProps) {
return null; return null;
} }
return ( return (
<label <label className={clsx('text-sm font-semibold whitespace-nowrap', className)} {...restProps}>
className={clsx( {text}
'text-sm font-semibold whitespace-nowrap', </label>
className );
)}
{...restProps}
>
{text}
</label>);
} }
export default Label; export default Label;

View File

@ -1,24 +1,19 @@
interface LabeledValueProps { interface LabeledValueProps {
id?: string id?: string;
label: string label: string;
text: string | number text: string | number;
title?: string title?: string;
} }
function LabeledValue({ id, label, text, title }: LabeledValueProps) { function LabeledValue({ id, label, text, title }: LabeledValueProps) {
return ( return (
<div className='flex justify-between gap-3'> <div className='flex justify-between gap-3'>
<label <label className='font-semibold' title={title} htmlFor={id}>
className='font-semibold' {label}
title={title} </label>
htmlFor={id} <span id={id}>{text}</span>
> </div>
{label} );
</label>
<span id={id}>
{text}
</span>
</div>);
} }
export default LabeledValue; export default LabeledValue;

View File

@ -4,37 +4,34 @@ import { globalIDs } from '@/utils/constants';
import { CProps } from '../props'; import { CProps } from '../props';
interface MiniButtonProps interface MiniButtonProps extends CProps.Button {
extends CProps.Button { icon: React.ReactNode;
icon: React.ReactNode noHover?: boolean;
noHover?: boolean
} }
function MiniButton({ function MiniButton({ icon, noHover, tabIndex, title, className, ...restProps }: MiniButtonProps) {
icon, noHover, tabIndex,
title, className,
...restProps
}: MiniButtonProps) {
return ( return (
<button type='button' <button
tabIndex={tabIndex ?? -1} type='button'
className={clsx( tabIndex={tabIndex ?? -1}
'px-1 py-1', className={clsx(
'rounded-full', 'px-1 py-1',
'clr-btn-clear', 'rounded-full',
'cursor-pointer disabled:cursor-not-allowed', 'clr-btn-clear',
{ 'cursor-pointer disabled:cursor-not-allowed',
'outline-none': noHover, {
'clr-hover': !noHover 'outline-none': noHover,
}, 'clr-hover': !noHover
className },
)} className
data-tooltip-id={title ? (globalIDs.tooltip) : undefined} )}
data-tooltip-content={title} data-tooltip-id={title ? globalIDs.tooltip : undefined}
{...restProps} data-tooltip-content={title}
> {...restProps}
{icon} >
</button>); {icon}
</button>
);
} }
export default MiniButton; export default MiniButton;

View File

@ -13,26 +13,30 @@ import Button from './Button';
import MiniButton from './MiniButton'; import MiniButton from './MiniButton';
import Overlay from './Overlay'; import Overlay from './Overlay';
export interface ModalProps export interface ModalProps extends CProps.Styling {
extends CProps.Styling { header?: string;
header?: string submitText?: string;
submitText?: string submitInvalidTooltip?: string;
submitInvalidTooltip?: string
readonly?: boolean
canSubmit?: boolean
hideWindow: () => void readonly?: boolean;
onSubmit?: () => void canSubmit?: boolean;
onCancel?: () => void
children: React.ReactNode hideWindow: () => void;
onSubmit?: () => void;
onCancel?: () => void;
children: React.ReactNode;
} }
function Modal({ function Modal({
header, hideWindow, onSubmit, header,
readonly, onCancel, canSubmit, hideWindow,
submitInvalidTooltip, className, onSubmit,
readonly,
onCancel,
canSubmit,
submitInvalidTooltip,
className,
children, children,
submitText = 'Продолжить', submitText = 'Продолжить',
...restProps ...restProps
@ -51,69 +55,58 @@ function Modal({
}; };
return ( return (
<> <>
<div className={clsx( <div className={clsx('z-navigation', 'fixed top-0 left-0', 'w-full h-full', 'clr-modal-backdrop')} />
'z-navigation', <motion.div
'fixed top-0 left-0', ref={ref}
'w-full h-full',
'clr-modal-backdrop'
)}/>
<motion.div ref={ref}
className={clsx(
'z-modal',
'fixed bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
'border shadow-md',
'clr-app'
)}
initial={{...animateModal.initial}}
animate={{...animateModal.animate}}
exit={{...animateModal.exit}}
{...restProps}
>
<Overlay position='right-[0.3rem] top-2'>
<MiniButton
title='Закрыть диалоговое окно [ESC]'
icon={<BiX size='1.25rem'/>}
onClick={handleCancel}
/>
</Overlay>
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
<div
className={clsx( className={clsx(
'overflow-auto', 'z-modal',
className 'fixed bottom-1/2 left-1/2 -translate-x-1/2 translate-y-1/2',
'border shadow-md',
'clr-app'
)} )}
style={{ initial={{ ...animateModal.initial }}
maxHeight: 'calc(100vh - 8rem)', animate={{ ...animateModal.animate }}
maxWidth: 'calc(100vw - 2rem)', exit={{ ...animateModal.exit }}
}} {...restProps}
> >
{children} <Overlay position='right-[0.3rem] top-2'>
</div> <MiniButton title='Закрыть диалоговое окно [ESC]' icon={<BiX size='1.25rem' />} onClick={handleCancel} />
</Overlay>
<div className={clsx( {header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
'z-modal-controls',
'px-6 py-3 flex gap-12 justify-center' <div
)}> className={clsx('overflow-auto', className)}
{!readonly ? style={{
<Button autoFocus maxHeight: 'calc(100vh - 8rem)',
text={submitText} maxWidth: 'calc(100vw - 2rem)'
title={!canSubmit ? submitInvalidTooltip: ''} }}
className='min-w-[8rem] min-h-[2.6rem]' >
colors='clr-btn-primary' {children}
disabled={!canSubmit} </div>
onClick={handleSubmit}
/> : null} <div className={clsx('z-modal-controls', 'px-6 py-3 flex gap-12 justify-center')}>
<Button {!readonly ? (
text={readonly ? 'Закрыть' : 'Отмена'} <Button
className='min-w-[8rem] min-h-[2.6rem]' autoFocus
onClick={handleCancel} text={submitText}
/> title={!canSubmit ? submitInvalidTooltip : ''}
</div> className='min-w-[8rem] min-h-[2.6rem]'
</motion.div> colors='clr-btn-primary'
</>); disabled={!canSubmit}
onClick={handleSubmit}
/>
) : null}
<Button
text={readonly ? 'Закрыть' : 'Отмена'}
className='min-w-[8rem] min-h-[2.6rem]'
onClick={handleCancel}
/>
</div>
</motion.div>
</>
);
} }
export default Modal; export default Modal;

View File

@ -1,34 +1,22 @@
import clsx from 'clsx' import clsx from 'clsx';
import { CProps } from '../props' import { CProps } from '../props';
interface OverlayProps extends CProps.Styling { interface OverlayProps extends CProps.Styling {
id?: string id?: string;
children: React.ReactNode children: React.ReactNode;
position?: string position?: string;
layer?: string layer?: string;
} }
function Overlay({ function Overlay({ children, className, position = 'top-0 right-0', layer = 'z-pop', ...restProps }: OverlayProps) {
children, className,
position='top-0 right-0',
layer='z-pop',
...restProps
}: OverlayProps) {
return ( return (
<div className='relative'> <div className='relative'>
<div <div className={clsx('absolute', className, position, layer)} {...restProps}>
className={clsx( {children}
'absolute', </div>
className, </div>
position, );
layer
)}
{...restProps}
>
{children}
</div>
</div>);
} }
export default Overlay; export default Overlay;

View File

@ -1,71 +0,0 @@
'use client';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import { useMemo, useState } from 'react';
import { Document, Page } from 'react-pdf';
import useWindowSize from '@/hooks/useWindowSize';
import { graphLightT } from '@/utils/color';
import Overlay from './Overlay';
import PageControls from './PageControls';
const MAXIMUM_WIDTH = 1000;
const MINIMUM_WIDTH = 600;
interface PDFViewerProps {
file?: string | ArrayBuffer | Blob
}
function PDFViewer({ file }: PDFViewerProps) {
const windowSize = useWindowSize();
const [pageCount, setPageCount] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
const pageWidth = useMemo(
() => {
return Math.max(MINIMUM_WIDTH, (Math.min((windowSize?.width ?? 0) - 300, MAXIMUM_WIDTH)));
}, [windowSize]);
function onDocumentLoadSuccess({ numPages }: PDFDocumentProxy) {
setPageCount(numPages);
}
return (
<Document
file={file}
onLoadSuccess={onDocumentLoadSuccess}
className='px-3'
loading='Загрузка PDF файла...'
error='Не удалось загрузить файл.'
>
<Overlay
position='top-6 left-1/2 -translate-x-1/2'
className='flex select-none'
>
<PageControls
pageCount={pageCount}
pageNumber={pageNumber}
setPageNumber={setPageNumber}
/>
</Overlay>
<Page
className='pointer-events-none select-none'
renderTextLayer={false}
renderAnnotationLayer={false}
pageNumber={pageNumber}
width={pageWidth}
canvasBackground={graphLightT.canvas.background}
/>
<Overlay position='bottom-6 left-1/2 -translate-x-1/2' className='flex select-none'>
<PageControls
pageCount={pageCount}
pageNumber={pageNumber}
setPageNumber={setPageNumber}
/>
</Overlay>
</Document>);
}
export default PDFViewer;

View File

@ -1,44 +0,0 @@
import { BiChevronLeft, BiChevronRight, BiFirstPage, BiLastPage } from 'react-icons/bi';
interface PageControlsProps {
pageNumber: number
pageCount: number
setPageNumber: React.Dispatch<React.SetStateAction<number>>
}
function PageControls({ pageNumber, pageCount, setPageNumber }: PageControlsProps) {
return (
<>
<button type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(1)}
disabled={pageNumber < 2}
>
<BiFirstPage size='1.5rem' />
</button>
<button type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(prev => prev - 1)}
disabled={pageNumber < 2}
>
<BiChevronLeft size='1.5rem' />
</button>
<p className='px-3 text-black'>Страница {pageNumber} из {pageCount}</p>
<button type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(prev => prev + 1)}
disabled={pageNumber >= pageCount}
>
<BiChevronRight size='1.5rem' />
</button>
<button type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(pageCount)}
disabled={pageNumber >= pageCount}
>
<BiLastPage size='1.5rem' />
</button>
</>);
}
export default PageControls;

View File

@ -1,12 +1,9 @@
interface PrettyJsonProps { interface PrettyJsonProps {
data: unknown data: unknown;
} }
function PrettyJson({ data }: PrettyJsonProps) { function PrettyJson({ data }: PrettyJsonProps) {
return ( return <pre>{JSON.stringify(data, null, 2)}</pre>;
<pre>
{JSON.stringify(data, null, 2)}
</pre>);
} }
export default PrettyJson; export default PrettyJson;

View File

@ -6,69 +6,69 @@ import Select, { GroupBase, Props, StylesConfig } from 'react-select';
import { useConceptTheme } from '@/context/ThemeContext'; import { useConceptTheme } from '@/context/ThemeContext';
import { selectDarkT, selectLightT } from '@/utils/color'; import { selectDarkT, selectLightT } from '@/utils/color';
export interface SelectMultiProps< export interface SelectMultiProps<Option, Group extends GroupBase<Option> = GroupBase<Option>>
Option, extends Omit<Props<Option, true, Group>, 'theme' | 'menuPortalTarget'> {
Group extends GroupBase<Option> = GroupBase<Option> noPortal?: boolean;
>
extends Omit<Props<Option, true, Group>, 'theme' | 'menuPortalTarget'> {
noPortal?: boolean
} }
function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>> ({ function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>>({
noPortal, ...restProps noPortal,
...restProps
}: SelectMultiProps<Option, Group>) { }: SelectMultiProps<Option, Group>) {
const { darkMode, colors } = useConceptTheme(); const { darkMode, colors } = useConceptTheme();
const themeColors = useMemo( const themeColors = useMemo(() => (!darkMode ? selectLightT : selectDarkT), [darkMode]);
() => !darkMode ? selectLightT : selectDarkT
, [darkMode]);
const adjustedStyles: StylesConfig<Option, true, Group> = useMemo( const adjustedStyles: StylesConfig<Option, true, Group> = useMemo(
() => ({ () => ({
control: (styles, { isDisabled }) => ({ control: (styles, { isDisabled }) => ({
...styles, ...styles,
borderRadius: '0.25rem', borderRadius: '0.25rem',
cursor: isDisabled ? 'not-allowed' : 'pointer', cursor: isDisabled ? 'not-allowed' : 'pointer',
boxShadow: 'none' boxShadow: 'none'
}),
option: (styles, { isSelected }) => ({
...styles,
backgroundColor: isSelected ? colors.bgSelected : styles.backgroundColor,
color: isSelected ? colors.fgSelected : styles.color,
borderWidth: '1px',
borderColor: colors.border
}),
menuPortal: styles => ({
...styles,
zIndex: 9999
}),
menuList: styles => ({
...styles,
padding: '0px'
}),
input: styles => ({ ...styles }),
placeholder: styles => ({ ...styles }),
multiValue: styles => ({
...styles,
borderRadius: '0.5rem',
backgroundColor: colors.bgSelected
})
}), }),
option: (styles, { isSelected }) => ({ [colors]
...styles, );
backgroundColor: isSelected ? colors.bgSelected : styles.backgroundColor,
color: isSelected ? colors.fgSelected : styles.color,
borderWidth: '1px',
borderColor: colors.border
}),
menuPortal: (styles) => ({
...styles,
zIndex: 9999
}),
menuList: (styles) => ({
...styles,
padding: '0px'
}),
input: (styles) => ({...styles}),
placeholder: (styles) => ({...styles}),
multiValue: (styles) => ({
...styles,
borderRadius: '0.5rem',
backgroundColor: colors.bgSelected,
})
}), [colors]);
return ( return (
<Select isMulti <Select
noOptionsMessage={() => 'Список пуст'} isMulti
theme={theme => ({ noOptionsMessage={() => 'Список пуст'}
...theme, theme={theme => ({
borderRadius: 0, ...theme,
colors: { borderRadius: 0,
...theme.colors, colors: {
...themeColors ...theme.colors,
}, ...themeColors
})} }
menuPortalTarget={!noPortal ? document.body : null} })}
styles={adjustedStyles} menuPortalTarget={!noPortal ? document.body : null}
{...restProps} styles={adjustedStyles}
/>); {...restProps}
/>
);
} }
export default SelectMulti; export default SelectMulti;

View File

@ -6,65 +6,64 @@ import Select, { GroupBase, Props, StylesConfig } from 'react-select';
import { useConceptTheme } from '@/context/ThemeContext'; import { useConceptTheme } from '@/context/ThemeContext';
import { selectDarkT, selectLightT } from '@/utils/color'; import { selectDarkT, selectLightT } from '@/utils/color';
interface SelectSingleProps< interface SelectSingleProps<Option, Group extends GroupBase<Option> = GroupBase<Option>>
Option, extends Omit<Props<Option, false, Group>, 'theme' | 'menuPortalTarget'> {
Group extends GroupBase<Option> = GroupBase<Option> noPortal?: boolean;
>
extends Omit<Props<Option, false, Group>, 'theme' | 'menuPortalTarget'> {
noPortal?: boolean
} }
function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option>> ({ function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option>>({
noPortal, ...restProps noPortal,
...restProps
}: SelectSingleProps<Option, Group>) { }: SelectSingleProps<Option, Group>) {
const { darkMode, colors } = useConceptTheme(); const { darkMode, colors } = useConceptTheme();
const themeColors = useMemo( const themeColors = useMemo(() => (!darkMode ? selectLightT : selectDarkT), [darkMode]);
() => !darkMode ? selectLightT : selectDarkT
, [darkMode]);
const adjustedStyles: StylesConfig<Option, false, Group> = useMemo( const adjustedStyles: StylesConfig<Option, false, Group> = useMemo(
() => ({ () => ({
control: (styles, { isDisabled }) => ({ control: (styles, { isDisabled }) => ({
...styles, ...styles,
borderRadius: '0.25rem', borderRadius: '0.25rem',
cursor: isDisabled ? 'not-allowed' : 'pointer', cursor: isDisabled ? 'not-allowed' : 'pointer',
boxShadow: 'none' boxShadow: 'none'
}),
menuPortal: styles => ({
...styles,
zIndex: 9999
}),
menuList: styles => ({
...styles,
padding: '0px'
}),
option: (styles, { isSelected }) => ({
...styles,
backgroundColor: isSelected ? colors.bgSelected : styles.backgroundColor,
color: isSelected ? colors.fgSelected : styles.color,
borderWidth: '1px',
borderColor: colors.border
}),
input: styles => ({ ...styles }),
placeholder: styles => ({ ...styles }),
singleValue: styles => ({ ...styles })
}), }),
menuPortal: (styles) => ({ [colors]
...styles, );
zIndex: 9999
}),
menuList: (styles) => ({
...styles,
padding: '0px'
}),
option: (styles, { isSelected }) => ({
...styles,
backgroundColor: isSelected ? colors.bgSelected : styles.backgroundColor,
color: isSelected ? colors.fgSelected : styles.color,
borderWidth: '1px',
borderColor: colors.border
}),
input: (styles) => ({...styles}),
placeholder: (styles) => ({...styles}),
singleValue: (styles) => ({...styles}),
}), [colors]);
return ( return (
<Select <Select
noOptionsMessage={() => 'Список пуст'} noOptionsMessage={() => 'Список пуст'}
theme={theme => ({ theme={theme => ({
...theme, ...theme,
borderRadius: 0, borderRadius: 0,
colors: { colors: {
...theme.colors, ...theme.colors,
...themeColors ...themeColors
}, }
})} })}
menuPortalTarget={!noPortal ? document.body : null} menuPortalTarget={!noPortal ? document.body : null}
styles={adjustedStyles} styles={adjustedStyles}
{...restProps} {...restProps}
/>); />
);
} }
export default SelectSingle; export default SelectSingle;

View File

@ -4,43 +4,46 @@ import { globalIDs } from '@/utils/constants';
import { CProps } from '../props'; import { CProps } from '../props';
interface SelectorButtonProps interface SelectorButtonProps extends CProps.Button {
extends CProps.Button { text?: string;
text?: string icon?: React.ReactNode;
icon?: React.ReactNode
colors?: string colors?: string;
transparent?: boolean transparent?: boolean;
} }
function SelectorButton({ function SelectorButton({
text, icon, title, text,
icon,
title,
colors = 'clr-btn-default', colors = 'clr-btn-default',
className, className,
transparent, transparent,
...restProps ...restProps
}: SelectorButtonProps) { }: SelectorButtonProps) {
return ( return (
<button type='button' <button
data-tooltip-id={title ? (globalIDs.tooltip) : undefined} type='button'
data-tooltip-content={title} data-tooltip-id={title ? globalIDs.tooltip : undefined}
className={clsx( data-tooltip-content={title}
'px-1 flex flex-start items-center gap-1', className={clsx(
'text-sm small-caps select-none', 'px-1 flex flex-start items-center gap-1',
'text-btn clr-text-controls', 'text-sm small-caps select-none',
'disabled:cursor-not-allowed cursor-pointer', 'text-btn clr-text-controls',
{ 'disabled:cursor-not-allowed cursor-pointer',
'clr-hover': transparent, {
'border': !transparent, 'clr-hover': transparent,
}, 'border': !transparent
className, },
!transparent && colors className,
)} !transparent && colors
{...restProps} )}
> {...restProps}
{icon ? icon : null} >
{text ? <div className={'font-semibold whitespace-nowrap pb-1'}>{text}</div> : null} {icon ? icon : null}
</button>); {text ? <div className={'font-semibold whitespace-nowrap pb-1'}>{text}</div> : null}
</button>
);
} }
export default SelectorButton; export default SelectorButton;

View File

@ -2,36 +2,32 @@ import clsx from 'clsx';
import { CProps } from '../props'; import { CProps } from '../props';
interface SubmitButtonProps interface SubmitButtonProps extends CProps.Button {
extends CProps.Button { text?: string;
text?: string loading?: boolean;
loading?: boolean icon?: React.ReactNode;
icon?: React.ReactNode
} }
function SubmitButton({ function SubmitButton({ text = 'ОК', icon, disabled, loading, className, ...restProps }: SubmitButtonProps) {
text = 'ОК',
icon, disabled, loading,
className,
...restProps
}: SubmitButtonProps) {
return ( return (
<button type='submit' <button
className={clsx( type='submit'
'px-3 py-2 flex gap-2 items-center justify-center', className={clsx(
'border', 'px-3 py-2 flex gap-2 items-center justify-center',
'font-semibold', 'border',
'clr-btn-primary', 'font-semibold',
'select-none disabled:cursor-not-allowed', 'clr-btn-primary',
loading && 'cursor-progress', 'select-none disabled:cursor-not-allowed',
className loading && 'cursor-progress',
)} className
disabled={disabled ?? loading} )}
{...restProps} disabled={disabled ?? loading}
> {...restProps}
{icon ? <span>{icon}</span> : null} >
{text ? <span>{text}</span> : null} {icon ? <span>{icon}</span> : null}
</button>); {text ? <span>{text}</span> : null}
</button>
);
} }
export default SubmitButton; export default SubmitButton;

View File

@ -2,39 +2,46 @@ import clsx from 'clsx';
import { CProps } from '../props'; import { CProps } from '../props';
interface SwitchButtonProps<ValueType> interface SwitchButtonProps<ValueType> extends CProps.Styling {
extends CProps.Styling { id?: string;
id?: string value: ValueType;
value: ValueType label?: string;
label?: string icon?: React.ReactNode;
icon?: React.ReactNode title?: string;
title?: string
isSelected?: boolean isSelected?: boolean;
onSelect: (value: ValueType) => void onSelect: (value: ValueType) => void;
} }
function SwitchButton<ValueType>({ function SwitchButton<ValueType>({
value, icon, label, className, value,
isSelected, onSelect, ...restProps icon,
label,
className,
isSelected,
onSelect,
...restProps
}: SwitchButtonProps<ValueType>) { }: SwitchButtonProps<ValueType>) {
return ( return (
<button type='button' tabIndex={-1} <button
onClick={() => onSelect(value)} type='button'
className={clsx( tabIndex={-1}
'px-2 py-1', onClick={() => onSelect(value)}
'border rounded-none', className={clsx(
'font-semibold small-caps', 'px-2 py-1',
'clr-btn-clear clr-hover', 'border rounded-none',
'cursor-pointer', 'font-semibold small-caps',
isSelected && 'clr-selected', 'clr-btn-clear clr-hover',
className 'cursor-pointer',
)} isSelected && 'clr-selected',
{...restProps} className
> )}
{icon ? icon : null} {...restProps}
{label} >
</button>); {icon ? icon : null}
{label}
</button>
);
} }
export default SwitchButton; export default SwitchButton;

View File

@ -3,44 +3,52 @@ import clsx from 'clsx';
import { CProps } from '../props'; import { CProps } from '../props';
import Label from './Label'; import Label from './Label';
export interface TextAreaProps export interface TextAreaProps extends CProps.Editor, CProps.Colors, CProps.TextArea {
extends CProps.Editor, CProps.Colors, CProps.TextArea { dense?: boolean;
dense?: boolean
} }
function TextArea({ function TextArea({
id, label, required, rows, id,
dense, noBorder, noOutline, label,
required,
rows,
dense,
noBorder,
noOutline,
className, className,
colors = 'clr-input', colors = 'clr-input',
...restProps ...restProps
}: TextAreaProps) { }: TextAreaProps) {
return ( return (
<div className={clsx( <div
{
'flex flex-col gap-2': !dense,
'flex items-center gap-3': dense
},
dense && className,
)}>
<Label text={label} htmlFor={id} />
<textarea id={id}
className={clsx( className={clsx(
'px-3 py-2',
'leading-tight',
{ {
'border': !noBorder, 'flex flex-col gap-2': !dense,
'flex-grow': dense, 'flex items-center gap-3': dense
'clr-outline': !noOutline
}, },
colors, dense && className
!dense && className
)} )}
rows={rows} >
required={required} <Label text={label} htmlFor={id} />
{...restProps} <textarea
/> id={id}
</div>); className={clsx(
'px-3 py-2',
'leading-tight',
{
'border': !noBorder,
'flex-grow': dense,
'clr-outline': !noOutline
},
colors,
!dense && className
)}
rows={rows}
required={required}
{...restProps}
/>
</div>
);
} }
export default TextArea; export default TextArea;

View File

@ -3,10 +3,9 @@ import clsx from 'clsx';
import { CProps } from '../props'; import { CProps } from '../props';
import Label from './Label'; import Label from './Label';
interface TextInputProps interface TextInputProps extends CProps.Editor, CProps.Colors, CProps.Input {
extends CProps.Editor, CProps.Colors, CProps.Input { dense?: boolean;
dense?: boolean allowEnter?: boolean;
allowEnter?: boolean
} }
function preventEnterCapture(event: React.KeyboardEvent<HTMLInputElement>) { function preventEnterCapture(event: React.KeyboardEvent<HTMLInputElement>) {
@ -16,40 +15,49 @@ function preventEnterCapture(event: React.KeyboardEvent<HTMLInputElement>) {
} }
function TextInput({ function TextInput({
id, label, id,
dense, noBorder, noOutline, allowEnter, disabled, label,
dense,
noBorder,
noOutline,
allowEnter,
disabled,
className, className,
colors = 'clr-input', colors = 'clr-input',
onKeyDown, onKeyDown,
...restProps ...restProps
}: TextInputProps) { }: TextInputProps) {
return ( return (
<div className={clsx( <div
{
'flex flex-col gap-2': !dense,
'flex items-center gap-3': dense,
},
dense && className
)}>
<Label text={label} htmlFor={id} />
<input id={id}
className={clsx( className={clsx(
'py-2',
'leading-tight truncate hover:text-clip',
{ {
'px-3': !noBorder || !disabled, 'flex flex-col gap-2': !dense,
'flex-grow': dense, 'flex items-center gap-3': dense
'border': !noBorder,
'clr-outline': !noOutline
}, },
colors, dense && className
!dense && className
)} )}
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown} >
disabled={disabled} <Label text={label} htmlFor={id} />
{...restProps} <input
/> id={id}
</div>); className={clsx(
'py-2',
'leading-tight truncate hover:text-clip',
{
'px-3': !noBorder || !disabled,
'flex-grow': dense,
'border': !noBorder,
'clr-outline': !noOutline
},
colors,
!dense && className
)}
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown}
disabled={disabled}
{...restProps}
/>
</div>
);
} }
export default TextInput; export default TextInput;

View File

@ -1,37 +1,30 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
interface TextURLProps { interface TextURLProps {
text: string text: string;
title?: string title?: string;
href?: string href?: string;
color?: string color?: string;
onClick?: () => void onClick?: () => void;
} }
function TextURL({ text, href, title, color='clr-text-url', onClick }: TextURLProps) { function TextURL({ text, href, title, color = 'clr-text-url', onClick }: TextURLProps) {
const design = `cursor-pointer hover:underline ${color}`; const design = `cursor-pointer hover:underline ${color}`;
if (href) { if (href) {
return ( return (
<Link tabIndex={-1} <Link tabIndex={-1} className={design} title={title} to={href}>
className={design} {text}
title={title} </Link>
to={href}
>
{text}
</Link>
); );
} else if (onClick) { } else if (onClick) {
return ( return (
<span tabIndex={-1} <span tabIndex={-1} className={design} onClick={onClick}>
className={design} {text}
onClick={onClick} </span>
> );
{text}
</span>);
} else { } else {
return null; return null;
} }
} }
export default TextURL; export default TextURL;

View File

@ -1,4 +1,3 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react'; import { useMemo } from 'react';
@ -8,26 +7,19 @@ import { CheckboxCheckedIcon, CheckboxNullIcon } from '../Icons';
import { CheckboxProps } from './Checkbox'; import { CheckboxProps } from './Checkbox';
import Label from './Label'; import Label from './Label';
export interface TristateProps export interface TristateProps extends Omit<CheckboxProps, 'value' | 'setValue'> {
extends Omit<CheckboxProps, 'value' | 'setValue'> { value: boolean | null;
value: boolean | null setValue?: (newValue: boolean | null) => void;
setValue?: (newValue: boolean | null) => void
} }
function Tristate({ function Tristate({ id, disabled, label, title, className, value, setValue, ...restProps }: TristateProps) {
id, disabled, label, title, const cursor = useMemo(() => {
className,
value, setValue,
...restProps
}: TristateProps) {
const cursor = useMemo(
() => {
if (disabled) { if (disabled) {
return 'cursor-not-allowed'; return 'cursor-not-allowed';
} else if (setValue) { } else if (setValue) {
return 'cursor-pointer'; return 'cursor-pointer';
} else { } else {
return '' return '';
} }
}, [disabled, setValue]); }, [disabled, setValue]);
@ -37,7 +29,7 @@ function Tristate({
return; return;
} }
if (value === false) { if (value === false) {
setValue(null); setValue(null);
} else if (value === null) { } else if (value === null) {
setValue(true); setValue(true);
} else { } else {
@ -46,32 +38,36 @@ function Tristate({
} }
return ( return (
<button type='button' id={id} <button
className={clsx( type='button'
'flex items-center gap-2 text-start', id={id}
'outline-none', className={clsx('flex items-center gap-2 text-start', 'outline-none', cursor, className)}
cursor, disabled={disabled}
className onClick={handleClick}
)} data-tooltip-id={title ? globalIDs.tooltip : undefined}
disabled={disabled} data-tooltip-content={title}
onClick={handleClick} {...restProps}
data-tooltip-id={title ? (globalIDs.tooltip) : undefined} >
data-tooltip-content={title} <div
{...restProps} className={clsx('w-4 h-4', 'border rounded-sm', {
> 'clr-primary': value !== false,
<div className={clsx( 'clr-app': value === false
'w-4 h-4', })}
'border rounded-sm', >
{ {value ? (
'clr-primary': value !== false, <div className='mt-[1px] ml-[1px]'>
'clr-app': value === false <CheckboxCheckedIcon />
} </div>
)}> ) : null}
{value ? <div className='mt-[1px] ml-[1px]'><CheckboxCheckedIcon /></div> : null} {value == null ? (
{value == null ? <div className='mt-[1px] ml-[1px]'><CheckboxNullIcon /></div> : null} <div className='mt-[1px] ml-[1px]'>
</div> <CheckboxNullIcon />
<Label className={cursor} text={label} htmlFor={id} /> </div>
</button>); ) : null}
</div>
<Label className={cursor} text={label} htmlFor={id} />
</button>
);
} }
export default Tristate; export default Tristate;

View File

@ -2,16 +2,12 @@ import { ToastContainer, type ToastContainerProps } from 'react-toastify';
import { useConceptTheme } from '@/context/ThemeContext'; import { useConceptTheme } from '@/context/ThemeContext';
interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'>{} interface ToasterThemedProps extends Omit<ToastContainerProps, 'theme'> {}
function ToasterThemed(props: ToasterThemedProps) { function ToasterThemed(props: ToasterThemedProps) {
const { darkMode } = useConceptTheme(); const { darkMode } = useConceptTheme();
return ( return <ToastContainer theme={darkMode ? 'dark' : 'light'} {...props} />;
<ToastContainer
theme={ darkMode ? 'dark' : 'light'}
{...props}
/>);
} }
export default ToasterThemed; export default ToasterThemed;

View File

@ -2,10 +2,17 @@
import { import {
ColumnSort, ColumnSort,
createColumnHelper, getCoreRowModel, createColumnHelper,
getPaginationRowModel, getSortedRowModel, getCoreRowModel,
PaginationState, RowData, type RowSelectionState, getPaginationRowModel,
SortingState, TableOptions, useReactTable, type VisibilityState getSortedRowModel,
PaginationState,
RowData,
type RowSelectionState,
SortingState,
TableOptions,
useReactTable,
type VisibilityState
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import clsx from 'clsx'; import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
@ -20,51 +27,56 @@ import TableHeader from './TableHeader';
export { createColumnHelper, type ColumnSort, type RowSelectionState, type VisibilityState }; export { createColumnHelper, type ColumnSort, type RowSelectionState, type VisibilityState };
export interface IConditionalStyle<TData> { export interface IConditionalStyle<TData> {
when: (rowData: TData) => boolean when: (rowData: TData) => boolean;
style: React.CSSProperties style: React.CSSProperties;
} }
export interface DataTableProps<TData extends RowData> export interface DataTableProps<TData extends RowData>
extends CProps.Styling, Pick<TableOptions<TData>, extends CProps.Styling,
'data' | 'columns' | Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
'onRowSelectionChange' | 'onColumnVisibilityChange' dense?: boolean;
> { headPosition?: string;
dense?: boolean noHeader?: boolean;
headPosition?: string noFooter?: boolean;
noHeader?: boolean
noFooter?: boolean
conditionalRowStyles?: IConditionalStyle<TData>[] conditionalRowStyles?: IConditionalStyle<TData>[];
noDataComponent?: React.ReactNode noDataComponent?: React.ReactNode;
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void;
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void;
enableRowSelection?: boolean enableRowSelection?: boolean;
rowSelection?: RowSelectionState rowSelection?: RowSelectionState;
enableHiding?: boolean enableHiding?: boolean;
columnVisibility?: VisibilityState columnVisibility?: VisibilityState;
enablePagination?: boolean enablePagination?: boolean;
paginationPerPage?: number paginationPerPage?: number;
paginationOptions?: number[] paginationOptions?: number[];
onChangePaginationOption?: (newValue: number) => void onChangePaginationOption?: (newValue: number) => void;
enableSorting?: boolean enableSorting?: boolean;
initialSorting?: ColumnSort initialSorting?: ColumnSort;
} }
/** /**
* UI element: data representation as a table. * UI element: data representation as a table.
* *
* @param headPosition - Top position of sticky header (0 if no other sticky elements are present). * @param headPosition - Top position of sticky header (0 if no other sticky elements are present).
* No sticky header if omitted * No sticky header if omitted
*/ */
function DataTable<TData extends RowData>({ function DataTable<TData extends RowData>({
style, className, style,
dense, headPosition, conditionalRowStyles, noFooter, noHeader, className,
onRowClicked, onRowDoubleClicked, noDataComponent, dense,
headPosition,
conditionalRowStyles,
noFooter,
noHeader,
onRowClicked,
onRowDoubleClicked,
noDataComponent,
enableRowSelection, enableRowSelection,
rowSelection, rowSelection,
@ -76,8 +88,8 @@ function DataTable<TData extends RowData>({
initialSorting, initialSorting,
enablePagination, enablePagination,
paginationPerPage=10, paginationPerPage = 10,
paginationOptions=[10, 20, 30, 40, 50], paginationOptions = [10, 20, 30, 40, 50],
onChangePaginationOption, onChangePaginationOption,
...restProps ...restProps
@ -86,14 +98,14 @@ function DataTable<TData extends RowData>({
const [pagination, setPagination] = useState<PaginationState>({ const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
pageSize: paginationPerPage, pageSize: paginationPerPage
}); });
const tableImpl = useReactTable({ const tableImpl = useReactTable({
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getSortedRowModel: enableSorting ? getSortedRowModel() : undefined, getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,
getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined, getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
state: { state: {
pagination: pagination, pagination: pagination,
sorting: sorting, sorting: sorting,
@ -110,39 +122,39 @@ function DataTable<TData extends RowData>({
const isEmpty = tableImpl.getRowModel().rows.length === 0; const isEmpty = tableImpl.getRowModel().rows.length === 0;
return ( return (
<div className={clsx(className)} style={style}> <div className={clsx(className)} style={style}>
<table className='w-full'> <table className='w-full'>
{!noHeader ? {!noHeader ? (
<TableHeader <TableHeader
table={tableImpl} table={tableImpl}
enableRowSelection={enableRowSelection} enableRowSelection={enableRowSelection}
enableSorting={enableSorting} enableSorting={enableSorting}
headPosition={headPosition} headPosition={headPosition}
/>: null} />
) : null}
<TableBody
table={tableImpl} <TableBody
dense={dense} table={tableImpl}
conditionalRowStyles={conditionalRowStyles} dense={dense}
enableRowSelection={enableRowSelection} conditionalRowStyles={conditionalRowStyles}
onRowClicked={onRowClicked} enableRowSelection={enableRowSelection}
onRowDoubleClicked={onRowDoubleClicked} onRowClicked={onRowClicked}
/> onRowDoubleClicked={onRowDoubleClicked}
/>
{!noFooter ?
<TableFooter {!noFooter ? <TableFooter table={tableImpl} /> : null}
table={tableImpl} </table>
/>: null}
</table> {enablePagination && !isEmpty ? (
<PaginationTools
{(enablePagination && !isEmpty) ? table={tableImpl}
<PaginationTools paginationOptions={paginationOptions}
table={tableImpl} onChangePaginationOption={onChangePaginationOption}
paginationOptions={paginationOptions} />
onChangePaginationOption={onChangePaginationOption} ) : null}
/> : null} {isEmpty ? noDataComponent ?? <DefaultNoData /> : null}
{isEmpty ? (noDataComponent ?? <DefaultNoData />) : null} </div>
</div>); );
} }
export default DataTable; export default DataTable;

View File

@ -1,8 +1,5 @@
function defaultNoDataComponent() { function defaultNoDataComponent() {
return ( return <div className='p-2 text-center'>Данные отсутствуют</div>;
<div className='p-2 text-center'>
Данные отсутствуют
</div>);
} }
export default defaultNoDataComponent; export default defaultNoDataComponent;

View File

@ -8,90 +8,93 @@ import { BiChevronLeft, BiChevronRight, BiFirstPage, BiLastPage } from 'react-ic
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
interface PaginationToolsProps<TData> { interface PaginationToolsProps<TData> {
table: Table<TData> table: Table<TData>;
paginationOptions: number[] paginationOptions: number[];
onChangePaginationOption?: (newValue: number) => void onChangePaginationOption?: (newValue: number) => void;
} }
function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOption }: PaginationToolsProps<TData>) { function PaginationTools<TData>({ table, paginationOptions, onChangePaginationOption }: PaginationToolsProps<TData>) {
const handlePaginationOptionsChange = useCallback( const handlePaginationOptionsChange = useCallback(
(event: React.ChangeEvent<HTMLSelectElement>) => { (event: React.ChangeEvent<HTMLSelectElement>) => {
const perPage = Number(event.target.value); const perPage = Number(event.target.value);
table.setPageSize(perPage); table.setPageSize(perPage);
if (onChangePaginationOption) { if (onChangePaginationOption) {
onChangePaginationOption(perPage); onChangePaginationOption(perPage);
} }
}, [onChangePaginationOption, table]); },
[onChangePaginationOption, table]
);
return ( return (
<div className={clsx( <div className={clsx('flex justify-end items-center', 'my-2', 'text-sm', 'clr-text-controls', 'select-none')}>
'flex justify-end items-center', <span className='mr-3'>
'my-2', {`${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
'text-sm',
'clr-text-controls',
'select-none'
)}>
<span className='mr-3'>
{`${table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}
- -
${Math.min(table.getFilteredRowModel().rows.length, (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize)} ${Math.min(
table.getFilteredRowModel().rows.length,
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize
)}
из из
${table.getFilteredRowModel().rows.length}`} ${table.getFilteredRowModel().rows.length}`}
</span> </span>
<div className='flex'> <div className='flex'>
<button type='button' <button
className='clr-hover clr-text-controls' type='button'
onClick={() => table.setPageIndex(0)} className='clr-hover clr-text-controls'
disabled={!table.getCanPreviousPage()} onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<BiFirstPage size='1.5rem' />
</button>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<BiChevronLeft size='1.5rem' />
</button>
<input
title='Номер страницы. Выделите для ручного ввода'
className='w-6 text-center clr-app'
value={table.getState().pagination.pageIndex + 1}
onChange={event => {
const page = event.target.value ? Number(event.target.value) - 1 : 0;
if (page + 1 <= table.getPageCount()) {
table.setPageIndex(page);
}
}}
/>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<BiChevronRight size='1.5rem' />
</button>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<BiLastPage size='1.5rem' />
</button>
</div>
<select
value={table.getState().pagination.pageSize}
onChange={handlePaginationOptionsChange}
className='mx-2 cursor-pointer clr-app'
> >
<BiFirstPage size='1.5rem' /> {paginationOptions.map(pageSize => (
</button> <option key={`${prefixes.page_size}${pageSize}`} value={pageSize}>
<button type='button' {pageSize} на стр
className='clr-hover clr-text-controls' </option>
onClick={() => table.previousPage()} ))}
disabled={!table.getCanPreviousPage()} </select>
>
<BiChevronLeft size='1.5rem' />
</button>
<input
title='Номер страницы. Выделите для ручного ввода'
className='w-6 text-center clr-app'
value={table.getState().pagination.pageIndex + 1}
onChange={event => {
const page = event.target.value ? Number(event.target.value) - 1 : 0;
if (page + 1 <= table.getPageCount()) {
table.setPageIndex(page);
}
}}
/>
<button type='button'
className='clr-hover clr-text-controls'
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<BiChevronRight size='1.5rem' />
</button>
<button type='button'
className='clr-hover clr-text-controls'
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<BiLastPage size='1.5rem' />
</button>
</div> </div>
<select );
value={table.getState().pagination.pageSize}
onChange={handlePaginationOptionsChange}
className='mx-2 cursor-pointer clr-app'
>
{paginationOptions.map(
(pageSize) => (
<option key={`${prefixes.page_size}${pageSize}`} value={pageSize}>
{pageSize} на стр
</option>
))}
</select>
</div>);
} }
export default PaginationTools; export default PaginationTools;

View File

@ -3,20 +3,20 @@ import { Table } from '@tanstack/react-table';
import Tristate from '@/components/Common/Tristate'; import Tristate from '@/components/Common/Tristate';
interface SelectAllProps<TData> { interface SelectAllProps<TData> {
table: Table<TData> table: Table<TData>;
} }
function SelectAll<TData>({ table }: SelectAllProps<TData>) { function SelectAll<TData>({ table }: SelectAllProps<TData>) {
return ( return (
<Tristate tabIndex={-1} <Tristate
title='Выделить все' tabIndex={-1}
value={ title='Выделить все'
(!table.getIsAllPageRowsSelected() && table.getIsSomePageRowsSelected()) value={
? null !table.getIsAllPageRowsSelected() && table.getIsSomePageRowsSelected() ? null : table.getIsAllPageRowsSelected()
: table.getIsAllPageRowsSelected() }
} setValue={value => table.toggleAllPageRowsSelected(value !== false)}
setValue={value => table.toggleAllPageRowsSelected(value !== false)} />
/>); );
} }
export default SelectAll; export default SelectAll;

View File

@ -3,15 +3,11 @@ import { Row } from '@tanstack/react-table';
import Checkbox from '@/components/Common/Checkbox'; import Checkbox from '@/components/Common/Checkbox';
interface SelectRowProps<TData> { interface SelectRowProps<TData> {
row: Row<TData> row: Row<TData>;
} }
function SelectRow<TData>({ row }: SelectRowProps<TData>) { function SelectRow<TData>({ row }: SelectRowProps<TData>) {
return ( return <Checkbox tabIndex={-1} value={row.getIsSelected()} setValue={row.getToggleSelectedHandler()} />;
<Checkbox tabIndex={-1}
value={row.getIsSelected()}
setValue={row.getToggleSelectedHandler()}
/>);
} }
export default SelectRow; export default SelectRow;

View File

@ -2,18 +2,18 @@ import { Column } from '@tanstack/react-table';
import { BiCaretDown, BiCaretUp } from 'react-icons/bi'; import { BiCaretDown, BiCaretUp } from 'react-icons/bi';
interface SortingIconProps<TData> { interface SortingIconProps<TData> {
column: Column<TData> column: Column<TData>;
} }
function SortingIcon<TData>({ column }: SortingIconProps<TData>) { function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
return (<> return (
{{ <>
desc: <BiCaretDown size='1rem' />, {{
asc: <BiCaretUp size='1rem'/>, desc: <BiCaretDown size='1rem' />,
}[column.getIsSorted() as string] ?? asc: <BiCaretUp size='1rem' />
<BiCaretDown size='1rem' className='opacity-0 hover:opacity-50' /> }[column.getIsSorted() as string] ?? <BiCaretDown size='1rem' className='opacity-0 hover:opacity-50' />}
} </>
</>); );
} }
export default SortingIcon; export default SortingIcon;

View File

@ -4,19 +4,21 @@ import { IConditionalStyle } from '.';
import SelectRow from './SelectRow'; import SelectRow from './SelectRow';
interface TableBodyProps<TData> { interface TableBodyProps<TData> {
table: Table<TData> table: Table<TData>;
dense?: boolean dense?: boolean;
enableRowSelection?: boolean enableRowSelection?: boolean;
conditionalRowStyles?: IConditionalStyle<TData>[] conditionalRowStyles?: IConditionalStyle<TData>[];
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void;
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void;
} }
function TableBody<TData>({ function TableBody<TData>({
table, dense, table,
dense,
enableRowSelection, enableRowSelection,
conditionalRowStyles, conditionalRowStyles,
onRowClicked, onRowDoubleClicked onRowClicked,
onRowDoubleClicked
}: TableBodyProps<TData>) { }: TableBodyProps<TData>) {
function handleRowClicked(row: Row<TData>, event: React.MouseEvent<Element, MouseEvent>) { function handleRowClicked(row: Row<TData>, event: React.MouseEvent<Element, MouseEvent>) {
if (onRowClicked) { if (onRowClicked) {
@ -28,47 +30,51 @@ function TableBody<TData>({
} }
function getRowStyles(row: Row<TData>) { function getRowStyles(row: Row<TData>) {
return ({...conditionalRowStyles! return {
.filter(item => item.when(row.original)) ...conditionalRowStyles!
.reduce((prev, item) => ({...prev, ...item.style}), {}) .filter(item => item.when(row.original))
}); .reduce((prev, item) => ({ ...prev, ...item.style }), {})
};
} }
return ( return (
<tbody> <tbody>
{table.getRowModel().rows.map( {table.getRowModel().rows.map((row: Row<TData>, index) => (
(row: Row<TData>, index) => ( <tr
<tr key={row.id}
key={row.id} className={
className={ row.getIsSelected()
row.getIsSelected() ? 'clr-selected clr-hover' : ? 'clr-selected clr-hover'
index % 2 === 0 ? 'clr-controls clr-hover' : 'clr-app clr-hover' : index % 2 === 0
} ? 'clr-controls clr-hover'
style={conditionalRowStyles && getRowStyles(row)} : 'clr-app clr-hover'
> }
{enableRowSelection ? style={conditionalRowStyles && getRowStyles(row)}
<td key={`select-${row.id}`} className='pl-3 pr-1 border-y'> >
<SelectRow row={row} /> {enableRowSelection ? (
</td> : null} <td key={`select-${row.id}`} className='pl-3 pr-1 border-y'>
{row.getVisibleCells().map( <SelectRow row={row} />
(cell: Cell<TData, unknown>) => ( </td>
<td ) : null}
key={cell.id} {row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
className='px-2 border-y' <td
style={{ key={cell.id}
cursor: onRowClicked || onRowDoubleClicked ? 'pointer': 'auto', className='px-2 border-y'
paddingBottom: dense ? '0.25rem': '0.5rem', style={{
paddingTop: dense ? '0.25rem': '0.5rem' cursor: onRowClicked || onRowDoubleClicked ? 'pointer' : 'auto',
}} paddingBottom: dense ? '0.25rem' : '0.5rem',
onClick={event => handleRowClicked(row, event)} paddingTop: dense ? '0.25rem' : '0.5rem'
onDoubleClick={event => onRowDoubleClicked ? onRowDoubleClicked(row.original, event) : undefined} }}
> onClick={event => handleRowClicked(row, event)}
{flexRender(cell.column.columnDef.cell, cell.getContext())} onDoubleClick={event => (onRowDoubleClicked ? onRowDoubleClicked(row.original, event) : undefined)}
</td> >
))} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</tr> </td>
))} ))}
</tbody>); </tr>
))}
</tbody>
);
} }
export default TableBody; export default TableBody;

View File

@ -1,24 +1,23 @@
import { flexRender, Header, HeaderGroup, Table } from '@tanstack/react-table'; import { flexRender, Header, HeaderGroup, Table } from '@tanstack/react-table';
interface TableFooterProps<TData> { interface TableFooterProps<TData> {
table: Table<TData> table: Table<TData>;
} }
function TableFooter<TData>({ table }: TableFooterProps<TData>) { function TableFooter<TData>({ table }: TableFooterProps<TData>) {
return ( return (
<tfoot> <tfoot>
{table.getFooterGroups().map( {table.getFooterGroups().map((footerGroup: HeaderGroup<TData>) => (
(footerGroup: HeaderGroup<TData>) => ( <tr key={footerGroup.id}>
<tr key={footerGroup.id}> {footerGroup.headers.map((header: Header<TData, unknown>) => (
{footerGroup.headers.map( <th key={header.id}>
(header: Header<TData, unknown>) => ( {!header.isPlaceholder ? flexRender(header.column.columnDef.footer, header.getContext()) : null}
<th key={header.id}> </th>
{!header.isPlaceholder ? flexRender(header.column.columnDef.footer, header.getContext()) : null} ))}
</th> </tr>
))} ))}
</tr> </tfoot>
))} );
</tfoot>);
} }
export default TableFooter; export default TableFooter;

View File

@ -4,53 +4,52 @@ import SelectAll from './SelectAll';
import SortingIcon from './SortingIcon'; import SortingIcon from './SortingIcon';
interface TableHeaderProps<TData> { interface TableHeaderProps<TData> {
table: Table<TData> table: Table<TData>;
headPosition?: string headPosition?: string;
enableRowSelection?: boolean enableRowSelection?: boolean;
enableSorting?: boolean enableSorting?: boolean;
} }
function TableHeader<TData>({ function TableHeader<TData>({ table, headPosition, enableRowSelection, enableSorting }: TableHeaderProps<TData>) {
table, headPosition,
enableRowSelection, enableSorting
}: TableHeaderProps<TData>) {
return ( return (
<thead <thead
className={`clr-app shadow-border`} className={`clr-app shadow-border`}
style={{ style={{
top: headPosition, top: headPosition,
position: 'sticky' position: 'sticky'
}} }}
> >
{table.getHeaderGroups().map( {table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
(headerGroup: HeaderGroup<TData>) => ( <tr key={headerGroup.id}>
<tr key={headerGroup.id}> {enableRowSelection ? (
{enableRowSelection ? <th className='pl-3 pr-1'>
<th className='pl-3 pr-1'> <SelectAll table={table} />
<SelectAll table={table} /> </th>
</th> : null} ) : null}
{headerGroup.headers.map( {headerGroup.headers.map((header: Header<TData, unknown>) => (
(header: Header<TData, unknown>) => ( <th
<th key={header.id} key={header.id}
colSpan={header.colSpan} colSpan={header.colSpan}
className='px-2 py-2 text-xs font-semibold select-none whitespace-nowrap' className='px-2 py-2 text-xs font-semibold select-none whitespace-nowrap'
style={{ style={{
textAlign: header.getSize() > 100 ? 'left': 'center', textAlign: header.getSize() > 100 ? 'left' : 'center',
width: header.getSize(), width: header.getSize(),
cursor: enableSorting && header.column.getCanSort() ? 'pointer': 'auto', cursor: enableSorting && header.column.getCanSort() ? 'pointer' : 'auto'
}} }}
onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined} onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined}
> >
{!header.isPlaceholder ? ( {!header.isPlaceholder ? (
<div className='flex gap-1'> <div className='flex gap-1'>
{flexRender(header.column.columnDef.header, header.getContext())} {flexRender(header.column.columnDef.header, header.getContext())}
{(enableSorting && header.column.getCanSort()) ? <SortingIcon column={header.column} /> : null} {enableSorting && header.column.getCanSort() ? <SortingIcon column={header.column} /> : null}
</div>) : null} </div>
</th> ) : null}
</th>
))}
</tr>
))} ))}
</tr> </thead>
))} );
</thead>);
} }
export default TableHeader; export default TableHeader;

View File

@ -1,5 +1,7 @@
export { export {
default, default,
createColumnHelper, type IConditionalStyle, createColumnHelper,
type RowSelectionState, type VisibilityState type IConditionalStyle,
} from './DataTable'; type RowSelectionState,
type VisibilityState
} from './DataTable';

View File

@ -5,14 +5,12 @@ import InfoError from './InfoError';
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return ( return (
<div className='flex flex-col items-center antialiased clr-app' role='alert'> <div className='flex flex-col items-center antialiased clr-app' role='alert'>
<h1>Что-то пошло не так!</h1> <h1>Что-то пошло не так!</h1>
<Button <Button onClick={resetErrorBoundary} text='Попробовать еще раз' />
onClick={resetErrorBoundary} <InfoError error={error as Error} />
text='Попробовать еще раз' </div>
/> );
<InfoError error={error as Error} />
</div>);
} }
export default ErrorFallback; export default ErrorFallback;

View File

@ -12,23 +12,21 @@ function ExpectedAnonymous() {
} }
return ( return (
<div className='flex flex-col items-center gap-3 py-6'> <div className='flex flex-col items-center gap-3 py-6'>
<p className='font-semibold'>{`Вы вошли в систему как ${user?.username ?? ''}`}</p> <p className='font-semibold'>{`Вы вошли в систему как ${user?.username ?? ''}`}</p>
<div className='flex gap-3'> <div className='flex gap-3'>
<TextURL text='Новая схема' href='/library/create'/> <TextURL text='Новая схема' href='/library/create' />
<span> | </span> <span> | </span>
<TextURL text='Библиотека' href='/library'/> <TextURL text='Библиотека' href='/library' />
<span> | </span> <span> | </span>
<TextURL text='Справка' href='/manuals'/> <TextURL text='Справка' href='/manuals' />
<span> | </span> <span> | </span>
<span <span className='cursor-pointer hover:underline clr-text-url' onClick={logoutAndRedirect}>
className='cursor-pointer hover:underline clr-text-url' Выйти
onClick={logoutAndRedirect} </span>
> </div>
Выйти
</span>
</div> </div>
</div>); );
} }
export default ExpectedAnonymous; export default ExpectedAnonymous;

View File

@ -11,23 +11,25 @@ function Footer() {
return null; return null;
} }
return ( return (
<footer tabIndex={-1} <footer
className={clsx( tabIndex={-1}
'z-navigation', className={clsx(
'px-4 py-2 flex flex-col items-center gap-1', 'z-navigation',
'text-sm select-none whitespace-nowrap' 'px-4 py-2 flex flex-col items-center gap-1',
)} 'text-sm select-none whitespace-nowrap'
> )}
<div className='flex gap-3'> >
<TextURL text='Библиотека' href='/library' color='clr-footer'/> <div className='flex gap-3'>
<TextURL text='Справка' href='/manuals' color='clr-footer'/> <TextURL text='Библиотека' href='/library' color='clr-footer' />
<TextURL text='Центр Концепт' href={urls.concept} color='clr-footer'/> <TextURL text='Справка' href='/manuals' color='clr-footer' />
<TextURL text='Экстеор' href='/manuals?topic=exteor' color='clr-footer'/> <TextURL text='Центр Концепт' href={urls.concept} color='clr-footer' />
</div> <TextURL text='Экстеор' href='/manuals?topic=exteor' color='clr-footer' />
<div> </div>
<p className='clr-footer'>© 2024 ЦИВТ КОНЦЕПТ</p> <div>
</div> <p className='clr-footer'>© 2024 ЦИВТ КОНЦЕПТ</p>
</footer>); </div>
</footer>
);
} }
export default Footer; export default Footer;

View File

@ -3,18 +3,16 @@ import InfoConstituenta from '@/components/Shared/InfoConstituenta';
import { IConstituenta } from '@/models/rsform'; import { IConstituenta } from '@/models/rsform';
interface ConstituentaTooltipProps { interface ConstituentaTooltipProps {
data: IConstituenta data: IConstituenta;
anchor: string anchor: string;
} }
function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) { function ConstituentaTooltip({ data, anchor }: ConstituentaTooltipProps) {
return ( return (
<ConceptTooltip clickable <ConceptTooltip clickable anchorSelect={anchor} className='max-w-[30rem]'>
anchorSelect={anchor} <InfoConstituenta data={data} />
className='max-w-[30rem]' </ConceptTooltip>
> );
<InfoConstituenta data={data} />
</ConceptTooltip>);
} }
export default ConstituentaTooltip; export default ConstituentaTooltip;

View File

@ -7,32 +7,25 @@ import { HelpTopic } from '@/models/miscellaneous';
import { CProps } from '../props'; import { CProps } from '../props';
import InfoTopic from './InfoTopic'; import InfoTopic from './InfoTopic';
interface HelpButtonProps interface HelpButtonProps extends CProps.Styling {
extends CProps.Styling { topic: HelpTopic;
topic: HelpTopic offset?: number;
offset?: number
} }
function HelpButton({ topic, ...restProps }: HelpButtonProps) { function HelpButton({ topic, ...restProps }: HelpButtonProps) {
return ( return (
<div <div id={`help-${topic}`} className='p-1'>
id={`help-${topic}`} <BiInfoCircle size='1.25rem' className='clr-text-primary' />
className='p-1' <ConceptTooltip clickable anchorSelect={`#help-${topic}`} layer='z-modal-tooltip' {...restProps}>
> <div className='relative'>
<BiInfoCircle size='1.25rem' className='clr-text-primary' /> <div className='absolute right-0 text-sm top-[0.4rem]'>
<ConceptTooltip clickable <TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
anchorSelect={`#help-${topic}`} </div>
layer='z-modal-tooltip' </div>
{...restProps} <InfoTopic topic={topic} />
> </ConceptTooltip>
<div className='relative'> </div>
<div className='absolute right-0 text-sm top-[0.4rem]'> );
<TextURL text='Справка...' href={`/manuals?topic=${topic}`} />
</div>
</div>
<InfoTopic topic={topic} />
</ConceptTooltip>
</div>);
} }
export default HelpButton; export default HelpButton;

View File

@ -2,6 +2,7 @@ import Divider from '@/components/Common/Divider';
import InfoCstStatus from '@/components/Shared/InfoCstStatus'; import InfoCstStatus from '@/components/Shared/InfoCstStatus';
function HelpConstituenta() { function HelpConstituenta() {
// prettier-ignore
return ( return (
<div className='leading-tight'> <div className='leading-tight'>
<h1>Редактор конституент</h1> <h1>Редактор конституент</h1>
@ -22,4 +23,4 @@ function HelpConstituenta() {
</div>); </div>);
} }
export default HelpConstituenta; export default HelpConstituenta;

View File

@ -2,6 +2,7 @@ import TextURL from '@/components/Common/TextURL';
import { urls } from '@/utils/constants'; import { urls } from '@/utils/constants';
function HelpExteor() { function HelpExteor() {
// prettier-ignore
return ( return (
<div> <div>
<h1>Экстеор</h1> <h1>Экстеор</h1>
@ -24,4 +25,4 @@ function HelpExteor() {
</div>); </div>);
} }
export default HelpExteor; export default HelpExteor;

View File

@ -2,6 +2,7 @@ import { BiCheckShield, BiShareAlt } from 'react-icons/bi';
import { FiBell } from 'react-icons/fi'; import { FiBell } from 'react-icons/fi';
function HelpLibrary() { function HelpLibrary() {
// prettier-ignore
return ( return (
<div className='max-w-[80rem]'> <div className='max-w-[80rem]'>
<h1>Библиотека концептуальных схем</h1> <h1>Библиотека концептуальных схем</h1>
@ -24,4 +25,4 @@ function HelpLibrary() {
</div>); </div>);
} }
export default HelpLibrary; export default HelpLibrary;

View File

@ -2,6 +2,7 @@ import TextURL from '@/components/Common/TextURL';
import { urls } from '@/utils/constants'; import { urls } from '@/utils/constants';
function HelpMain() { function HelpMain() {
// prettier-ignore
return ( return (
<div className='max-w-[60rem]'> <div className='max-w-[60rem]'>
<h1>Портал</h1> <h1>Портал</h1>
@ -20,4 +21,4 @@ function HelpMain() {
</div>); </div>);
} }
export default HelpMain; export default HelpMain;

View File

@ -1,11 +1,8 @@
import PDFViewer from '@/components/Common/PDFViewer'; import PDFViewer from '@/components/PDFViewer';
import { resources } from '@/utils/constants'; import { resources } from '@/utils/constants';
function HelpPrivacy() { function HelpPrivacy() {
return ( return <PDFViewer file={resources.privacy_policy} />;
<PDFViewer
file={resources.privacy_policy}
/>);
} }
export default HelpPrivacy; export default HelpPrivacy;

View File

@ -2,6 +2,7 @@ import Divider from '@/components/Common/Divider';
import InfoCstStatus from '@/components/Shared/InfoCstStatus'; import InfoCstStatus from '@/components/Shared/InfoCstStatus';
function HelpRSFormItems() { function HelpRSFormItems() {
// prettier-ignore
return ( return (
<div> <div>
<h1>Горячие клавиши</h1> <h1>Горячие клавиши</h1>
@ -15,4 +16,4 @@ function HelpRSFormItems() {
</div>); </div>);
} }
export default HelpRSFormItems; export default HelpRSFormItems;

View File

@ -1,4 +1,5 @@
function HelpRSFormMeta() { function HelpRSFormMeta() {
// prettier-ignore
return ( return (
<div> <div>
<h1>Паспорт схемы</h1> <h1>Паспорт схемы</h1>
@ -12,4 +13,4 @@ function HelpRSFormMeta() {
</div>); </div>);
} }
export default HelpRSFormMeta; export default HelpRSFormMeta;

View File

@ -9,13 +9,13 @@ const OPT_VIDEO_H = 1080;
function HelpRSLang() { function HelpRSLang() {
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const videoHeight = useMemo( const videoHeight = useMemo(() => {
() => {
const viewH = windowSize.height ?? 0; const viewH = windowSize.height ?? 0;
const viewW = windowSize.width ?? 0; const viewW = windowSize.width ?? 0;
return Math.min(OPT_VIDEO_H, viewH - 320, Math.floor((viewW - 290)*9/16)); return Math.min(OPT_VIDEO_H, viewH - 320, Math.floor(((viewW - 290) * 9) / 16));
}, [windowSize]); }, [windowSize]);
// prettier-ignore
return ( return (
<div className='flex flex-col gap-4'> <div className='flex flex-col gap-4'>
<div> <div>
@ -38,4 +38,4 @@ function HelpRSLang() {
</div>); </div>);
} }
export default HelpRSLang; export default HelpRSLang;

View File

@ -1,4 +1,5 @@
function HelpRSTemplates() { function HelpRSTemplates() {
// prettier-ignore
return ( return (
<div className='flex flex-col gap-2 pb-2 max-w-[80rem]'> <div className='flex flex-col gap-2 pb-2 max-w-[80rem]'>
<h1>Банк выражений</h1> <h1>Банк выражений</h1>
@ -13,4 +14,4 @@ function HelpRSTemplates() {
</div>); </div>);
} }
export default HelpRSTemplates; export default HelpRSTemplates;

View File

@ -3,6 +3,7 @@ import InfoCstClass from '@/components/Shared/InfoCstClass';
import InfoCstStatus from '@/components/Shared/InfoCstStatus'; import InfoCstStatus from '@/components/Shared/InfoCstStatus';
function HelpTermGraph() { function HelpTermGraph() {
// prettier-ignore
return ( return (
<div className='flex max-w-[80rem]'> <div className='flex max-w-[80rem]'>
<div> <div>
@ -33,4 +34,4 @@ function HelpTermGraph() {
</div>); </div>);
} }
export default HelpTermGraph; export default HelpTermGraph;

View File

@ -1,5 +1,5 @@
function HelpTerminologyControl() { function HelpTerminologyControl() {
// prettier-ignore
return ( return (
<div className='flex flex-col gap-1 max-w-[80rem]'> <div className='flex flex-col gap-1 max-w-[80rem]'>
<h1>Терминологизация</h1> <h1>Терминологизация</h1>
@ -13,4 +13,4 @@ function HelpTerminologyControl() {
</div>); </div>);
} }
export default HelpTerminologyControl; export default HelpTerminologyControl;

View File

@ -14,7 +14,7 @@ import HelpTermGraph from './HelpTermGraph';
import HelpTerminologyControl from './HelpTerminologyControl'; import HelpTerminologyControl from './HelpTerminologyControl';
interface InfoTopicProps { interface InfoTopicProps {
topic: HelpTopic topic: HelpTopic;
} }
function InfoTopic({ topic }: InfoTopicProps) { function InfoTopic({ topic }: InfoTopicProps) {
@ -33,4 +33,4 @@ function InfoTopic({ topic }: InfoTopicProps) {
return null; return null;
} }
export default InfoTopic; export default InfoTopic;

View File

@ -1,30 +1,31 @@
// Search new icons at https://reactsvgicons.com/ // Search new icons at https://reactsvgicons.com/
interface IconSVGProps { interface IconSVGProps {
viewBox: string viewBox: string;
size?: string size?: string;
className?: string className?: string;
props?: React.SVGProps<SVGSVGElement> props?: React.SVGProps<SVGSVGElement>;
children: React.ReactNode children: React.ReactNode;
} }
export interface IconProps { export interface IconProps {
size?: string size?: string;
className?: string className?: string;
} }
function IconSVG({ viewBox, size = '1.5rem', className, props, children }: IconSVGProps) { function IconSVG({ viewBox, size = '1.5rem', className, props, children }: IconSVGProps) {
return ( return (
<svg <svg
width={size} width={size}
height={size} height={size}
className={`w-[${size}] h-[${size}] ${className}`} className={`w-[${size}] h-[${size}] ${className}`}
fill='currentColor' fill='currentColor'
viewBox={viewBox} viewBox={viewBox}
{...props} {...props}
> >
{children} {children}
</svg>); </svg>
);
} }
export function EducationIcon(props: IconProps) { export function EducationIcon(props: IconProps) {
@ -46,11 +47,7 @@ export function InDoorIcon(props: IconProps) {
export function CheckboxCheckedIcon() { export function CheckboxCheckedIcon() {
return ( return (
<svg <svg className='w-3 h-3' viewBox='0 0 512 512' fill='#ffffff'>
className='w-3 h-3'
viewBox='0 0 512 512'
fill='#ffffff'
>
<path d='M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7l233.4-233.3c12.5-12.5 32.8-12.5 45.3 0z' /> <path d='M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7l233.4-233.3c12.5-12.5 32.8-12.5 45.3 0z' />
</svg> </svg>
); );
@ -58,12 +55,8 @@ export function CheckboxCheckedIcon() {
export function CheckboxNullIcon() { export function CheckboxNullIcon() {
return ( return (
<svg <svg className='w-3 h-3' viewBox='0 0 16 16' fill='#ffffff'>
className='w-3 h-3'
viewBox='0 0 16 16'
fill='#ffffff'
>
<path d='M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z' /> <path d='M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z' />
</svg> </svg>
); );
} }

View File

@ -7,10 +7,10 @@ import PrettyJson from './Common/PrettyJSON';
export type ErrorData = string | Error | AxiosError | undefined; export type ErrorData = string | Error | AxiosError | undefined;
interface InfoErrorProps { interface InfoErrorProps {
error: ErrorData error: ErrorData;
} }
function DescribeError({error} : {error: ErrorData}) { function DescribeError({ error }: { error: ErrorData }) {
if (!error) { if (!error) {
return <p>Ошибки отсутствуют</p>; return <p>Ошибки отсутствуют</p>;
} else if (typeof error === 'string') { } else if (typeof error === 'string') {
@ -23,10 +23,11 @@ function DescribeError({error} : {error: ErrorData}) {
} }
if (error.response.status === 404) { if (error.response.status === 404) {
return ( return (
<div> <div>
<p>{'Обращение к несуществующему API'}</p> <p>{'Обращение к несуществующему API'}</p>
<PrettyJson data={error} /> <PrettyJson data={error} />
</div>); </div>
);
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
@ -35,20 +36,23 @@ function DescribeError({error} : {error: ErrorData}) {
<div> <div>
<p className='underline'>Ошибка</p> <p className='underline'>Ошибка</p>
<p>{error.message}</p> <p>{error.message}</p>
{error.response.data && (<> {error.response.data && (
<p className='mt-2 underline'>Описание</p> <>
{isHtml ? <div dangerouslySetInnerHTML={{ __html: error.response.data as TrustedHTML }} /> : null} <p className='mt-2 underline'>Описание</p>
{!isHtml ? <PrettyJson data={error.response.data as object} /> : null} {isHtml ? <div dangerouslySetInnerHTML={{ __html: error.response.data as TrustedHTML }} /> : null}
</>)} {!isHtml ? <PrettyJson data={error.response.data as object} /> : null}
</>
)}
</div> </div>
); );
} }
function InfoError({ error }: InfoErrorProps) { function InfoError({ error }: InfoErrorProps) {
return ( return (
<div className='px-3 py-2 min-w-[15rem] text-sm font-semibold select-text clr-text-warning'> <div className='px-3 py-2 min-w-[15rem] text-sm font-semibold select-text clr-text-warning'>
<DescribeError error={error} /> <DescribeError error={error} />
</div>); </div>
);
} }
export default InfoError; export default InfoError;

View File

@ -3,9 +3,11 @@ import { useConceptTheme } from '@/context/ThemeContext';
function Logo() { function Logo() {
const { darkMode } = useConceptTheme(); const { darkMode } = useConceptTheme();
return ( return (
<img alt='Логотип КонцептПортал' <img
className='max-h-[1.6rem] min-w-[11.5rem]' alt='Логотип КонцептПортал'
src={!darkMode ? '/logo_full.svg' : '/logo_full_dark.svg'} className='max-h-[1.6rem] min-w-[11.5rem]'
/>); src={!darkMode ? '/logo_full.svg' : '/logo_full_dark.svg'}
/>
);
} }
export default Logo; export default Logo;

View File

@ -13,7 +13,7 @@ import NavigationButton from './NavigationButton';
import ToggleNavigationButton from './ToggleNavigationButton'; import ToggleNavigationButton from './ToggleNavigationButton';
import UserMenu from './UserMenu'; import UserMenu from './UserMenu';
function Navigation () { function Navigation() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { noNavigationAnimation } = useConceptTheme(); const { noNavigationAnimation } = useConceptTheme();
@ -21,54 +21,43 @@ function Navigation () {
const navigateLibrary = () => router.push('/library'); const navigateLibrary = () => router.push('/library');
const navigateHelp = () => router.push('/manuals'); const navigateHelp = () => router.push('/manuals');
const navigateCreateNew = () => router.push('/library/create'); const navigateCreateNew = () => router.push('/library/create');
return ( return (
<nav className={clsx( <nav className={clsx('z-navigation', 'sticky top-0 left-0 right-0', 'clr-app', 'select-none')}>
'z-navigation', <ToggleNavigationButton />
'sticky top-0 left-0 right-0', <motion.div
'clr-app', className={clsx('pl-2 pr-[0.9rem] h-[3rem]', 'flex justify-between', 'shadow-border')}
'select-none' initial={false}
)}> animate={!noNavigationAnimation ? 'open' : 'closed'}
<ToggleNavigationButton /> variants={animateNavigation}
<motion.div
className={clsx(
'pl-2 pr-[0.9rem] h-[3rem]',
'flex justify-between',
'shadow-border'
)}
initial={false}
animate={!noNavigationAnimation ? 'open' : 'closed'}
variants={animateNavigation}
>
<div tabIndex={-1}
className='flex items-center mr-2 cursor-pointer'
onClick={navigateHome}
> >
<Logo /> <div tabIndex={-1} className='flex items-center mr-2 cursor-pointer' onClick={navigateHome}>
</div> <Logo />
<div className='flex'> </div>
<NavigationButton <div className='flex'>
text='Новая схема' <NavigationButton
title='Создать новую схему' text='Новая схема'
icon={<FaSquarePlus size='1.5rem' />} title='Создать новую схему'
onClick={navigateCreateNew} icon={<FaSquarePlus size='1.5rem' />}
/> onClick={navigateCreateNew}
<NavigationButton />
text='Библиотека' <NavigationButton
title='Список схем' text='Библиотека'
icon={<IoLibrary size='1.5rem' />} title='Список схем'
onClick={navigateLibrary} icon={<IoLibrary size='1.5rem' />}
/> onClick={navigateLibrary}
<NavigationButton />
text='Справка' <NavigationButton
title='Справочные материалы' text='Справка'
icon={<EducationIcon />} title='Справочные материалы'
onClick={navigateHelp} icon={<EducationIcon />}
/> onClick={navigateHelp}
<UserMenu /> />
</div> <UserMenu />
</motion.div> </div>
</nav>); </motion.div>
</nav>
);
} }
export default Navigation; export default Navigation;

View File

@ -3,32 +3,35 @@ import clsx from 'clsx';
import { globalIDs } from '@/utils/constants'; import { globalIDs } from '@/utils/constants';
interface NavigationButtonProps { interface NavigationButtonProps {
text?: string text?: string;
icon: React.ReactNode icon: React.ReactNode;
title?: string title?: string;
onClick?: () => void onClick?: () => void;
} }
function NavigationButton({ icon, title, onClick, text }: NavigationButtonProps) { function NavigationButton({ icon, title, onClick, text }: NavigationButtonProps) {
return ( return (
<button type='button' tabIndex={-1} <button
data-tooltip-id={title ? (globalIDs.tooltip) : undefined} type='button'
data-tooltip-content={title} tabIndex={-1}
onClick={onClick} data-tooltip-id={title ? globalIDs.tooltip : undefined}
className={clsx( data-tooltip-content={title}
'mr-1 h-full', onClick={onClick}
'flex items-center gap-1', className={clsx(
'clr-btn-nav', 'mr-1 h-full', //
'small-caps whitespace-nowrap', 'flex items-center gap-1',
{ 'clr-btn-nav',
'px-2': text, 'small-caps whitespace-nowrap',
'px-4': !text {
} 'px-2': text,
)} 'px-4': !text
> }
{icon ? <span>{icon}</span> : null} )}
{text ? <span className='font-semibold'>{text}</span> : null} >
</button>); {icon ? <span>{icon}</span> : null}
{text ? <span className='font-semibold'>{text}</span> : null}
</button>
);
} }
export default NavigationButton; export default NavigationButton;

View File

@ -8,21 +8,24 @@ import { animateNavigationToggle } from '@/utils/animations';
function ToggleNavigationButton() { function ToggleNavigationButton() {
const { noNavigationAnimation, toggleNoNavigation } = useConceptTheme(); const { noNavigationAnimation, toggleNoNavigation } = useConceptTheme();
return ( return (
<motion.button type='button' tabIndex={-1} <motion.button
title={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'} type='button'
className={clsx( tabIndex={-1}
'absolute top-0 right-0 z-navigation flex items-center justify-center', title={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
'clr-btn-nav', className={clsx(
'select-none disabled:cursor-not-allowed' 'absolute top-0 right-0 z-navigation flex items-center justify-center',
)} 'clr-btn-nav',
onClick={toggleNoNavigation} 'select-none disabled:cursor-not-allowed'
initial={false} )}
animate={noNavigationAnimation ? 'off' : 'on'} onClick={toggleNoNavigation}
variants={animateNavigationToggle} initial={false}
> animate={noNavigationAnimation ? 'off' : 'on'}
{!noNavigationAnimation ? <RiPushpinFill /> : null} variants={animateNavigationToggle}
{noNavigationAnimation ? <RiUnpinLine /> : null} >
</motion.button>); {!noNavigationAnimation ? <RiPushpinFill /> : null}
{noNavigationAnimation ? <RiUnpinLine /> : null}
</motion.button>
);
} }
export default ToggleNavigationButton; export default ToggleNavigationButton;

View File

@ -5,8 +5,8 @@ import { useConceptNavigation } from '@/context/NavigationContext';
import { useConceptTheme } from '@/context/ThemeContext'; import { useConceptTheme } from '@/context/ThemeContext';
interface UserDropdownProps { interface UserDropdownProps {
isOpen: boolean isOpen: boolean;
hideDropdown: () => void hideDropdown: () => void;
} }
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) { function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
@ -19,30 +19,22 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
router.push('/profile'); router.push('/profile');
}; };
const logoutAndRedirect = const logoutAndRedirect = () => {
() => {
hideDropdown(); hideDropdown();
logout(() => router.push('/login/')); logout(() => router.push('/login/'));
}; };
return ( return (
<Dropdown className='w-36' stretchLeft isOpen={isOpen}> <Dropdown className='w-36' stretchLeft isOpen={isOpen}>
<DropdownButton <DropdownButton text={user?.username} title='Профиль пользователя' onClick={navigateProfile} />
text={user?.username} <DropdownButton
title='Профиль пользователя' text={darkMode ? 'Светлая тема' : 'Темная тема'}
onClick={navigateProfile} title='Переключение темы оформления'
/> onClick={toggleDarkMode}
<DropdownButton />
text={darkMode ? 'Светлая тема' : 'Темная тема'} <DropdownButton text='Выйти...' className='font-semibold' onClick={logoutAndRedirect} />
title='Переключение темы оформления' </Dropdown>
onClick={toggleDarkMode} );
/>
<DropdownButton
text='Выйти...'
className='font-semibold'
onClick={logoutAndRedirect}
/>
</Dropdown>);
} }
export default UserDropdown; export default UserDropdown;

View File

@ -15,23 +15,18 @@ function UserMenu() {
const navigateLogin = () => router.push('/login'); const navigateLogin = () => router.push('/login');
return ( return (
<div ref={menu.ref} className='h-full'> <div ref={menu.ref} className='h-full'>
{!user ? {!user ? (
<NavigationButton <NavigationButton
title='Перейти на страницу логина' title='Перейти на страницу логина'
icon={<InDoorIcon size='1.5rem' className='clr-text-primary' />} icon={<InDoorIcon size='1.5rem' className='clr-text-primary' />}
onClick={navigateLogin} onClick={navigateLogin}
/> : null} />
{user ? ) : null}
<NavigationButton {user ? <NavigationButton icon={<FaCircleUser size='1.5rem' />} onClick={menu.toggle} /> : null}
icon={<FaCircleUser size='1.5rem' />} <UserDropdown isOpen={!!user && menu.isOpen} hideDropdown={() => menu.hide()} />
onClick={menu.toggle} </div>
/> : null} );
<UserDropdown
isOpen={!!user && menu.isOpen}
hideDropdown={() => menu.hide()}
/>
</div>);
} }
export default UserMenu; export default UserMenu;

View File

@ -1 +1 @@
export { default } from './Navigation'; export { default } from './Navigation';

View File

@ -0,0 +1,60 @@
'use client';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import { useMemo, useState } from 'react';
import { Document, Page } from 'react-pdf';
import useWindowSize from '@/hooks/useWindowSize';
import { graphLightT } from '@/utils/color';
import Overlay from '../Common/Overlay';
import PageControls from './PageControls';
const MAXIMUM_WIDTH = 1000;
const MINIMUM_WIDTH = 600;
interface PDFViewerProps {
file?: string | ArrayBuffer | Blob;
}
function PDFViewer({ file }: PDFViewerProps) {
const windowSize = useWindowSize();
const [pageCount, setPageCount] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
const pageWidth = useMemo(() => {
return Math.max(MINIMUM_WIDTH, Math.min((windowSize?.width ?? 0) - 300, MAXIMUM_WIDTH));
}, [windowSize]);
function onDocumentLoadSuccess({ numPages }: PDFDocumentProxy) {
setPageCount(numPages);
}
return (
<Document
file={file}
onLoadSuccess={onDocumentLoadSuccess}
className='px-3'
loading='Загрузка PDF файла...'
error='Не удалось загрузить файл.'
>
<Overlay position='top-6 left-1/2 -translate-x-1/2' className='flex select-none'>
<PageControls pageCount={pageCount} pageNumber={pageNumber} setPageNumber={setPageNumber} />
</Overlay>
<Page
className='pointer-events-none select-none'
renderTextLayer={false}
renderAnnotationLayer={false}
pageNumber={pageNumber}
width={pageWidth}
canvasBackground={graphLightT.canvas.background}
/>
<Overlay position='bottom-6 left-1/2 -translate-x-1/2' className='flex select-none'>
<PageControls pageCount={pageCount} pageNumber={pageNumber} setPageNumber={setPageNumber} />
</Overlay>
</Document>
);
}
export default PDFViewer;

View File

@ -0,0 +1,51 @@
import { BiChevronLeft, BiChevronRight, BiFirstPage, BiLastPage } from 'react-icons/bi';
interface PageControlsProps {
pageNumber: number;
pageCount: number;
setPageNumber: React.Dispatch<React.SetStateAction<number>>;
}
function PageControls({ pageNumber, pageCount, setPageNumber }: PageControlsProps) {
return (
<>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(1)}
disabled={pageNumber < 2}
>
<BiFirstPage size='1.5rem' />
</button>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(prev => prev - 1)}
disabled={pageNumber < 2}
>
<BiChevronLeft size='1.5rem' />
</button>
<p className='px-3 text-black'>
Страница {pageNumber} из {pageCount}
</p>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(prev => prev + 1)}
disabled={pageNumber >= pageCount}
>
<BiChevronRight size='1.5rem' />
</button>
<button
type='button'
className='clr-hover clr-text-controls'
onClick={() => setPageNumber(pageCount)}
disabled={pageNumber >= pageCount}
>
<BiLastPage size='1.5rem' />
</button>
</>
);
}
export default PageControls;

View File

@ -0,0 +1 @@
export { default } from './PDFViewer';

View File

@ -14,7 +14,7 @@ import { useConceptTheme } from '@/context/ThemeContext';
import { ccBracketMatching } from './bracketMatching'; import { ccBracketMatching } from './bracketMatching';
import { RSLanguage } from './rslang'; import { RSLanguage } from './rslang';
import { getSymbolSubstitute,RSTextWrapper } from './textEditing'; import { getSymbolSubstitute, RSTextWrapper } from './textEditing';
import { rsHoverTooltip } from './tooltip'; import { rsHoverTooltip } from './tooltip';
const editorSetup: BasicSetupOptions = { const editorSetup: BasicSetupOptions = {
@ -45,108 +45,106 @@ const editorSetup: BasicSetupOptions = {
lintKeymap: false lintKeymap: false
}; };
interface RSInputProps interface RSInputProps
extends Pick<ReactCodeMirrorProps, extends Pick<
'id' | 'height' | 'minHeight' | 'maxHeight' | 'value' | ReactCodeMirrorProps,
'onFocus' | 'onBlur' | 'placeholder' | 'style' | 'className' 'id' | 'height' | 'minHeight' | 'maxHeight' | 'value' | 'onFocus' | 'onBlur' | 'placeholder' | 'style' | 'className'
> { > {
label?: string label?: string;
disabled?: boolean disabled?: boolean;
noTooltip?: boolean noTooltip?: boolean;
onChange?: (newValue: string) => void onChange?: (newValue: string) => void;
onAnalyze?: () => void onAnalyze?: () => void;
} }
const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>( const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
({ ({ id, label, onChange, onAnalyze, disabled, noTooltip, className, style, ...restProps }, ref) => {
id, label, onChange, onAnalyze, const { darkMode, colors } = useConceptTheme();
disabled, noTooltip, const { schema } = useRSForm();
className, style,
...restProps
}, ref) => {
const { darkMode, colors } = useConceptTheme();
const { schema } = useRSForm();
const internalRef = useRef<ReactCodeMirrorRef>(null); const internalRef = useRef<ReactCodeMirrorRef>(null);
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]); const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
const cursor = useMemo(() => !disabled ? 'cursor-text': 'cursor-default', [disabled]); const cursor = useMemo(() => (!disabled ? 'cursor-text' : 'cursor-default'), [disabled]);
const customTheme: Extension = useMemo( const customTheme: Extension = useMemo(
() => createTheme({ () =>
theme: darkMode ? 'dark' : 'light', createTheme({
settings: { theme: darkMode ? 'dark' : 'light',
fontFamily: 'inherit', settings: {
background: !disabled ? colors.bgInput : colors.bgDefault, fontFamily: 'inherit',
foreground: colors.fgDefault, background: !disabled ? colors.bgInput : colors.bgDefault,
selection: colors.bgHover foreground: colors.fgDefault,
}, selection: colors.bgHover
styles: [ },
{ tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // GlobalID styles: [
{ tag: tags.variableName, color: colors.fgGreen }, // LocalID { tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // GlobalID
{ tag: tags.propertyName, color: colors.fgTeal }, // Radical { tag: tags.variableName, color: colors.fgGreen }, // LocalID
{ tag: tags.keyword, color: colors.fgBlue }, // keywords { tag: tags.propertyName, color: colors.fgTeal }, // Radical
{ tag: tags.literal, color: colors.fgBlue }, // literals { tag: tags.keyword, color: colors.fgBlue }, // keywords
{ tag: tags.controlKeyword, fontWeight: '500'}, // R | I | D { tag: tags.literal, color: colors.fgBlue }, // literals
{ tag: tags.unit, fontSize: '0.75rem' }, // indices { tag: tags.controlKeyword, fontWeight: '500' }, // R | I | D
{ tag: tags.brace, color:colors.fgPurple, fontWeight: '700' }, // braces (curly brackets) { tag: tags.unit, fontSize: '0.75rem' }, // indices
] { tag: tags.brace, color: colors.fgPurple, fontWeight: '700' } // braces (curly brackets)
}), [disabled, colors, darkMode]); ]
}),
[disabled, colors, darkMode]
);
const editorExtensions = useMemo( const editorExtensions = useMemo(
() => [ () => [
EditorView.lineWrapping, EditorView.lineWrapping,
RSLanguage, RSLanguage,
ccBracketMatching(darkMode), ccBracketMatching(darkMode),
... noTooltip ? [] : [rsHoverTooltip(schema?.items || [])], ...(noTooltip ? [] : [rsHoverTooltip(schema?.items || [])])
], [darkMode, schema?.items, noTooltip]); ],
[darkMode, schema?.items, noTooltip]
);
const handleInput = useCallback( const handleInput = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => { (event: React.KeyboardEvent<HTMLDivElement>) => {
if (!thisRef.current) { if (!thisRef.current) {
return; return;
} }
const text = new RSTextWrapper(thisRef.current as Required<ReactCodeMirrorRef>); const text = new RSTextWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
if (event.altKey) { if (event.altKey) {
if (text.processAltKey(event.code, event.shiftKey)) { if (text.processAltKey(event.code, event.shiftKey)) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
} else if (!event.ctrlKey) { } else if (!event.ctrlKey) {
const newSymbol = getSymbolSubstitute(event.code, event.shiftKey); const newSymbol = getSymbolSubstitute(event.code, event.shiftKey);
if (newSymbol) { if (newSymbol) {
text.replaceWith(newSymbol); text.replaceWith(newSymbol);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
} else if (event.ctrlKey && event.code === 'KeyQ' && onAnalyze) { } else if (event.ctrlKey && event.code === 'KeyQ' && onAnalyze) {
onAnalyze(); onAnalyze();
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
}, [thisRef, onAnalyze]); },
[thisRef, onAnalyze]
);
return ( return (
<div <div className={clsx('flex flex-col gap-2', className, cursor)} style={style}>
className={clsx( <Label text={label} htmlFor={id} />
'flex flex-col gap-2', <CodeMirror
className, id={id}
cursor ref={thisRef}
)} basicSetup={editorSetup}
style={style} theme={customTheme}
> extensions={editorExtensions}
<Label text={label} htmlFor={id}/> indentWithTab={false}
<CodeMirror id={id} onChange={onChange}
ref={thisRef} editable={!disabled}
basicSetup={editorSetup} onKeyDown={handleInput}
theme={customTheme} {...restProps}
extensions={editorExtensions} />
indentWithTab={false} </div>
onChange={onChange} );
editable={!disabled} }
onKeyDown={handleInput} );
{...restProps}
/>
</div>);
});
export default RSInput; export default RSInput;

View File

@ -3,8 +3,8 @@ import { Decoration, EditorView } from '@codemirror/view';
import { bracketsDarkT, bracketsLightT } from '@/utils/color'; import { bracketsDarkT, bracketsLightT } from '@/utils/color';
const matchingMark = Decoration.mark({class: 'cc-matchingBracket'}); const matchingMark = Decoration.mark({ class: 'cc-matchingBracket' });
const nonMatchingMark = Decoration.mark({class: 'cc-nonmatchingBracket'}); const nonMatchingMark = Decoration.mark({ class: 'cc-nonmatchingBracket' });
function bracketRender(match: MatchResult) { function bracketRender(match: MatchResult) {
const decorations = []; const decorations = [];
@ -22,10 +22,10 @@ const lightTheme = EditorView.baseTheme(bracketsLightT);
export function ccBracketMatching(darkMode: boolean) { export function ccBracketMatching(darkMode: boolean) {
return [ return [
bracketMatching({ bracketMatching({
renderMatch: bracketRender, renderMatch: bracketRender,
brackets:'{}[]()' brackets: '{}[]()'
}), }),
darkMode ? darkTheme : lightTheme darkMode ? darkTheme : lightTheme
]; ];
} }

View File

@ -1 +1 @@
export { default } from './RSInput'; export { default } from './RSInput';

View File

@ -1,23 +1,23 @@
import {styleTags, tags} from '@lezer/highlight'; import { styleTags, tags } from '@lezer/highlight';
export const highlighting = styleTags({ export const highlighting = styleTags({
Index: tags.unit, 'Index': tags.unit,
ComplexIndex: tags.unit, 'ComplexIndex': tags.unit,
Literal: tags.literal, 'Literal': tags.literal,
Radical: tags.propertyName,
Function: tags.name,
Predicate: tags.name,
Global: tags.name,
Local: tags.variableName,
TextFunction: tags.keyword, 'Radical': tags.propertyName,
Filter: tags.keyword, 'Function': tags.name,
PrefixR: tags.controlKeyword, 'Predicate': tags.name,
PrefixI: tags.controlKeyword, 'Global': tags.name,
PrefixD: tags.controlKeyword, 'Local': tags.variableName,
"{": tags.brace,
"}": tags.brace, 'TextFunction': tags.keyword,
"|": tags.brace, 'Filter': tags.keyword,
";": tags.brace, 'PrefixR': tags.controlKeyword,
}); 'PrefixI': tags.controlKeyword,
'PrefixD': tags.controlKeyword,
'{': tags.brace,
'}': tags.brace,
'|': tags.brace,
';': tags.brace
});

View File

@ -1,13 +1,11 @@
import {LRLanguage} from '@codemirror/language'; import { LRLanguage } from '@codemirror/language';
import { parser } from './parser'; import { parser } from './parser';
import { Function, Global, Predicate } from './parser.terms'; import { Function, Global, Predicate } from './parser.terms';
export const GlobalTokens: number[] = [ export const GlobalTokens: number[] = [Global, Function, Predicate];
Global, Function, Predicate
]
export const RSLanguage = LRLanguage.define({ export const RSLanguage = LRLanguage.define({
parser: parser, parser: parser,
languageData: {} languageData: {}
}); });

View File

@ -6,6 +6,7 @@ import { TokenID } from '@/models/rslang';
import { CodeMirrorWrapper } from '@/utils/codemirror'; import { CodeMirrorWrapper } from '@/utils/codemirror';
export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): string | undefined { export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): string | undefined {
// prettier-ignore
if (shiftPressed) { if (shiftPressed) {
switch (keyCode) { switch (keyCode) {
case 'Backquote': return '∃'; case 'Backquote': return '∃';
@ -59,7 +60,7 @@ export function getSymbolSubstitute(keyCode: string, shiftPressed: boolean): str
/** /**
* Wrapper class for RSLang editor. * Wrapper class for RSLang editor.
*/ */
export class RSTextWrapper extends CodeMirrorWrapper { export class RSTextWrapper extends CodeMirrorWrapper {
constructor(object: Required<ReactCodeMirrorRef>) { constructor(object: Required<ReactCodeMirrorRef>) {
super(object); super(object);
@ -67,151 +68,218 @@ export class RSTextWrapper extends CodeMirrorWrapper {
insertToken(tokenID: TokenID): boolean { insertToken(tokenID: TokenID): boolean {
const selection = this.getSelection(); const selection = this.getSelection();
const hasSelection = selection.from !== selection.to const hasSelection = selection.from !== selection.to;
switch (tokenID) { switch (tokenID) {
case TokenID.NT_DECLARATIVE_EXPR: { case TokenID.NT_DECLARATIVE_EXPR: {
if (hasSelection) { if (hasSelection) {
this.envelopeWith('D{ξ∈X1 | ', '}'); this.envelopeWith('D{ξ∈X1 | ', '}');
} else { } else {
this.envelopeWith('D{ξ∈X1 | P1[ξ]', '}'); this.envelopeWith('D{ξ∈X1 | P1[ξ]', '}');
}
this.ref.view.dispatch({
selection: {
anchor: selection.from + 2,
} }
});
return true;
}
case TokenID.NT_IMPERATIVE_EXPR: {
if (hasSelection) {
this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; ', '}');
} else {
this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; P1[σ, γ]', '}');
}
return true;
}
case TokenID.NT_RECURSIVE_FULL: {
if (hasSelection) {
this.envelopeWith('R{ξ:=D1 | F1[ξ]≠∅ | ', '}');
} else {
this.envelopeWith('R{ξ:=D1 | F1[ξ]≠∅ | ξF1[ξ]', '}');
}
return true;
}
case TokenID.BIGPR: this.envelopeWith('Pr1(', ')'); return true;
case TokenID.SMALLPR: this.envelopeWith('pr1(', ')'); return true;
case TokenID.FILTER: this.envelopeWith('Fi1[α](', ')'); return true;
case TokenID.REDUCE: this.envelopeWith('red(', ')'); return true;
case TokenID.CARD: this.envelopeWith('card(', ')'); return true;
case TokenID.BOOL: this.envelopeWith('bool(', ')'); return true;
case TokenID.DEBOOL: this.envelopeWith('debool(', ')'); return true;
case TokenID.PUNCTUATION_PL: {
this.envelopeWith('(', ')');
this.ref.view.dispatch({
selection: {
anchor: hasSelection ? selection.to: selection.from + 1,
}
});
return true;
}
case TokenID.PUNCTUATION_SL: {
this.envelopeWith('[', ']');
if (hasSelection) {
this.ref.view.dispatch({ this.ref.view.dispatch({
selection: { selection: {
anchor: hasSelection ? selection.to: selection.from + 1, anchor: selection.from + 2
} }
}); });
return true;
} }
return true; case TokenID.NT_IMPERATIVE_EXPR: {
} if (hasSelection) {
case TokenID.BOOLEAN: { this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; ', '}');
const selStart = selection.from; } else {
if (hasSelection && this.ref.view.state.sliceDoc(selStart, selStart + 1) === '') { this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; P1[σ, γ]', '}');
this.envelopeWith('', ''); }
} else { return true;
this.envelopeWith('(', ')');
} }
return true; case TokenID.NT_RECURSIVE_FULL: {
} if (hasSelection) {
this.envelopeWith('R{ξ:=D1 | F1[ξ]≠∅ | ', '}');
} else {
this.envelopeWith('R{ξ:=D1 | F1[ξ]≠∅ | ξF1[ξ]', '}');
}
return true;
}
case TokenID.BIGPR:
this.envelopeWith('Pr1(', ')');
return true;
case TokenID.SMALLPR:
this.envelopeWith('pr1(', ')');
return true;
case TokenID.FILTER:
this.envelopeWith('Fi1[α](', ')');
return true;
case TokenID.REDUCE:
this.envelopeWith('red(', ')');
return true;
case TokenID.CARD:
this.envelopeWith('card(', ')');
return true;
case TokenID.BOOL:
this.envelopeWith('bool(', ')');
return true;
case TokenID.DEBOOL:
this.envelopeWith('debool(', ')');
return true;
case TokenID.DECART: this.replaceWith('×'); return true; case TokenID.PUNCTUATION_PL: {
case TokenID.QUANTOR_UNIVERSAL: this.replaceWith('∀'); return true; this.envelopeWith('(', ')');
case TokenID.QUANTOR_EXISTS: this.replaceWith('∃'); return true; this.ref.view.dispatch({
case TokenID.SET_IN: this.replaceWith('∈'); return true; selection: {
case TokenID.SET_NOT_IN: this.replaceWith('∉'); return true; anchor: hasSelection ? selection.to : selection.from + 1
case TokenID.LOGIC_OR: this.replaceWith(''); return true; }
case TokenID.LOGIC_AND: this.replaceWith('&'); return true; });
case TokenID.SUBSET_OR_EQ: this.replaceWith('⊆'); return true; return true;
case TokenID.LOGIC_IMPLICATION: this.replaceWith('⇒'); return true; }
case TokenID.SET_INTERSECTION: this.replaceWith('∩'); return true; case TokenID.PUNCTUATION_SL: {
case TokenID.SET_UNION: this.replaceWith(''); return true; this.envelopeWith('[', ']');
case TokenID.SET_MINUS: this.replaceWith('\\'); return true; if (hasSelection) {
case TokenID.SET_SYMMETRIC_MINUS: this.replaceWith('∆'); return true; this.ref.view.dispatch({
case TokenID.LIT_EMPTYSET: this.replaceWith('∅'); return true; selection: {
case TokenID.LIT_WHOLE_NUMBERS: this.replaceWith('Z'); return true; anchor: hasSelection ? selection.to : selection.from + 1
case TokenID.SUBSET: this.replaceWith('⊂'); return true; }
case TokenID.NOT_SUBSET: this.replaceWith('⊄'); return true; });
case TokenID.EQUAL: this.replaceWith('='); return true; }
case TokenID.NOTEQUAL: this.replaceWith('≠'); return true; return true;
case TokenID.LOGIC_NOT: this.replaceWith('¬'); return true; }
case TokenID.LOGIC_EQUIVALENT: this.replaceWith('⇔'); return true; case TokenID.BOOLEAN: {
case TokenID.GREATER_OR_EQ: this.replaceWith('≥'); return true; const selStart = selection.from;
case TokenID.LESSER_OR_EQ: this.replaceWith('≤'); return true; if (hasSelection && this.ref.view.state.sliceDoc(selStart, selStart + 1) === '') {
case TokenID.PUNCTUATION_ASSIGN: this.replaceWith(':='); return true; this.envelopeWith('', '');
case TokenID.PUNCTUATION_ITERATE: this.replaceWith(':∈'); return true; } else {
case TokenID.MULTIPLY: this.replaceWith('*'); return true; this.envelopeWith('(', ')');
}
return true;
}
case TokenID.DECART:
this.replaceWith('×');
return true;
case TokenID.QUANTOR_UNIVERSAL:
this.replaceWith('∀');
return true;
case TokenID.QUANTOR_EXISTS:
this.replaceWith('∃');
return true;
case TokenID.SET_IN:
this.replaceWith('∈');
return true;
case TokenID.SET_NOT_IN:
this.replaceWith('∉');
return true;
case TokenID.LOGIC_OR:
this.replaceWith('');
return true;
case TokenID.LOGIC_AND:
this.replaceWith('&');
return true;
case TokenID.SUBSET_OR_EQ:
this.replaceWith('⊆');
return true;
case TokenID.LOGIC_IMPLICATION:
this.replaceWith('⇒');
return true;
case TokenID.SET_INTERSECTION:
this.replaceWith('∩');
return true;
case TokenID.SET_UNION:
this.replaceWith('');
return true;
case TokenID.SET_MINUS:
this.replaceWith('\\');
return true;
case TokenID.SET_SYMMETRIC_MINUS:
this.replaceWith('∆');
return true;
case TokenID.LIT_EMPTYSET:
this.replaceWith('∅');
return true;
case TokenID.LIT_WHOLE_NUMBERS:
this.replaceWith('Z');
return true;
case TokenID.SUBSET:
this.replaceWith('⊂');
return true;
case TokenID.NOT_SUBSET:
this.replaceWith('⊄');
return true;
case TokenID.EQUAL:
this.replaceWith('=');
return true;
case TokenID.NOTEQUAL:
this.replaceWith('≠');
return true;
case TokenID.LOGIC_NOT:
this.replaceWith('¬');
return true;
case TokenID.LOGIC_EQUIVALENT:
this.replaceWith('⇔');
return true;
case TokenID.GREATER_OR_EQ:
this.replaceWith('≥');
return true;
case TokenID.LESSER_OR_EQ:
this.replaceWith('≤');
return true;
case TokenID.PUNCTUATION_ASSIGN:
this.replaceWith(':=');
return true;
case TokenID.PUNCTUATION_ITERATE:
this.replaceWith(':∈');
return true;
case TokenID.MULTIPLY:
this.replaceWith('*');
return true;
} }
return false; return false;
} }
processAltKey(keyCode: string, shiftPressed: boolean): boolean { processAltKey(keyCode: string, shiftPressed: boolean): boolean {
// prettier-ignore
if (shiftPressed) { if (shiftPressed) {
switch (keyCode) { switch (keyCode) {
case 'KeyE': return this.insertToken(TokenID.DECART); case 'KeyE': return this.insertToken(TokenID.DECART);
case 'Backquote': return this.insertToken(TokenID.NOTEQUAL); case 'Backquote': return this.insertToken(TokenID.NOTEQUAL);
case 'Digit1': return this.insertToken(TokenID.SET_NOT_IN); // ! case 'Digit1': return this.insertToken(TokenID.SET_NOT_IN); // !
case 'Digit2': return this.insertToken(TokenID.NOT_SUBSET); // @ case 'Digit2': return this.insertToken(TokenID.NOT_SUBSET); // @
case 'Digit3': return this.insertToken(TokenID.LOGIC_OR); // # case 'Digit3': return this.insertToken(TokenID.LOGIC_OR); // #
case 'Digit4': return this.insertToken(TokenID.LOGIC_EQUIVALENT); // $ case 'Digit4': return this.insertToken(TokenID.LOGIC_EQUIVALENT); // $
case 'Digit5': return this.insertToken(TokenID.SET_SYMMETRIC_MINUS); // % case 'Digit5': return this.insertToken(TokenID.SET_SYMMETRIC_MINUS); // %
case 'Digit6': return this.insertToken(TokenID.PUNCTUATION_ASSIGN); // ^ case 'Digit6': return this.insertToken(TokenID.PUNCTUATION_ASSIGN); // ^
case 'Digit7': return this.insertToken(TokenID.GREATER_OR_EQ); // & case 'Digit7': return this.insertToken(TokenID.GREATER_OR_EQ); // &
case 'Digit8': return this.insertToken(TokenID.LESSER_OR_EQ); // * case 'Digit8': return this.insertToken(TokenID.LESSER_OR_EQ); // *
case 'Digit9': return this.insertToken(TokenID.PUNCTUATION_PL); // ( case 'Digit9': return this.insertToken(TokenID.PUNCTUATION_PL); // (
} }
} else { } else {
switch (keyCode) { switch (keyCode) {
case 'KeyQ': return this.insertToken(TokenID.BIGPR); case 'KeyQ': return this.insertToken(TokenID.BIGPR);
case 'KeyW': return this.insertToken(TokenID.SMALLPR); case 'KeyW': return this.insertToken(TokenID.SMALLPR);
case 'KeyE': return this.insertToken(TokenID.BOOLEAN); case 'KeyE': return this.insertToken(TokenID.BOOLEAN);
case 'KeyR': return this.insertToken(TokenID.REDUCE); case 'KeyR': return this.insertToken(TokenID.REDUCE);
case 'KeyT': return this.insertToken(TokenID.NT_RECURSIVE_FULL); case 'KeyT': return this.insertToken(TokenID.NT_RECURSIVE_FULL);
case 'KeyA': return this.insertToken(TokenID.SET_INTERSECTION); case 'KeyA': return this.insertToken(TokenID.SET_INTERSECTION);
case 'KeyS': return this.insertToken(TokenID.SET_UNION); case 'KeyS': return this.insertToken(TokenID.SET_UNION);
case 'KeyD': return this.insertToken(TokenID.NT_DECLARATIVE_EXPR); case 'KeyD': return this.insertToken(TokenID.NT_DECLARATIVE_EXPR);
case 'KeyF': return this.insertToken(TokenID.FILTER); case 'KeyF': return this.insertToken(TokenID.FILTER);
case 'KeyG': return this.insertToken(TokenID.NT_IMPERATIVE_EXPR); case 'KeyG': return this.insertToken(TokenID.NT_IMPERATIVE_EXPR);
case 'KeyZ': return this.insertToken(TokenID.LIT_WHOLE_NUMBERS); case 'KeyZ': return this.insertToken(TokenID.LIT_WHOLE_NUMBERS);
case 'KeyX': return this.insertToken(TokenID.LIT_EMPTYSET); case 'KeyX': return this.insertToken(TokenID.LIT_EMPTYSET);
case 'KeyC': return this.insertToken(TokenID.CARD); case 'KeyC': return this.insertToken(TokenID.CARD);
case 'KeyV': return this.insertToken(TokenID.DEBOOL); case 'KeyV': return this.insertToken(TokenID.DEBOOL);
case 'KeyB': return this.insertToken(TokenID.BOOL); case 'KeyB': return this.insertToken(TokenID.BOOL);
case 'Backquote': return this.insertToken(TokenID.LOGIC_NOT); case 'Backquote': return this.insertToken(TokenID.LOGIC_NOT);
case 'Digit1': return this.insertToken(TokenID.SET_IN); case 'Digit1': return this.insertToken(TokenID.SET_IN);
case 'Digit2': return this.insertToken(TokenID.SUBSET_OR_EQ); case 'Digit2': return this.insertToken(TokenID.SUBSET_OR_EQ);
case 'Digit3': return this.insertToken(TokenID.LOGIC_AND); case 'Digit3': return this.insertToken(TokenID.LOGIC_AND);
case 'Digit4': return this.insertToken(TokenID.LOGIC_IMPLICATION); case 'Digit4': return this.insertToken(TokenID.LOGIC_IMPLICATION);
case 'Digit5': return this.insertToken(TokenID.SET_MINUS); case 'Digit5': return this.insertToken(TokenID.SET_MINUS);
case 'Digit6': return this.insertToken(TokenID.PUNCTUATION_ITERATE); case 'Digit6': return this.insertToken(TokenID.PUNCTUATION_ITERATE);
case 'Digit7': return this.insertToken(TokenID.SUBSET); case 'Digit7': return this.insertToken(TokenID.SUBSET);
case 'Digit8': return this.insertToken(TokenID.MULTIPLY); case 'Digit8': return this.insertToken(TokenID.MULTIPLY);
case 'BracketLeft': return this.insertToken(TokenID.PUNCTUATION_SL); case 'BracketLeft': return this.insertToken(TokenID.PUNCTUATION_SL);
} }
} }
return false; return false;
} }
} }

View File

@ -1,4 +1,4 @@
import { syntaxTree } from "@codemirror/language" import { syntaxTree } from '@codemirror/language';
import { Extension } from '@codemirror/state'; import { Extension } from '@codemirror/state';
import { hoverTooltip } from '@codemirror/view'; import { hoverTooltip } from '@codemirror/view';
import { EditorState } from '@uiw/react-codemirror'; import { EditorState } from '@uiw/react-codemirror';
@ -15,15 +15,14 @@ function findAliasAt(pos: number, state: EditorState) {
let alias = ''; let alias = '';
let start = 0; let start = 0;
let end = 0; let end = 0;
nodes.forEach( nodes.forEach(node => {
node => {
if (node.to <= lineEnd && node.from >= lineStart) { if (node.to <= lineEnd && node.from >= lineStart) {
alias = text.slice(node.from - lineStart, node.to - lineStart); alias = text.slice(node.from - lineStart, node.to - lineStart);
start = node.from; start = node.from;
end = node.to; end = node.to;
} }
}); });
return {alias, start, end}; return { alias, start, end };
} }
const globalsHoverTooltip = (items: IConstituenta[]) => { const globalsHoverTooltip = (items: IConstituenta[]) => {
@ -38,10 +37,10 @@ const globalsHoverTooltip = (items: IConstituenta[]) => {
end: end, end: end,
above: false, above: false,
create: () => domTooltipConstituenta(cst) create: () => domTooltipConstituenta(cst)
} };
}); });
} };
export function rsHoverTooltip(items: IConstituenta[]): Extension { export function rsHoverTooltip(items: IConstituenta[]): Extension {
return [globalsHoverTooltip(items)]; return [globalsHoverTooltip(items)];
} }

View File

@ -49,158 +49,159 @@ const editorSetup: BasicSetupOptions = {
lintKeymap: false lintKeymap: false
}; };
interface RefsInputInputProps interface RefsInputInputProps
extends Pick<ReactCodeMirrorProps, extends Pick<ReactCodeMirrorProps, 'id' | 'height' | 'value' | 'className' | 'onFocus' | 'onBlur' | 'placeholder'> {
'id'| 'height' | 'value' | 'className' | 'onFocus' | 'onBlur' | 'placeholder' label?: string;
> { onChange?: (newValue: string) => void;
label?: string items?: IConstituenta[];
onChange?: (newValue: string) => void disabled?: boolean;
items?: IConstituenta[]
disabled?: boolean initialValue?: string;
value?: string;
initialValue?: string resolved?: string;
value?: string
resolved?: string
} }
const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>( const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
({ ({ id, label, disabled, items, initialValue, value, resolved, onFocus, onBlur, onChange, ...restProps }, ref) => {
id, label, disabled, items, const { darkMode, colors } = useConceptTheme();
initialValue, value, resolved, const { schema } = useRSForm();
onFocus, onBlur, onChange,
...restProps
}, ref) => {
const { darkMode, colors } = useConceptTheme();
const { schema } = useRSForm();
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
const [showEditor, setShowEditor] = useState(false); const [showEditor, setShowEditor] = useState(false);
const [currentType, setCurrentType] = useState<ReferenceType>(ReferenceType.ENTITY); const [currentType, setCurrentType] = useState<ReferenceType>(ReferenceType.ENTITY);
const [refText, setRefText] = useState(''); const [refText, setRefText] = useState('');
const [hintText, setHintText] = useState(''); const [hintText, setHintText] = useState('');
const [basePosition, setBasePosition] = useState(0); const [basePosition, setBasePosition] = useState(0);
const [mainRefs, setMainRefs] = useState<string[]>([]); const [mainRefs, setMainRefs] = useState<string[]>([]);
const internalRef = useRef<ReactCodeMirrorRef>(null); const internalRef = useRef<ReactCodeMirrorRef>(null);
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]); const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
const cursor = useMemo(() => !disabled ? 'cursor-text': 'cursor-default', [disabled]); const cursor = useMemo(() => (!disabled ? 'cursor-text' : 'cursor-default'), [disabled]);
const customTheme: Extension = useMemo( const customTheme: Extension = useMemo(
() => createTheme({ () =>
theme: darkMode ? 'dark' : 'light', createTheme({
settings: { theme: darkMode ? 'dark' : 'light',
fontFamily: 'inherit', settings: {
background: !disabled ? colors.bgInput : colors.bgDefault, fontFamily: 'inherit',
foreground: colors.fgDefault, background: !disabled ? colors.bgInput : colors.bgDefault,
selection: colors.bgHover foreground: colors.fgDefault,
}, selection: colors.bgHover
styles: [ },
{ tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // EntityReference styles: [
{ tag: tags.literal, color: colors.fgTeal, cursor: 'default' }, // SyntacticReference { tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // EntityReference
{ tag: tags.comment, color: colors.fgRed }, // Error { tag: tags.literal, color: colors.fgTeal, cursor: 'default' }, // SyntacticReference
] { tag: tags.comment, color: colors.fgRed } // Error
}), [disabled, colors, darkMode]); ]
}),
[disabled, colors, darkMode]
);
const editorExtensions = useMemo( const editorExtensions = useMemo(
() => [ () => [EditorView.lineWrapping, NaturalLanguage, refsHoverTooltip(schema?.items || [], colors)],
EditorView.lineWrapping, [schema?.items, colors]
NaturalLanguage, );
refsHoverTooltip(schema?.items || [], colors)
], [schema?.items, colors]);
function handleChange(newValue: string) { function handleChange(newValue: string) {
if (onChange) onChange(newValue); if (onChange) onChange(newValue);
}
function handleFocusIn(event: React.FocusEvent<HTMLDivElement>) {
setIsFocused(true);
if (onFocus) onFocus(event);
}
function handleFocusOut(event: React.FocusEvent<HTMLDivElement>) {
setIsFocused(false);
if (onBlur) onBlur(event);
}
const handleInput = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (!thisRef.current?.view) {
event.preventDefault();
return;
} }
if (event.ctrlKey && event.code === 'Space') {
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
wrap.fixSelection(ReferenceTokens);
const nodes = wrap.getEnvelopingNodes(ReferenceTokens);
if (nodes.length !== 1) {
setCurrentType(ReferenceType.ENTITY);
setRefText('');
setHintText(wrap.getSelectionText());
} else {
setCurrentType(nodes[0].type.id === RefEntity ? ReferenceType.ENTITY : ReferenceType.SYNTACTIC);
setRefText(wrap.getSelectionText());
}
const selection = wrap.getSelection(); function handleFocusIn(event: React.FocusEvent<HTMLDivElement>) {
const mainNodes = wrap.getAllNodes([RefEntity]).filter(node => node.from >= selection.to || node.to <= selection.from); setIsFocused(true);
setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to))); if (onFocus) onFocus(event);
setBasePosition(mainNodes.filter(node => node.to <= selection.from).length);
setShowEditor(true);
} }
}, [thisRef]);
const handleInputReference = useCallback( function handleFocusOut(event: React.FocusEvent<HTMLDivElement>) {
(referenceText: string) => { setIsFocused(false);
if (!thisRef.current?.view) { if (onBlur) onBlur(event);
return;
} }
thisRef.current.view.focus();
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
wrap.replaceWith(referenceText);
}, [thisRef]);
return (<> const handleInput = useCallback(
<AnimatePresence> (event: React.KeyboardEvent<HTMLDivElement>) => {
{showEditor ? if (!thisRef.current?.view) {
<DlgEditReference event.preventDefault();
hideWindow={() => setShowEditor(false)} return;
items={items ?? []} }
initial={{ if (event.ctrlKey && event.code === 'Space') {
type: currentType, const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
refRaw: refText, wrap.fixSelection(ReferenceTokens);
text: hintText, const nodes = wrap.getEnvelopingNodes(ReferenceTokens);
basePosition: basePosition, if (nodes.length !== 1) {
mainRefs: mainRefs setCurrentType(ReferenceType.ENTITY);
}} setRefText('');
onSave={handleInputReference} setHintText(wrap.getSelectionText());
/> : null} } else {
</AnimatePresence> setCurrentType(nodes[0].type.id === RefEntity ? ReferenceType.ENTITY : ReferenceType.SYNTACTIC);
setRefText(wrap.getSelectionText());
<div className={clsx( }
'flex flex-col gap-2',
cursor
)}>
<Label text={label} htmlFor={id} />
<CodeMirror id={id} ref={thisRef}
basicSetup={editorSetup}
theme={customTheme}
extensions={editorExtensions}
value={isFocused ? value : (value !== initialValue || showEditor ? value : resolved)} const selection = wrap.getSelection();
const mainNodes = wrap
.getAllNodes([RefEntity])
.filter(node => node.from >= selection.to || node.to <= selection.from);
setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to)));
setBasePosition(mainNodes.filter(node => node.to <= selection.from).length);
indentWithTab={false} setShowEditor(true);
onChange={handleChange} }
editable={!disabled} },
onKeyDown={handleInput} [thisRef]
onFocus={handleFocusIn} );
onBlur={handleFocusOut}
// spellCheck= // TODO: figure out while automatic spellcheck doesnt work or implement with extension
{...restProps}
/>
</div>
</>);
});
export default RefsInput; const handleInputReference = useCallback(
(referenceText: string) => {
if (!thisRef.current?.view) {
return;
}
thisRef.current.view.focus();
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
wrap.replaceWith(referenceText);
},
[thisRef]
);
return (
<>
<AnimatePresence>
{showEditor ? (
<DlgEditReference
hideWindow={() => setShowEditor(false)}
items={items ?? []}
initial={{
type: currentType,
refRaw: refText,
text: hintText,
basePosition: basePosition,
mainRefs: mainRefs
}}
onSave={handleInputReference}
/>
) : null}
</AnimatePresence>
<div className={clsx('flex flex-col gap-2', cursor)}>
<Label text={label} htmlFor={id} />
<CodeMirror
id={id}
ref={thisRef}
basicSetup={editorSetup}
theme={customTheme}
extensions={editorExtensions}
value={isFocused ? value : value !== initialValue || showEditor ? value : resolved}
indentWithTab={false}
onChange={handleChange}
editable={!disabled}
onKeyDown={handleInput}
onFocus={handleFocusIn}
onBlur={handleFocusOut}
// spellCheck= // TODO: figure out while automatic spellcheck doesn't work or implement with extension
{...restProps}
/>
</div>
</>
);
}
);
export default RefsInput;

View File

@ -1 +1 @@
export { default } from './RefsInput'; export { default } from './RefsInput';

View File

@ -1,4 +1,4 @@
import {styleTags, tags} from '@lezer/highlight'; import { styleTags, tags } from '@lezer/highlight';
export const highlighting = styleTags({ export const highlighting = styleTags({
RefEntity: tags.name, RefEntity: tags.name,
@ -10,4 +10,4 @@ export const highlighting = styleTags({
Nominal: tags.literal, Nominal: tags.literal,
Error: tags.comment Error: tags.comment
}); });

View File

@ -1,13 +1,11 @@
import {LRLanguage} from '@codemirror/language'; import { LRLanguage } from '@codemirror/language';
import { parser } from './parser'; import { parser } from './parser';
import { RefEntity, RefSyntactic } from './parser.terms'; import { RefEntity, RefSyntactic } from './parser.terms';
export const ReferenceTokens: number[] = [ export const ReferenceTokens: number[] = [RefSyntactic, RefEntity];
RefSyntactic, RefEntity
]
export const NaturalLanguage = LRLanguage.define({ export const NaturalLanguage = LRLanguage.define({
parser: parser, parser: parser,
languageData: {} languageData: {}
}); });

View File

@ -13,12 +13,11 @@ const testData = [
['@{-1| черный }', '[Text[RefSyntactic[Offset][Nominal]]]'], ['@{-1| черный }', '[Text[RefSyntactic[Offset][Nominal]]]'],
['@{-100| черный слон }', '[Text[RefSyntactic[Offset][Nominal]]]'], ['@{-100| черный слон }', '[Text[RefSyntactic[Offset][Nominal]]]'],
['@{X1|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]'], ['@{X1|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]'],
['@{X12|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]'], ['@{X12|VERB,past,sing}', '[Text[RefEntity[Global][Grams]]]']
]; ];
describe('Testing NaturalParser', () => { describe('Testing NaturalParser', () => {
it.each(testData)('Parse %p', it.each(testData)('Parse %p', (input: string, expectedTree: string) => {
(input: string, expectedTree: string) => {
// NOTE: use strict parser to determine exact error position // NOTE: use strict parser to determine exact error position
// const tree = parser.configure({strict: true}).parse(input); // const tree = parser.configure({strict: true}).parse(input);
const tree = parser.parse(input); const tree = parser.parse(input);

View File

@ -1,18 +1,22 @@
import { syntaxTree } from '@codemirror/language' import { syntaxTree } from '@codemirror/language';
import { Extension } from '@codemirror/state'; import { Extension } from '@codemirror/state';
import { hoverTooltip } from '@codemirror/view'; import { hoverTooltip } from '@codemirror/view';
import { parseEntityReference, parseSyntacticReference } from '@/models/languageAPI'; import { parseEntityReference, parseSyntacticReference } from '@/models/languageAPI';
import { IConstituenta } from '@/models/rsform'; import { IConstituenta } from '@/models/rsform';
import { domTooltipEntityReference, domTooltipSyntacticReference, findContainedNodes, findEnvelopingNodes } from '@/utils/codemirror'; import {
domTooltipEntityReference,
domTooltipSyntacticReference,
findContainedNodes,
findEnvelopingNodes
} from '@/utils/codemirror';
import { IColorTheme } from '@/utils/color'; import { IColorTheme } from '@/utils/color';
import { ReferenceTokens } from './parse'; import { ReferenceTokens } from './parse';
import { RefEntity, RefSyntactic } from './parse/parser.terms'; import { RefEntity, RefSyntactic } from './parse/parser.terms';
export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme) => { export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme) => {
return hoverTooltip( return hoverTooltip((view, pos) => {
(view, pos) => {
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(view.state), ReferenceTokens); const nodes = findEnvelopingNodes(pos, pos, syntaxTree(view.state), ReferenceTokens);
if (nodes.length !== 1) { if (nodes.length !== 1) {
return null; return null;
@ -28,7 +32,7 @@ export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme)
end: end, end: end,
above: false, above: false,
create: () => domTooltipEntityReference(ref, cst, colors) create: () => domTooltipEntityReference(ref, cst, colors)
} };
} else if (nodes[0].type.id === RefSyntactic) { } else if (nodes[0].type.id === RefSyntactic) {
const ref = parseSyntacticReference(text); const ref = parseSyntacticReference(text);
let masterText: string | undefined = undefined; let masterText: string | undefined = undefined;
@ -50,13 +54,13 @@ export const globalsHoverTooltip = (items: IConstituenta[], colors: IColorTheme)
end: end, end: end,
above: false, above: false,
create: () => domTooltipSyntacticReference(ref, masterText) create: () => domTooltipSyntacticReference(ref, masterText)
} };
} else { } else {
return null; return null;
} }
}); });
} };
export function refsHoverTooltip(items: IConstituenta[], colors: IColorTheme): Extension { export function refsHoverTooltip(items: IConstituenta[], colors: IColorTheme): Extension {
return [globalsHoverTooltip(items, colors)]; return [globalsHoverTooltip(items, colors)];
} }

View File

@ -5,7 +5,7 @@ import { useAuth } from '@/context/AuthContext';
import TextURL from './Common/TextURL'; import TextURL from './Common/TextURL';
interface RequireAuthProps { interface RequireAuthProps {
children: React.ReactNode children: React.ReactNode;
} }
function RequireAuth({ children }: RequireAuthProps) { function RequireAuth({ children }: RequireAuthProps) {
@ -15,13 +15,14 @@ function RequireAuth({ children }: RequireAuthProps) {
return children; return children;
} else { } else {
return ( return (
<div className='flex flex-col items-center gap-1 mt-2'> <div className='flex flex-col items-center gap-1 mt-2'>
<p className='mb-2'>Пожалуйста войдите в систему</p> <p className='mb-2'>Пожалуйста войдите в систему</p>
<TextURL text='Войти в Портал' href='/login'/> <TextURL text='Войти в Портал' href='/login' />
<TextURL text='Зарегистрироваться' href='/signup'/> <TextURL text='Зарегистрироваться' href='/signup' />
<TextURL text='Начальная страница' href='/'/> <TextURL text='Начальная страница' href='/' />
</div>); </div>
);
} }
} }
export default RequireAuth; export default RequireAuth;

View File

@ -4,46 +4,43 @@ import ConceptTooltip from '@/components/Common/ConceptTooltip';
import ConstituentaTooltip from '@/components/Help/ConstituentaTooltip'; import ConstituentaTooltip from '@/components/Help/ConstituentaTooltip';
import { IConstituenta } from '@/models/rsform'; import { IConstituenta } from '@/models/rsform';
import { isMockCst } from '@/models/rsformAPI'; import { isMockCst } from '@/models/rsformAPI';
import { colorFgCstStatus,IColorTheme } from '@/utils/color'; import { colorFgCstStatus, IColorTheme } from '@/utils/color';
import { describeExpressionStatus } from '@/utils/labels'; import { describeExpressionStatus } from '@/utils/labels';
interface ConstituentaBadgeProps { interface ConstituentaBadgeProps {
prefixID?: string prefixID?: string;
shortTooltip?: boolean shortTooltip?: boolean;
value: IConstituenta value: IConstituenta;
theme: IColorTheme theme: IColorTheme;
} }
function ConstituentaBadge({ value, prefixID, shortTooltip, theme }: ConstituentaBadgeProps) { function ConstituentaBadge({ value, prefixID, shortTooltip, theme }: ConstituentaBadgeProps) {
return ( return (
<div <div
id={`${prefixID}${value.alias}`} id={`${prefixID}${value.alias}`}
className={clsx( className={clsx(
'min-w-[3.1rem] max-w-[3.1rem]', 'min-w-[3.1rem] max-w-[3.1rem]',
'px-1', 'px-1',
'border rounded-md', 'border rounded-md',
'text-center font-semibold whitespace-nowrap' 'text-center font-semibold whitespace-nowrap'
)} )}
style={{ style={{
borderColor: colorFgCstStatus(value.status, theme), borderColor: colorFgCstStatus(value.status, theme),
color: colorFgCstStatus(value.status, theme), color: colorFgCstStatus(value.status, theme),
backgroundColor: isMockCst(value) ? theme.bgWarning : theme.bgInput backgroundColor: isMockCst(value) ? theme.bgWarning : theme.bgInput
}} }}
>
{value.alias}
{!shortTooltip ?
<ConstituentaTooltip
anchor={`#${prefixID}${value.alias}`}
data={value}
/> : null}
{shortTooltip ?
<ConceptTooltip
anchorSelect={`#${prefixID}${value.alias}`}
place='right'
> >
<p><b>Статус</b>: {describeExpressionStatus(value.status)}</p> {value.alias}
</ConceptTooltip> : null} {!shortTooltip ? <ConstituentaTooltip anchor={`#${prefixID}${value.alias}`} data={value} /> : null}
</div>); {shortTooltip ? (
<ConceptTooltip anchorSelect={`#${prefixID}${value.alias}`} place='right'>
<p>
<b>Статус</b>: {describeExpressionStatus(value.status)}
</p>
</ConceptTooltip>
) : null}
</div>
);
} }
export default ConstituentaBadge; export default ConstituentaBadge;

View File

@ -12,35 +12,35 @@ import { describeConstituenta } from '@/utils/labels';
import ConstituentaBadge from './ConstituentaBadge'; import ConstituentaBadge from './ConstituentaBadge';
interface ConstituentaPickerProps { interface ConstituentaPickerProps {
prefixID?: string prefixID?: string;
data?: IConstituenta[] data?: IConstituenta[];
rows?: number rows?: number;
onBeginFilter?: (cst: IConstituenta) => boolean onBeginFilter?: (cst: IConstituenta) => boolean;
describeFunc?: (cst: IConstituenta) => string describeFunc?: (cst: IConstituenta) => string;
matchFunc?: (cst: IConstituenta, filter: string) => boolean matchFunc?: (cst: IConstituenta, filter: string) => boolean;
value?: IConstituenta value?: IConstituenta;
onSelectValue: (newValue: IConstituenta) => void onSelectValue: (newValue: IConstituenta) => void;
} }
const columnHelper = createColumnHelper<IConstituenta>(); const columnHelper = createColumnHelper<IConstituenta>();
function ConstituentaPicker({ function ConstituentaPicker({
data, value, data,
value,
rows = 4, rows = 4,
prefixID = prefixes.cst_list, prefixID = prefixes.cst_list,
describeFunc = describeConstituenta, describeFunc = describeConstituenta,
matchFunc = (cst, filter) => matchConstituenta(cst, filter, CstMatchMode.ALL), matchFunc = (cst, filter) => matchConstituenta(cst, filter, CstMatchMode.ALL),
onBeginFilter, onBeginFilter,
onSelectValue onSelectValue
} : ConstituentaPickerProps) { }: ConstituentaPickerProps) {
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
const [filteredData, setFilteredData] = useState<IConstituenta[]>([]); const [filteredData, setFilteredData] = useState<IConstituenta[]>([]);
const [filterText, setFilterText] = useState(''); const [filterText, setFilterText] = useState('');
useEffect( useEffect(() => {
() => {
if (!data) { if (!data) {
setFilteredData([]); setFilteredData([]);
} else { } else {
@ -51,57 +51,58 @@ function ConstituentaPicker({
setFilteredData(newData); setFilteredData(newData);
} }
} }
}, [data, filterText, matchFunc, onBeginFilter]); }, [data, filterText, matchFunc, onBeginFilter]);
const columns = useMemo( const columns = useMemo(
() => [ () => [
columnHelper.accessor('alias', { columnHelper.accessor('alias', {
id: 'alias', id: 'alias',
size: 65, size: 65,
minSize: 65, minSize: 65,
maxSize: 65, maxSize: 65,
cell: props => cell: props => <ConstituentaBadge theme={colors} value={props.row.original} prefixID={prefixID} />
<ConstituentaBadge }),
theme={colors} columnHelper.accessor(cst => describeFunc(cst), {
value={props.row.original} id: 'description'
prefixID={prefixID} })
/> ],
}), [colors, prefixID, describeFunc]
columnHelper.accessor(cst => describeFunc(cst), { );
id: 'description'
})
], [colors, prefixID, describeFunc]);
const size = useMemo(() => (`calc(2px + (2px + 1.8rem)*${rows})`), [rows]); const size = useMemo(() => `calc(2px + (2px + 1.8rem)*${rows})`, [rows]);
const conditionalRowStyles = useMemo( const conditionalRowStyles = useMemo(
(): IConditionalStyle<IConstituenta>[] => [{ (): IConditionalStyle<IConstituenta>[] => [
when: (cst: IConstituenta) => cst.id === value?.id, {
style: { backgroundColor: colors.bgSelected }, when: (cst: IConstituenta) => cst.id === value?.id,
}], [value, colors]); style: { backgroundColor: colors.bgSelected }
}
],
[value, colors]
);
return ( return (
<div> <div>
<ConceptSearch <ConceptSearch value={filterText} onChange={newValue => setFilterText(newValue)} />
value={filterText} <DataTable
onChange={newValue => setFilterText(newValue)} dense
/> noHeader
<DataTable dense noHeader noFooter noFooter
className='overflow-y-auto text-sm border select-none' className='overflow-y-auto text-sm border select-none'
style={{ maxHeight: size, minHeight: size }} style={{ maxHeight: size, minHeight: size }}
data={filteredData} data={filteredData}
columns={columns} columns={columns}
conditionalRowStyles={conditionalRowStyles} conditionalRowStyles={conditionalRowStyles}
noDataComponent={ noDataComponent={
<span className='p-2 min-h-[5rem] flex flex-col justify-center text-center'> <span className='p-2 min-h-[5rem] flex flex-col justify-center text-center'>
<p>Список конституент пуст</p> <p>Список конституент пуст</p>
<p>Измените параметры фильтра</p> <p>Измените параметры фильтра</p>
</span> </span>
} }
onRowClicked={onSelectValue} onRowClicked={onSelectValue}
/> />
</div>); </div>
);
} }
export default ConstituentaPicker; export default ConstituentaPicker;

View File

@ -6,29 +6,30 @@ import { colorFgGrammeme } from '@/utils/color';
import { labelGrammeme } from '@/utils/labels'; import { labelGrammeme } from '@/utils/labels';
interface GrammemeBadgeProps { interface GrammemeBadgeProps {
key?: string key?: string;
grammeme: GramData grammeme: GramData;
} }
function GrammemeBadge({ key, grammeme }: GrammemeBadgeProps) { function GrammemeBadge({ key, grammeme }: GrammemeBadgeProps) {
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
return ( return (
<div <div
key={key} key={key}
className={clsx( className={clsx(
'min-w-[3rem]', 'min-w-[3rem]',
'px-1', 'px-1',
'border rounded-md', 'border rounded-md',
'text-sm font-semibold text-center whitespace-nowrap' 'text-sm font-semibold text-center whitespace-nowrap'
)} )}
style={{ style={{
borderColor: colorFgGrammeme(grammeme, colors), borderColor: colorFgGrammeme(grammeme, colors),
color: colorFgGrammeme(grammeme, colors), color: colorFgGrammeme(grammeme, colors),
backgroundColor: colors.bgInput backgroundColor: colors.bgInput
}} }}
> >
{labelGrammeme(grammeme)} {labelGrammeme(grammeme)}
</div>); </div>
);
} }
export default GrammemeBadge; export default GrammemeBadge;

View File

@ -1,39 +1,42 @@
import { IConstituenta } from '@/models/rsform'; import { IConstituenta } from '@/models/rsform';
import { labelCstTypification } from '@/utils/labels'; import { labelCstTypification } from '@/utils/labels';
interface InfoConstituentaProps interface InfoConstituentaProps extends React.HTMLAttributes<HTMLDivElement> {
extends React.HTMLAttributes<HTMLDivElement> { data: IConstituenta;
data: IConstituenta
} }
function InfoConstituenta({ data, ...restProps }: InfoConstituentaProps) { function InfoConstituenta({ data, ...restProps }: InfoConstituentaProps) {
return ( return (
<div {...restProps}> <div {...restProps}>
<h2>Конституента {data.alias}</h2> <h2>Конституента {data.alias}</h2>
<p> <p>
<b>Типизация: </b> <b>Типизация: </b>
{labelCstTypification(data)} {labelCstTypification(data)}
</p> </p>
<p> <p>
<b>Термин: </b> <b>Термин: </b>
{data.term_resolved || data.term_raw} {data.term_resolved || data.term_raw}
</p> </p>
{data.definition_formal ? {data.definition_formal ? (
<p> <p>
<b>Выражение: </b> <b>Выражение: </b>
{data.definition_formal} {data.definition_formal}
</p> : null} </p>
{data.definition_resolved ? ) : null}
<p> {data.definition_resolved ? (
<b>Определение: </b> <p>
{data.definition_resolved} <b>Определение: </b>
</p> : null} {data.definition_resolved}
{data.convention ? </p>
<p> ) : null}
<b>Конвенция: </b> {data.convention ? (
{data.convention} <p>
</p> : null} <b>Конвенция: </b>
</div>); {data.convention}
</p>
) : null}
</div>
);
} }
export default InfoConstituenta; export default InfoConstituenta;

View File

@ -7,38 +7,37 @@ import { prefixes } from '@/utils/constants';
import { describeCstClass, labelCstClass } from '@/utils/labels'; import { describeCstClass, labelCstClass } from '@/utils/labels';
interface InfoCstClassProps { interface InfoCstClassProps {
header?: string header?: string;
} }
function InfoCstClass({ header }: InfoCstClassProps) { function InfoCstClass({ header }: InfoCstClassProps) {
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
return ( return (
<div className='flex flex-col gap-1 mb-2'> <div className='flex flex-col gap-1 mb-2'>
{header ? <h1>{header}</h1> : null} {header ? <h1>{header}</h1> : null}
{Object.values(CstClass).map( {Object.values(CstClass).map((cstClass, index) => {
(cclass, index) => { return (
return ( <p key={`${prefixes.cst_status_list}${index}`}>
<p key={`${prefixes.cst_status_list}${index}`}> <span
<span className={clsx(
className={clsx( 'inline-block',
'inline-block', 'min-w-[7rem]',
'min-w-[7rem]', 'px-1',
'px-1', 'border',
'border', 'text-center text-sm small-caps font-semibold'
'text-center text-sm small-caps font-semibold' )}
)} style={{ backgroundColor: colorBgCstClass(cstClass, colors) }}
style={{backgroundColor: colorBgCstClass(cclass, colors)}} >
> {labelCstClass(cstClass)}
{labelCstClass(cclass)} </span>
</span> <span> - </span>
<span> - </span> <span>{describeCstClass(cstClass)}</span>
<span> </p>
{describeCstClass(cclass)} );
</span> })}
</p>); </div>
})} );
</div>);
} }
export default InfoCstClass; export default InfoCstClass;

View File

@ -7,39 +7,37 @@ import { prefixes } from '@/utils/constants';
import { describeExpressionStatus, labelExpressionStatus } from '@/utils/labels'; import { describeExpressionStatus, labelExpressionStatus } from '@/utils/labels';
interface InfoCstStatusProps { interface InfoCstStatusProps {
title?: string title?: string;
} }
function InfoCstStatus({ title }: InfoCstStatusProps) { function InfoCstStatus({ title }: InfoCstStatusProps) {
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
return ( return (
<div className='flex flex-col gap-1 mb-2'> <div className='flex flex-col gap-1 mb-2'>
{title ? <h1>{title}</h1> : null} {title ? <h1>{title}</h1> : null}
{Object.values(ExpressionStatus) {Object.values(ExpressionStatus)
.filter(status => status !== ExpressionStatus.UNDEFINED) .filter(status => status !== ExpressionStatus.UNDEFINED)
.map( .map((status, index) => (
(status, index) => <p key={`${prefixes.cst_status_list}${index}`}>
<p key={`${prefixes.cst_status_list}${index}`}> <span
<span className={clsx(
className={clsx( 'inline-block',
'inline-block', 'min-w-[7rem]',
'min-w-[7rem]', 'px-1',
'px-1', 'border',
'border', 'text-center text-sm small-caps font-semibold'
'text-center text-sm small-caps font-semibold' )}
)} style={{ backgroundColor: colorBgCstStatus(status, colors) }}
style={{backgroundColor: colorBgCstStatus(status, colors)}} >
> {labelExpressionStatus(status)}
{labelExpressionStatus(status)} </span>
</span> <span> - </span>
<span> - </span> <span>{describeExpressionStatus(status)}</span>
<span> </p>
{describeExpressionStatus(status)} ))}
</span> </div>
</p> );
)}
</div>);
} }
export default InfoCstStatus; export default InfoCstStatus;

View File

@ -4,35 +4,36 @@ import { useUsers } from '@/context/UsersContext';
import { ILibraryItemEx } from '@/models/library'; import { ILibraryItemEx } from '@/models/library';
interface InfoLibraryItemProps { interface InfoLibraryItemProps {
item?: ILibraryItemEx item?: ILibraryItemEx;
} }
function InfoLibraryItem({ item }: InfoLibraryItemProps) { function InfoLibraryItem({ item }: InfoLibraryItemProps) {
const { getUserLabel } = useUsers(); const { getUserLabel } = useUsers();
const intl = useIntl(); const intl = useIntl();
return ( return (
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-1'>
<div className='flex'> <div className='flex'>
<label className='font-semibold'>Владелец:</label> <label className='font-semibold'>Владелец:</label>
<span className='min-w-[200px] ml-2 overflow-ellipsis overflow-hidden whitespace-nowrap'> <span className='min-w-[200px] ml-2 overflow-ellipsis overflow-hidden whitespace-nowrap'>
{getUserLabel(item?.owner ?? null)} {getUserLabel(item?.owner ?? null)}
</span> </span>
</div>
<div className='flex'>
<label className='font-semibold'>Отслеживают:</label>
<span id='subscriber-count' className='ml-2'>
{item?.subscribers.length ?? 0}
</span>
</div>
<div className='flex'>
<label className='font-semibold'>Дата обновления:</label>
<span className='ml-2'>{item && new Date(item?.time_update).toLocaleString(intl.locale)}</span>
</div>
<div className='flex'>
<label className='font-semibold'>Дата создания:</label>
<span className='ml-8'>{item && new Date(item?.time_create).toLocaleString(intl.locale)}</span>
</div>
</div> </div>
<div className='flex'> );
<label className='font-semibold'>Отслеживают:</label>
<span id='subscriber-count' className='ml-2'>
{ item?.subscribers.length ?? 0 }
</span>
</div>
<div className='flex'>
<label className='font-semibold'>Дата обновления:</label>
<span className='ml-2'>{item && new Date(item?.time_update).toLocaleString(intl.locale)}</span>
</div>
<div className='flex'>
<label className='font-semibold'>Дата создания:</label>
<span className='ml-8'>{item && new Date(item?.time_create).toLocaleString(intl.locale)}</span>
</div>
</div>);
} }
export default InfoLibraryItem; export default InfoLibraryItem;

View File

@ -3,39 +3,33 @@ import { useEffect, useState } from 'react';
import SelectMulti, { SelectMultiProps } from '@/components/Common/SelectMulti'; import SelectMulti, { SelectMultiProps } from '@/components/Common/SelectMulti';
import { Grammeme } from '@/models/language'; import { Grammeme } from '@/models/language';
import { getCompatibleGrams } from '@/models/languageAPI'; import { getCompatibleGrams } from '@/models/languageAPI';
import { compareGrammemeOptions,IGrammemeOption, SelectorGrammemes } from '@/utils/selectors'; import { compareGrammemeOptions, IGrammemeOption, SelectorGrammemes } from '@/utils/selectors';
interface SelectGrammemeProps extends interface SelectGrammemeProps extends Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'> {
Omit<SelectMultiProps<IGrammemeOption>, 'value' | 'onChange'> { value: IGrammemeOption[];
value: IGrammemeOption[] setValue: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>;
setValue: React.Dispatch<React.SetStateAction<IGrammemeOption[]>> className?: string;
className?: string placeholder?: string;
placeholder?: string
} }
function SelectGrammeme({ function SelectGrammeme({ value, setValue, ...restProps }: SelectGrammemeProps) {
value, setValue,
...restProps
}: SelectGrammemeProps) {
const [options, setOptions] = useState<IGrammemeOption[]>([]); const [options, setOptions] = useState<IGrammemeOption[]>([]);
useEffect( useEffect(() => {
() => {
const compatible = getCompatibleGrams( const compatible = getCompatibleGrams(
value value.filter(data => Object.values(Grammeme).includes(data.value as Grammeme)).map(data => data.value as Grammeme)
.filter(data => Object.values(Grammeme).includes(data.value as Grammeme))
.map(data => data.value as Grammeme)
); );
setOptions(SelectorGrammemes.filter(({value}) => compatible.includes(value as Grammeme))); setOptions(SelectorGrammemes.filter(({ value }) => compatible.includes(value as Grammeme)));
}, [value]); }, [value]);
return ( return (
<SelectMulti <SelectMulti
options={options} options={options}
value={value} value={value}
onChange={newValue => setValue([...newValue].sort(compareGrammemeOptions))} onChange={newValue => setValue([...newValue].sort(compareGrammemeOptions))}
{...restProps} {...restProps}
/>); />
);
} }
export default SelectGrammeme; export default SelectGrammeme;

View File

@ -1,26 +1,21 @@
import Overlay from '@/components/Common/Overlay'; import Overlay from '@/components/Common/Overlay';
interface SelectedCounterProps { interface SelectedCounterProps {
total: number total: number;
selected: number selected: number;
position?: string position?: string;
hideZero?: boolean hideZero?: boolean;
} }
function SelectedCounter({ function SelectedCounter({ total, selected, hideZero, position = 'top-0 left-0' }: SelectedCounterProps) {
total, selected, hideZero,
position = 'top-0 left-0',
} : SelectedCounterProps) {
if (selected === 0 && hideZero) { if (selected === 0 && hideZero) {
return null; return null;
} }
return ( return (
<Overlay <Overlay position={`px-2 ${position}`} className='select-none whitespace-nowrap small-caps clr-app'>
position={`px-2 ${position}`} Выбор {selected} из {total}
className='select-none whitespace-nowrap small-caps clr-app' </Overlay>
> );
Выбор {selected} из {total}
</Overlay>);
} }
export default SelectedCounter; export default SelectedCounter;

View File

@ -3,21 +3,18 @@ import { IWordForm } from '@/models/language';
import GrammemeBadge from './GrammemeBadge'; import GrammemeBadge from './GrammemeBadge';
interface WordFormBadgeProps { interface WordFormBadgeProps {
keyPrefix?: string keyPrefix?: string;
form: IWordForm form: IWordForm;
} }
function WordFormBadge({ keyPrefix, form }: WordFormBadgeProps) { function WordFormBadge({ keyPrefix, form }: WordFormBadgeProps) {
return ( return (
<div className='flex flex-wrap justify-start gap-1 select-none'> <div className='flex flex-wrap justify-start gap-1 select-none'>
{form.grams.map( {form.grams.map(gram => (
(gram) => <GrammemeBadge key={`${keyPrefix}-${gram}`} grammeme={gram} />
<GrammemeBadge ))}
key={`${keyPrefix}-${gram}`} </div>
grammeme={gram} );
/>
)}
</div>);
} }
export default WordFormBadge; export default WordFormBadge;

View File

@ -2,39 +2,40 @@
import { HTMLMotionProps } from 'framer-motion'; import { HTMLMotionProps } from 'framer-motion';
export namespace CProps { export namespace CProps {
export type Control = {
title?: string;
disabled?: boolean;
noBorder?: boolean;
noOutline?: boolean;
};
export type Control = { export type Styling = {
title?: string style?: React.CSSProperties;
disabled?: boolean className?: string;
noBorder?: boolean };
noOutline?: boolean
export type Editor = Control & {
label?: string;
};
export type Colors = {
colors?: string;
};
export type Div = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
export type Button = Omit<
React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>,
'children' | 'type'
>;
export type Label = Omit<
React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>,
'children'
>;
export type TextArea = React.DetailedHTMLProps<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
HTMLTextAreaElement
>;
export type Input = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
export type AnimatedButton = Omit<HTMLMotionProps<'button'>, 'type'>;
} }
export type Styling = {
style?: React.CSSProperties
className?: string
}
export type Editor = Control & {
label?: string
}
export type Colors = {
colors?: string
}
export type Div = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
export type Button = Omit<
React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>,
'children' | 'type'
>;
export type Label = Omit<
React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>,
'children'
>;
export type TextArea = React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>;
export type Input = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
export type AnimatedButton = Omit<HTMLMotionProps<'button'>, 'type'>;
}

Some files were not shown because too many files have changed in this diff Show More