# Conflicts:
#	rsconcept/frontend/src/index.css
This commit is contained in:
Ulle9 2023-09-09 12:21:27 +03:00
commit d8002923cb
34 changed files with 1462 additions and 1243 deletions

View File

@ -20,7 +20,7 @@ This readme file is used mostly to document project dependencies
- react-tabs - react-tabs
- react-intl - react-intl
- react-data-table-component - react-data-table-component
- react-dropdown-select - react-select
- react-error-boundary - react-error-boundary
- reagraph - reagraph
- react-tooltip - react-tooltip

View File

@ -9,12 +9,11 @@ For more specific TODOs see comments in code
- блок организации библиотеки моделей - блок организации библиотеки моделей
- проектный модуль? - проектный модуль?
- обратная связь - система баг репортов - обратная связь - система баг репортов
- система обработки ошибок backend
[Tech] [Tech]
- create custom Select component
- reload react-data-table-component - reload react-data-table-component
[deployment] [deployment]
- database backup daemon
- logs collection - logs collection
- status dashboard for servers - status dashboard for servers

View File

@ -1,5 +1,5 @@
# ======== Multi-stage base ========== # ======== Multi-stage base ==========
FROM node:bullseye-slim as node-base FROM node:18-bullseye-slim as node-base
RUN apt-get update -qq && \ RUN apt-get update -qq && \
apt-get upgrade -y && \ apt-get upgrade -y && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*

File diff suppressed because it is too large Load Diff

View File

@ -12,44 +12,44 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@lezer/lr": "^1.3.9", "@lezer/lr": "^1.3.10",
"@uiw/codemirror-themes": "^4.21.9", "@uiw/codemirror-themes": "^4.21.13",
"@uiw/react-codemirror": "^4.21.9", "@uiw/react-codemirror": "^4.21.13",
"axios": "^1.4.0", "axios": "^1.5.0",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"react": "^18.2.0", "react": "^18.2.0",
"react-data-table-component": "^7.5.3", "react-data-table-component": "^7.5.4",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropdown-select": "^4.10.0", "react-error-boundary": "^4.0.11",
"react-error-boundary": "^4.0.10",
"react-intl": "^6.4.4", "react-intl": "^6.4.4",
"react-loader-spinner": "^5.3.4", "react-loader-spinner": "^5.4.5",
"react-router-dom": "^6.14.2", "react-router-dom": "^6.15.0",
"react-select": "^5.7.4",
"react-tabs": "^6.0.2", "react-tabs": "^6.0.2",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"react-tooltip": "^5.19.0", "react-tooltip": "^5.21.3",
"reagraph": "^4.11.1" "reagraph": "^4.13.0"
}, },
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.4.0", "@lezer/generator": "^1.5.0",
"@types/jest": "^29.5.3", "@types/jest": "^29.5.4",
"@types/node": "^20.4.5", "@types/node": "^20.5.9",
"@types/react": "^18.2.15", "@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.6.0",
"@vitejs/plugin-react": "^4.0.3", "@vitejs/plugin-react": "^4.0.4",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.15",
"eslint": "^8.45.0", "eslint": "^8.48.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3", "eslint-plugin-react-refresh": "^0.4.3",
"eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-simple-import-sort": "^10.0.0",
"jest": "^29.6.2", "jest": "^29.6.4",
"postcss": "^8.4.27", "postcss": "^8.4.29",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
"typescript": "^5.0.2", "typescript": "^5.2.2",
"vite": "^4.4.5" "vite": "^4.4.9"
}, },
"jest": { "jest": {
"preset": "ts-jest", "preset": "ts-jest",

View File

@ -1,5 +1,6 @@
import { useRef } from 'react'; import { useMemo } from 'react';
import { CheckboxChecked } from '../Icons';
import Label from './Label'; import Label from './Label';
export interface CheckboxProps { export interface CheckboxProps {
@ -9,51 +10,52 @@ export interface CheckboxProps {
disabled?: boolean disabled?: boolean
widthClass?: string widthClass?: string
tooltip?: string tooltip?: string
value?: boolean
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void value: boolean
setValue?: (newValue: boolean) => void
} }
function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-fit', value, onChange }: CheckboxProps) { function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-fit', value, setValue }: CheckboxProps) {
const inputRef = useRef<HTMLInputElement | null>(null); const cursor = useMemo(
() => {
const cursor = disabled ? 'cursor-not-allowed' : 'cursor-pointer'; if (disabled) {
return 'cursor-not-allowed';
} else if (setValue) {
return 'cursor-pointer';
} else {
return ''
}
}, [disabled, setValue]);
const bgColor = useMemo(
() => {
return value !== false ? 'clr-primary' : 'clr-app'
}, [value]);
function handleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void { function handleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void {
event.preventDefault(); event.preventDefault();
if (!disabled) { if (disabled || !setValue) {
inputRef.current?.click(); return;
} }
setValue(!value);
} }
return ( return (
<button <button
className={'flex [&:not(:first-child)]:mt-3 clr-outline focus:outline-dotted focus:outline-1 ' + widthClass} id={id}
className={`flex items-center [&:not(:first-child)]:mt-3 clr-outline focus:outline-dotted focus:outline-1 ${widthClass}`}
title={tooltip} title={tooltip}
disabled={disabled} disabled={disabled}
onClick={handleClick} onClick={handleClick}
> >
<input id={id} type='checkbox' ref={inputRef} <div className={`relative peer w-4 h-4 shrink-0 mt-0.5 border rounded-sm appearance-none ${bgColor} ${cursor}`} />
className={`relative peer w-4 h-4 shrink-0 mt-0.5 border rounded-sm appearance-none clr-checkbox ${cursor}`}
required={required}
disabled={disabled}
checked={value}
onChange={onChange}
tabIndex={-1}
/>
{ label && { label &&
<Label <Label
className={`${cursor} px-2`} className={`${cursor} px-2 text-start`}
text={label} text={label}
required={required} required={required}
htmlFor={id} htmlFor={id}
/>} />}
<svg {value && <CheckboxChecked />}
className='absolute hidden w-3 h-3 mt-1 ml-0.5 text-white pointer-events-none peer-checked:block'
viewBox='0 0 512 512'
fill='currentColor'
>
<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>
</button> </button>
); );
} }

View File

@ -1,19 +0,0 @@
import { PropsWithRef } from 'react';
import Select, { SelectProps } from 'react-dropdown-select';
interface ConceptSelectProps<T>
extends Omit<PropsWithRef<SelectProps<T>>, 'noDataLabel'> {
}
function ConceptSelect<T extends object | string>({ className, ...props }: ConceptSelectProps<T>) {
return (
<Select
className={`overflow-x-ellipsis whitespace-nowrap clr-border clr-input outline-none ${className}`}
{...props}
noDataLabel='Список пуст'
/>
);
}
export default ConceptSelect;

View File

@ -0,0 +1,66 @@
import { useMemo } from 'react';
import Select, { GroupBase, Props, StylesConfig } from 'react-select';
import { useConceptTheme } from '../../context/ThemeContext';
import { selectDarkT, selectLightT } from '../../utils/color';
interface ConceptSelectSingleProps<
Option,
Group extends GroupBase<Option> = GroupBase<Option>
>
extends Omit<Props<Option, false, Group>, 'theme'> {
}
function ConceptSelectSingle<
Option,
Group extends GroupBase<Option> = GroupBase<Option>
> ({ ...props }: ConceptSelectSingleProps<Option, Group>) {
const { darkMode, colors } = useConceptTheme();
const themeColors = useMemo(
() => {
return !darkMode ? selectLightT : selectDarkT;
}, [darkMode]);
const adjustedStyles: StylesConfig<Option, false, Group> = useMemo(
() => {
return {
control: (styles, { isDisabled }) => {
return {
...styles,
borderRadius: '0.25rem',
cursor: isDisabled ? 'not-allowed' : 'pointer'
};
},
option: (styles, { isSelected }) => {
return {
...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 (
<Select
noOptionsMessage={() => 'Список пуст'}
theme={theme => ({
...theme,
borderRadius: 0,
colors: {
...theme.colors,
...themeColors
},
})}
styles={adjustedStyles}
{...props}
/>
);
}
export default ConceptSelectSingle;

View File

@ -1,15 +1,15 @@
import Checkbox from './Checkbox'; import Checkbox from './Checkbox';
interface DropdownCheckboxProps { interface DropdownCheckboxProps {
value: boolean
label?: string label?: string
tooltip?: string tooltip?: string
disabled?: boolean disabled?: boolean
value?: boolean setValue?: (newValue: boolean) => void
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
} }
function DropdownCheckbox({ tooltip, onChange, disabled, ...props }: DropdownCheckboxProps) { function DropdownCheckbox({ tooltip, setValue, disabled, ...props }: DropdownCheckboxProps) {
const behavior = (onChange && !disabled ? 'clr-hover' : ''); const behavior = (setValue && !disabled ? 'clr-hover' : '');
return ( return (
<div <div
title={tooltip} title={tooltip}
@ -18,7 +18,7 @@ function DropdownCheckbox({ tooltip, onChange, disabled, ...props }: DropdownChe
<Checkbox <Checkbox
widthClass='w-full' widthClass='w-full'
disabled={disabled} disabled={disabled}
onChange={onChange} setValue={setValue}
{...props} {...props}
/> />
</div> </div>

View File

@ -4,18 +4,22 @@ import { UploadIcon } from '../Icons';
import Button from './Button'; import Button from './Button';
import Label from './Label'; import Label from './Label';
interface FileInputProps { interface FileInputProps
id?: string extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className' | 'title' | 'style' | 'accept' | 'type'> {
required?: boolean
label: string label: string
tooltip?: string
acceptType?: string acceptType?: string
widthClass?: string widthClass?: string
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
} }
function FileInput({ id, required, label, acceptType, widthClass = 'w-full', onChange }: FileInputProps) { function FileInput({
label, acceptType, tooltip,
widthClass = 'w-fit', onChange,
...props
}: FileInputProps) {
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
const [labelText, setLabelText] = useState(''); const [fileName, setFileName] = useState('');
const handleUploadClick = () => { const handleUploadClick = () => {
inputRef.current?.click(); inputRef.current?.click();
@ -23,9 +27,9 @@ function FileInput({ id, required, label, acceptType, widthClass = 'w-full', onC
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) { if (event.target.files && event.target.files.length > 0) {
setLabelText(event.target.files[0].name) setFileName(event.target.files[0].name)
} else { } else {
setLabelText('') setFileName('')
} }
if (onChange) { if (onChange) {
onChange(event); onChange(event);
@ -33,21 +37,22 @@ function FileInput({ id, required, label, acceptType, widthClass = 'w-full', onC
}; };
return ( return (
<div className={'flex flex-col gap-2 py-2 [&:not(:first-child)]:mt-3 items-start ' + widthClass}> <div className={`flex flex-col gap-2 py-2 mt-3 items-start ${widthClass}`}>
<input id={id} type='file' <input type='file'
ref={inputRef} ref={inputRef}
required={required}
style={{ display: 'none' }} style={{ display: 'none' }}
accept={acceptType} accept={acceptType}
onChange={handleFileChange} onChange={handleFileChange}
{...props}
/> />
<Button <Button
text={label} text={label}
icon={<UploadIcon/>} icon={<UploadIcon/>}
onClick={handleUploadClick} onClick={handleUploadClick}
tooltip={tooltip}
/> />
<Label <Label
text={labelText} text={fileName}
/> />
</div> </div>
); );

View File

@ -14,7 +14,7 @@ function SubmitButton({
return ( return (
<button type='submit' <button type='submit'
title={tooltip} title={tooltip}
className={`px-4 py-2 inline-flex items-center gap-2 align-middle justify-center font-bold select-none disabled:cursor-not-allowed border rounded clr-btn-primary ${widthClass} ${loading ? ' cursor-progress' : ''}`} className={`px-4 py-2 inline-flex items-center gap-2 align-middle justify-center font-semibold select-none disabled:cursor-not-allowed border rounded clr-btn-primary ${widthClass} ${loading ? ' cursor-progress' : ''}`}
disabled={disabled ?? loading} disabled={disabled ?? loading}
> >
{icon && <span>{icon}</span>} {icon && <span>{icon}</span>}

View File

@ -1,9 +1,7 @@
import { type InputHTMLAttributes } from 'react';
import Label from './Label'; import Label from './Label';
interface TextInputProps interface TextInputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'title'> { extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className' | 'title'> {
id: string id: string
label: string label: string
tooltip?: string tooltip?: string

View File

@ -0,0 +1,64 @@
import { useMemo } from 'react';
import { CheckboxChecked, CheckboxNull } from '../Icons';
import { CheckboxProps } from './Checkbox';
import Label from './Label';
export interface TristateProps
extends Omit<CheckboxProps, 'value' | 'setValue'> {
value: boolean | null
setValue?: (newValue: boolean | null) => void
}
function Checkbox({ id, required, disabled, tooltip, label, widthClass = 'w-fit', value, setValue }: TristateProps) {
const cursor = useMemo(
() => {
if (disabled) {
return 'cursor-not-allowed';
} else if (setValue) {
return 'cursor-pointer';
} else {
return ''
}
}, [disabled, setValue]);
const bgColor = useMemo(
() => {
return value !== false ? 'clr-primary' : 'clr-app'
}, [value]);
function handleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void {
event.preventDefault();
if (disabled || !setValue) {
return;
}
if (value === false) {
setValue(null);
} else {
setValue(!value);
}
}
return (
<button
id={id}
className={`flex items-center [&:not(:first-child)]:mt-3 clr-outline focus:outline-dotted focus:outline-1 ${widthClass}`}
title={tooltip}
disabled={disabled}
onClick={handleClick}
>
<div className={`relative peer w-4 h-4 shrink-0 mt-0.5 border rounded-sm appearance-none ${bgColor} ${cursor}`} />
{ label &&
<Label
className={`${cursor} px-2 text-start`}
text={label}
required={required}
htmlFor={id}
/>}
{value && <CheckboxChecked />}
{value === null && <CheckboxNull />}
</button>
);
}
export default Checkbox;

View File

@ -317,3 +317,27 @@ export function InDoor(props: IconProps) {
</IconSVG> </IconSVG>
); );
} }
export function CheckboxChecked() {
return (
<svg
className='absolute w-3 h-3 mt-1 ml-0.5'
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' />
</svg>
);
}
export function CheckboxNull() {
return (
<svg
className='absolute w-3 h-3 mt-1 ml-0.5'
viewBox='0 0 512 512'
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' />
</svg>
);
}

View File

@ -1,6 +1,6 @@
import { Extension } from '@codemirror/state'; import { Extension } from '@codemirror/state';
import { tags as t } from '@lezer/highlight'; import { tags } from '@lezer/highlight';
import { createTheme } from '@uiw/codemirror-themes'; import { createTheme } from '@uiw/codemirror-themes';
import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror'; import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { EditorView } from 'codemirror'; import { EditorView } from 'codemirror';
@ -64,9 +64,9 @@ function RSInput({
}, [internalRef, innerref]); }, [internalRef, innerref]);
const cursor = useMemo(() => editable ? 'cursor-text': 'cursor-default', [editable]); const cursor = useMemo(() => editable ? 'cursor-text': 'cursor-default', [editable]);
const lightTheme: Extension = useMemo( const customTheme: Extension = useMemo(
() => createTheme({ () => createTheme({
theme: 'light', theme: darkMode ? 'dark' : 'light',
settings: { settings: {
fontFamily: 'inherit', fontFamily: 'inherit',
background: editable ? colors.bgInput : colors.bgDefault, background: editable ? colors.bgInput : colors.bgDefault,
@ -74,35 +74,15 @@ function RSInput({
selection: colors.bgHover selection: colors.bgHover
}, },
styles: [ styles: [
{ tag: t.name, class: 'text-[#b266ff] cursor-default' }, // GlobalID { tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // GlobalID
{ tag: t.variableName, class: 'text-[#24821a]' }, // LocalID { tag: tags.variableName, color: colors.fgGreen }, // LocalID
{ tag: t.propertyName, class: '' }, // Radical { tag: tags.propertyName, color: colors.fgTeal }, // Radical
{ tag: t.keyword, class: 'text-[#001aff]' }, // keywords { tag: tags.keyword, color: colors.fgBlue }, // keywords
{ tag: t.literal, class: 'text-[#001aff]' }, // literals { tag: tags.literal, color: colors.fgBlue }, // literals
{ tag: t.controlKeyword, class: 'font-semibold'}, // R | I | D { tag: tags.controlKeyword, fontWeight: '500'}, // R | I | D
{ tag: t.unit, class: 'text-[0.75rem]' }, // indicies { tag: tags.unit, fontSize: '0.75rem' }, // indicies
] ]
}), [editable, colors]); }), [editable, colors, darkMode]);
const darkTheme: Extension = useMemo(
() => createTheme({
theme: 'dark',
settings: {
fontFamily: 'inherit',
background: editable ? colors.bgInput : colors.bgDefault,
foreground: colors.fgDefault,
selection: colors.bgHover
},
styles: [
{ tag: t.name, class: 'text-[#dfbfff] cursor-default' }, // GlobalID
{ tag: t.variableName, class: 'text-[#69bf60]' }, // LocalID
{ tag: t.propertyName, class: '' }, // Radical
{ tag: t.keyword, class: 'text-[#808dff]' }, // keywords
{ tag: t.literal, class: 'text-[#808dff]' }, // literals
{ tag: t.controlKeyword, class: 'font-semibold'}, // R | I | D
{ tag: t.unit, class: 'text-[0.75rem]' }, // indicies
]
}), [editable, colors]);
const editorExtensions = useMemo( const editorExtensions = useMemo(
() => [ () => [
@ -148,7 +128,7 @@ function RSInput({
<CodeMirror id={id} <CodeMirror id={id}
ref={thisRef} ref={thisRef}
basicSetup={editorSetup} basicSetup={editorSetup}
theme={darkMode ? darkTheme : lightTheme} theme={customTheme}
extensions={editorExtensions} extensions={editorExtensions}
indentWithTab={false} indentWithTab={false}
onChange={onChange} onChange={onChange}

View File

@ -6,60 +6,48 @@
:root { :root {
/* Light Theme */ /* Light Theme */
--cl-bg-120: #ffffff; --cl-bg-120: hsl(000, 000%, 100%);
--cl-bg-100: #f9fafb; --cl-bg-100: hsl(220, 020%, 098%);
--cl-bg-80: #f3f4f6; --cl-bg-80: hsl(220, 014%, 096%);
--cl-bg-60: #e5e7eb; --cl-bg-60: hsl(220, 013%, 091%);
--cl-bg-40: #d1d5db; --cl-bg-40: hsl(216, 012%, 084%);
--cl-fg-40: #8c8c8c; --cl-fg-60: hsl(000, 000%, 055%);
--cl-fg-60: #777777; --cl-fg-80: hsl(000, 000%, 047%);
--cl-fg-80: #333333; --cl-fg-100: hsl(000, 000%, 000%);
--cl-fg-100: #000000;
--cl-prim-bg-100: #3377ff; --cl-prim-bg-100: hsl(220, 100%, 060%);
--cl-prim-bg-80: #ccddff; --cl-prim-bg-80: hsl(220, 100%, 090%);
--cl-prim-bg-60: #e0ebff; --cl-prim-bg-60: hsl(220, 100%, 094%);
--cl-prim-fg-60: #1a63ff; --cl-prim-fg-80: hsl(220, 100%, 050%);
--cl-prim-fg-80: #0051ff; --cl-prim-fg-100: hsl(000, 000%, 100%);
--cl-prim-fg-100: #ffffff;
--cl-red-bg-100: #ffe5e5; --cl-red-bg-100: hsl(000, 100%, 095%);
--cl-red-fg-100: #dc2626; --cl-red-fg-100: hsl(000, 072%, 051%);
--cl-green-fg-100: #4ade80; --cl-green-fg-100: hsl(120, 080%, 058%);
/* Dark Theme */ /* Dark Theme */
--cd-bg-120: #0d0d0d; --cd-bg-120: hsl(000, 000%, 005%);
--cd-bg-100: #181818; --cd-bg-100: hsl(000, 000%, 009%);
--cd-bg-80: #272727; --cd-bg-80: hsl(000, 000%, 015%);
--cd-bg-60: #383838; --cd-bg-60: hsl(000, 000%, 022%);
--cd-bg-40: #595959; --cd-bg-40: hsl(000, 000%, 035%);
--cd-fg-40: #878792; --cd-fg-60: hsl(000, 000%, 055%);
--cd-fg-60: #bcbcc2; --cd-fg-80: hsl(000, 000%, 080%);
--cd-fg-80: #d4d4d8; --cd-fg-100: hsl(000, 000%, 093%);
--cd-fg-100: #e4e4e7;
/* --cd-prim-bg-100: #e66000; --cd-prim-bg-100: hsl(025, 079%, 052%);
--cd-prim-bg-80: #cc7700; --cd-prim-bg-80: hsl(035, 080%, 043%);
--cd-prim-bg-60: #995900; --cd-prim-bg-60: hsl(045, 080%, 031%);
--cd-prim-fg-60: #ffa666; --cd-prim-fg-80: hsl(025, 080%, 050%);
--cd-prim-fg-80: #ff6a00; --cd-prim-fg-100: hsl(000, 000%, 100%);
--cd-prim-fg-100: #ffffff; */
--cd-prim-bg-100: hsl(267, 90%, 60%); --cd-red-bg-100: hsl(000, 100%, 015%);
--cd-prim-bg-80: hsl(267, 90%, 45%); --cd-red-fg-100: hsl(000, 100%, 060%);
--cd-prim-bg-60: hsl(269, 90%, 30%); --cd-green-fg-100: hsl(120, 080%, 040%);
--cd-prim-fg-60: hsl(267, 90%, 45%);
--cd-prim-fg-80: hsl(267, 90%, 60%);
--cd-prim-fg-100: #ffffff;
--cd-red-bg-100: #4d0000;
--cd-red-fg-100: #ff334b;
--cd-green-fg-100: #22c55e;
/* Import overrides */ /* Import overrides */
--toastify-color-dark: var(--cd-bg-60); --toastify-color-dark: var(--cd-bg-60);
@ -148,7 +136,6 @@
.clr-footer, .clr-footer,
.clr-modal-backdrop, .clr-modal-backdrop,
.clr-btn-nav, .clr-btn-nav,
.clr-checkbox,
.clr-input:disabled .clr-input:disabled
) { ) {
background-color: var(--cl-bg-100); background-color: var(--cl-bg-100);
@ -176,8 +163,7 @@
:is(.clr-primary, :is(.clr-primary,
.clr-btn-primary:hover, .clr-btn-primary:hover,
.clr-btn-primary:focus, .clr-btn-primary:focus
.clr-checkbox:checked
) { ) {
color: var(--cl-prim-fg-100); color: var(--cl-prim-fg-100);
background-color: var(--cl-prim-bg-100); background-color: var(--cl-prim-bg-100);
@ -202,10 +188,10 @@
.clr-btn-default, .clr-btn-default,
.clr-btn-primary .clr-btn-primary
):disabled { ):disabled {
color: var(--cl-fg-60); color: var(--cl-fg-80);
background-color: var(--cl-bg-60); background-color: var(--cl-bg-60);
.dark & { .dark & {
color: var(--cd-fg-60); color: var(--cd-fg-80);
background-color: var(--cd-bg-60); background-color: var(--cd-bg-60);
} }
} }
@ -230,9 +216,9 @@
):focus { ):focus {
outline-width: 2px; outline-width: 2px;
outline-style: solid; outline-style: solid;
outline-color: var(--cl-bg-40); outline-color: var(--cl-prim-bg-100);
.dark & { .dark & {
outline-color: var(--cd-bg-40); outline-color: var(--cd-prim-bg-100);
} }
} }
@ -246,28 +232,28 @@
} }
.clr-footer { .clr-footer {
color: var(--cl-fg-40); color: var(--cl-fg-60);
.dark & { .dark & {
color: var(--cd-fg-40); color: var(--cd-fg-60);
} }
} }
.clr-btn-nav { .clr-btn-nav {
color: var(--cl-fg-60); color: var(--cl-fg-80);
.dark & { .dark & {
color: var(--cd-fg-60); color: var(--cd-fg-80);
} }
} }
.clr-btn-clear { .clr-btn-clear {
color: var(--cl-fg-60); color: var(--cl-fg-80);
&:disabled { &:disabled {
color: var(--cl-fg-40); color: var(--cl-fg-60);
} }
.dark & { .dark & {
color: var(--cd-fg-60); color: var(--cd-fg-80);
&:disabled { &:disabled {
color: var(--cd-fg-40); color: var(--cd-fg-60);
} }
} }
} }
@ -303,10 +289,10 @@
} }
.cm-editor.cm-focused { .cm-editor.cm-focused {
border-color: var(--cl-bg-40); border-color: var(--cl-bg-40);
outline-color: var(--cl-bg-40); outline-color: var(--cl-prim-bg-100);
.dark & { .dark & {
border-color: var(--cd-bg-40); border-color: var(--cd-bg-40);
outline-color: var(--cd-bg-40); outline-color: var(--cd-prim-bg-100);
} }
@apply border shadow rounded outline-2 outline @apply border shadow rounded outline-2 outline
} }
@ -314,22 +300,3 @@
.rdt_TableCell{ .rdt_TableCell{
font-size: 0.875rem; font-size: 0.875rem;
} }
.react-dropdown-select-item {
color: var(--cl-fg-100);
border-color: var(--cl-bg-40);
background-color: var(--cl-bg-100);
&:hover {
background-color: var(--cl-prim-bg-60);
}
.dark & {
color: var(--cd-fg-100);
border-color: var(--cd-bg-40);
background-color: var(--cd-bg-100);
&:hover {
background-color: var(--cd-prim-bg-60);
}
}
}

View File

@ -1,18 +1,21 @@
import { useEffect, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import BackendError from '../components/BackendError'; import BackendError from '../components/BackendError';
import Button from '../components/Common/Button'; import Button from '../components/Common/Button';
import Checkbox from '../components/Common/Checkbox'; import Checkbox from '../components/Common/Checkbox';
import FileInput from '../components/Common/FileInput';
import Form from '../components/Common/Form'; import Form from '../components/Common/Form';
import Label from '../components/Common/Label';
import MiniButton from '../components/Common/MiniButton';
import SubmitButton from '../components/Common/SubmitButton'; import SubmitButton from '../components/Common/SubmitButton';
import TextArea from '../components/Common/TextArea'; import TextArea from '../components/Common/TextArea';
import TextInput from '../components/Common/TextInput'; import TextInput from '../components/Common/TextInput';
import { UploadIcon } from '../components/Icons';
import RequireAuth from '../components/RequireAuth'; import RequireAuth from '../components/RequireAuth';
import { useLibrary } from '../context/LibraryContext'; import { useLibrary } from '../context/LibraryContext';
import { useConceptNavigation } from '../context/NagivationContext'; import { useConceptNavigation } from '../context/NagivationContext';
import { EXTEOR_TRS_FILE } from '../utils/constants';
import { IRSFormCreateData, LibraryItemType } from '../utils/models'; import { IRSFormCreateData, LibraryItemType } from '../utils/models';
function CreateRSFormPage() { function CreateRSFormPage() {
@ -24,20 +27,15 @@ function CreateRSFormPage() {
const [alias, setAlias] = useState(''); const [alias, setAlias] = useState('');
const [comment, setComment] = useState(''); const [comment, setComment] = useState('');
const [common, setCommon] = useState(false); const [common, setCommon] = useState(false);
const [file, setFile] = useState<File | undefined>()
const [fileName, setFileName] = useState('');
const [file, setFile] = useState<File | undefined>();
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => { useEffect(() => {
setError(undefined); setError(undefined);
}, [title, alias, setError]); }, [title, alias, setError]);
function handleFile(event: React.ChangeEvent<HTMLInputElement>) {
if (event.target.files && event.target.files.length > 0) {
setFile(event.target.files[0]);
} else {
setFile(undefined);
}
}
function handleCancel() { function handleCancel() {
if (location.key !== 'default') { if (location.key !== 'default') {
navigateHistory(-1); navigateHistory(-1);
@ -67,12 +65,39 @@ function CreateRSFormPage() {
}); });
} }
function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
if (event.target.files && event.target.files.length > 0) {
setFileName(event.target.files[0].name);
setFile(event.target.files[0]);
} else {
setFileName('');
setFile(undefined);
}
}
return ( return (
<RequireAuth> <RequireAuth>
<Form title='Создание концептуальной схемы' <Form title='Создание концептуальной схемы'
onSubmit={handleSubmit} onSubmit={handleSubmit}
widthClass='max-w-lg w-full mt-4' widthClass='max-w-lg w-full mt-4'
> >
<div className='relative w-full'>
<div className='absolute top-[-2.4rem] right-[-1rem] flex'>
<input
type='file'
ref={inputRef}
style={{ display: 'none' }}
accept={EXTEOR_TRS_FILE}
onChange={handleFileChange}
/>
<MiniButton
tooltip='Загрузить из Экстеор'
icon={<UploadIcon size={5} color='text-primary'/>}
onClick={() => inputRef.current?.click()}
/>
</div>
</div>
{ fileName && <Label text={`Загружен файл: ${fileName}`} />}
<TextInput id='title' label='Полное название' type='text' <TextInput id='title' label='Полное название' type='text'
required={!file} required={!file}
placeholder={file && 'Загрузить из файла'} placeholder={file && 'Загрузить из файла'}
@ -93,13 +118,8 @@ function CreateRSFormPage() {
/> />
<Checkbox id='common' label='Общедоступная схема' <Checkbox id='common' label='Общедоступная схема'
value={common} value={common}
onChange={event => setCommon(event.target.checked)} setValue={value => setCommon(value ?? false)}
/> />
<FileInput id='trs' label='Загрузить из Экстеор'
acceptType='.trs'
onChange={handleFile}
/>
<div className='flex items-center justify-center gap-4 py-2 mt-4'> <div className='flex items-center justify-center gap-4 py-2 mt-4'>
<SubmitButton <SubmitButton
text='Создать схему' text='Создать схему'

View File

@ -36,38 +36,38 @@ function PickerStrategy({ value, onChange }: PickerStrategyProps) {
{ pickerMenu.isActive && { pickerMenu.isActive &&
<Dropdown> <Dropdown>
<DropdownCheckbox <DropdownCheckbox
onChange={() => handleChange(LibraryFilterStrategy.MANUAL)} setValue={() => handleChange(LibraryFilterStrategy.MANUAL)}
value={value === LibraryFilterStrategy.MANUAL} value={value === LibraryFilterStrategy.MANUAL}
label='Отображать все' label='Отображать все'
/> />
<DropdownCheckbox <DropdownCheckbox
onChange={() => handleChange(LibraryFilterStrategy.COMMON)} setValue={() => handleChange(LibraryFilterStrategy.COMMON)}
value={value === LibraryFilterStrategy.COMMON} value={value === LibraryFilterStrategy.COMMON}
label='Общедоступные' label='Общедоступные'
tooltip='Отображать только общедоступные схемы' tooltip='Отображать только общедоступные схемы'
/> />
<DropdownCheckbox <DropdownCheckbox
onChange={() => handleChange(LibraryFilterStrategy.CANONICAL)} setValue={() => handleChange(LibraryFilterStrategy.CANONICAL)}
value={value === LibraryFilterStrategy.CANONICAL} value={value === LibraryFilterStrategy.CANONICAL}
label='Неизменные' label='Неизменные'
tooltip='Отображать только стандартные схемы' tooltip='Отображать только стандартные схемы'
/> />
<DropdownCheckbox <DropdownCheckbox
onChange={() => handleChange(LibraryFilterStrategy.PERSONAL)} setValue={() => handleChange(LibraryFilterStrategy.PERSONAL)}
value={value === LibraryFilterStrategy.PERSONAL} value={value === LibraryFilterStrategy.PERSONAL}
label='Личные' label='Личные'
disabled={!user} disabled={!user}
tooltip='Отображать только подписки и владеемые схемы' tooltip='Отображать только подписки и владеемые схемы'
/> />
<DropdownCheckbox <DropdownCheckbox
onChange={() => handleChange(LibraryFilterStrategy.SUBSCRIBE)} setValue={() => handleChange(LibraryFilterStrategy.SUBSCRIBE)}
value={value === LibraryFilterStrategy.SUBSCRIBE} value={value === LibraryFilterStrategy.SUBSCRIBE}
label='Подписки' label='Подписки'
disabled={!user} disabled={!user}
tooltip='Отображать только подписки' tooltip='Отображать только подписки'
/> />
<DropdownCheckbox <DropdownCheckbox
onChange={() => handleChange(LibraryFilterStrategy.OWNED)} setValue={() => handleChange(LibraryFilterStrategy.OWNED)}
value={value === LibraryFilterStrategy.OWNED} value={value === LibraryFilterStrategy.OWNED}
disabled={!user} disabled={!user}
label='Я - Владелец!' label='Я - Владелец!'

View File

@ -79,7 +79,7 @@ function DlgCloneRSForm({ hideWindow }: DlgCloneRSFormProps) {
/> />
<Checkbox id='common' label='Общедоступная схема' <Checkbox id='common' label='Общедоступная схема'
value={common} value={common}
onChange={event => setCommon(event.target.checked)} setValue={value => setCommon(value)}
/> />
</Modal> </Modal>
); );

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import ConceptSelect from '../../components/Common/ConceptSelect'; import ConceptSelectSingle from '../../components/Common/ConceptSelectSingle';
import Modal from '../../components/Common/Modal'; import Modal from '../../components/Common/Modal';
import TextArea from '../../components/Common/TextArea'; import TextArea from '../../components/Common/TextArea';
import RSInput from '../../components/RSInput'; import RSInput from '../../components/RSInput';
@ -57,12 +57,12 @@ function DlgCreateCst({ hideWindow, initial, onCreate }: DlgCreateCstProps) {
> >
<div className='h-fit w-[35rem] px-2 mb-2 flex flex-col justify-stretch'> <div className='h-fit w-[35rem] px-2 mb-2 flex flex-col justify-stretch'>
<div className='flex justify-center w-full'> <div className='flex justify-center w-full'>
<ConceptSelect <ConceptSelectSingle
className='my-2 min-w-[15rem] self-center' className='my-2 min-w-[15rem] self-center'
options={CstTypeSelector} options={CstTypeSelector}
placeholder='Выберите тип' placeholder='Выберите тип'
values={selectedType ? [{ value: selectedType, label: getCstTypeLabel(selectedType) }] : []} value={selectedType ? { value: selectedType, label: getCstTypeLabel(selectedType) } : null}
onChange={data => setSelectedType(data.length > 0 ? data[0].value : CstType.BASE)} onChange={data => setSelectedType(data?.value ?? CstType.BASE)}
/> />
</div> </div>
<TextArea id='term' label='Термин' <TextArea id='term' label='Термин'
@ -72,12 +72,14 @@ function DlgCreateCst({ hideWindow, initial, onCreate }: DlgCreateCstProps) {
spellCheck spellCheck
onChange={event => setTerm(event.target.value)} onChange={event => setTerm(event.target.value)}
/> />
<div className='mt-3'>
<RSInput id='expression' label='Формальное выражение' <RSInput id='expression' label='Формальное выражение'
editable editable
height='5.5rem' height='5.5rem'
value={expression} value={expression}
onChange={value => setExpression(value)} onChange={value => setExpression(value)}
/> />
</div>
<TextArea id='definition' label='Текстовое определение' <TextArea id='definition' label='Текстовое определение'
placeholder='Лингвистическая интерпретация формального выражения' placeholder='Лингвистическая интерпретация формального выражения'
rows={2} rows={2}

View File

@ -52,7 +52,7 @@ function DlgDeleteCst({ hideWindow, selected, onDelete }: DlgDeleteCstProps) {
<Checkbox <Checkbox
label='Удалить зависимые конституенты' label='Удалить зависимые конституенты'
value={expandOut} value={expandOut}
onChange={data => setExpandOut(data.target.checked)} setValue={value => setExpandOut(value)}
/> />
</div> </div>
</Modal> </Modal>

View File

@ -81,25 +81,25 @@ function DlgGraphOptions({ hideWindow, initial, onConfirm }:DlgGraphOptionsProps
label='Скрыть текст' label='Скрыть текст'
tooltip='Не отображать термины' tooltip='Не отображать термины'
value={noTerms} value={noTerms}
onChange={ event => setNoTerms(event.target.checked) } setValue={ value => setNoTerms(value) }
/> />
<Checkbox <Checkbox
label='Скрыть несвязанные' label='Скрыть несвязанные'
tooltip='Неиспользуемые конституенты' tooltip='Неиспользуемые конституенты'
value={noHermits} value={noHermits}
onChange={ event => setNoHermits(event.target.checked) } setValue={ value => setNoHermits(value) }
/> />
<Checkbox <Checkbox
label='Скрыть шаблоны' label='Скрыть шаблоны'
tooltip='Терм-функции и предикат-функции с параметризованными аргументами' tooltip='Терм-функции и предикат-функции с параметризованными аргументами'
value={noTemplates} value={noTemplates}
onChange={ event => setNoTemplates(event.target.checked) } setValue={ value => setNoTemplates(value) }
/> />
<Checkbox <Checkbox
label='Транзитивная редукция' label='Транзитивная редукция'
tooltip='Удалить связи, образующие транзитивные пути в графе' tooltip='Удалить связи, образующие транзитивные пути в графе'
value={noTransitive} value={noTransitive}
onChange={ event => setNoTransitive(event.target.checked) } setValue={ value => setNoTransitive(value) }
/> />
</div> </div>
<div className='flex flex-col'> <div className='flex flex-col'>
@ -107,42 +107,42 @@ function DlgGraphOptions({ hideWindow, initial, onConfirm }:DlgGraphOptionsProps
<Checkbox <Checkbox
label={getCstTypeLabel(CstType.BASE)} label={getCstTypeLabel(CstType.BASE)}
value={allowBase} value={allowBase}
onChange={ event => setAllowBase(event.target.checked) } setValue={ value => setAllowBase(value) }
/> />
<Checkbox <Checkbox
label={getCstTypeLabel(CstType.STRUCTURED)} label={getCstTypeLabel(CstType.STRUCTURED)}
value={allowStruct} value={allowStruct}
onChange={ event => setAllowStruct(event.target.checked) } setValue={ value => setAllowStruct(value) }
/> />
<Checkbox <Checkbox
label={getCstTypeLabel(CstType.TERM)} label={getCstTypeLabel(CstType.TERM)}
value={allowTerm} value={allowTerm}
onChange={ event => setAllowTerm(event.target.checked) } setValue={ value => setAllowTerm(value) }
/> />
<Checkbox <Checkbox
label={getCstTypeLabel(CstType.AXIOM)} label={getCstTypeLabel(CstType.AXIOM)}
value={allowAxiom} value={allowAxiom}
onChange={ event => setAllowAxiom(event.target.checked) } setValue={ value => setAllowAxiom(value) }
/> />
<Checkbox <Checkbox
label={getCstTypeLabel(CstType.FUNCTION)} label={getCstTypeLabel(CstType.FUNCTION)}
value={allowFunction} value={allowFunction}
onChange={ event => setAllowFunction(event.target.checked) } setValue={ value => setAllowFunction(value) }
/> />
<Checkbox <Checkbox
label={getCstTypeLabel(CstType.PREDICATE)} label={getCstTypeLabel(CstType.PREDICATE)}
value={allowPredicate} value={allowPredicate}
onChange={ event => setAllowPredicate(event.target.checked) } setValue={ value => setAllowPredicate(value) }
/> />
<Checkbox <Checkbox
label={getCstTypeLabel(CstType.CONSTANT)} label={getCstTypeLabel(CstType.CONSTANT)}
value={allowConstant} value={allowConstant}
onChange={ event => setAllowConstant(event.target.checked) } setValue={ value => setAllowConstant(value) }
/> />
<Checkbox <Checkbox
label={getCstTypeLabel(CstType.THEOREM)} label={getCstTypeLabel(CstType.THEOREM)}
value={allowTheorem} value={allowTheorem}
onChange={ event => setAllowTheorem(event.target.checked) } setValue ={ value => setAllowTheorem(value) }
/> />
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
import { useLayoutEffect, useState } from 'react'; import { useLayoutEffect, useState } from 'react';
import ConceptSelect from '../../components/Common/ConceptSelect'; import ConceptSelectSingle from '../../components/Common/ConceptSelectSingle';
import Modal from '../../components/Common/Modal'; import Modal from '../../components/Common/Modal';
import TextInput from '../../components/Common/TextInput'; import TextInput from '../../components/Common/TextInput';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
@ -67,12 +67,12 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
submitText='Переименовать' submitText='Переименовать'
> >
<div className='flex items-center gap-4 px-2 my-2 h-fit min-w-[25rem]'> <div className='flex items-center gap-4 px-2 my-2 h-fit min-w-[25rem]'>
<ConceptSelect <ConceptSelectSingle
className='min-w-[14rem] self-center' className='min-w-[14rem] self-center'
options={CstTypeSelector} options={CstTypeSelector}
placeholder='Выберите тип' placeholder='Выберите тип'
values={cstType ? [{ value: cstType, label: getCstTypeLabel(cstType) }] : []} value={cstType ? { value: cstType, label: getCstTypeLabel(cstType) } : null}
onChange={data => setCstType(data.length > 0 ? data[0].value : CstType.BASE)} onChange={data => setCstType(data?.value ?? CstType.BASE)}
/> />
<div> <div>
<TextInput id='alias' label='Имя' <TextInput id='alias' label='Имя'

View File

@ -5,6 +5,7 @@ import Checkbox from '../../components/Common/Checkbox';
import FileInput from '../../components/Common/FileInput'; import FileInput from '../../components/Common/FileInput';
import Modal from '../../components/Common/Modal'; import Modal from '../../components/Common/Modal';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { EXTEOR_TRS_FILE } from '../../utils/constants';
import { IRSFormUploadData } from '../../utils/models'; import { IRSFormUploadData } from '../../utils/models';
interface DlgUploadRSFormProps { interface DlgUploadRSFormProps {
@ -44,16 +45,17 @@ function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
onSubmit={handleSubmit} onSubmit={handleSubmit}
submitText='Загрузить' submitText='Загрузить'
> >
<div className='max-w-[20rem]'> <div className='flex flex-col items-center'>
<FileInput <FileInput
label='Выбрать файл' label='Выбрать файл'
acceptType='.trs' acceptType={EXTEOR_TRS_FILE}
onChange={handleFile} onChange={handleFile}
/> />
<Checkbox <Checkbox
label='Загружать название и комментарий' label='Загружать название и комментарий'
value={loadMetadata} value={loadMetadata}
onChange={event => setLoadMetadata(event.target.checked)} setValue={value => setLoadMetadata(value)}
widthClass='w-fit pb-2'
/> />
</div> </div>
</Modal> </Modal>

View File

@ -136,14 +136,14 @@ function EditorRSForm({ onDestroy, onClaim, onShare, isModified, setIsModified,
value={common} value={common}
widthClass='w-fit mt-3' widthClass='w-fit mt-3'
disabled={!isEditable} disabled={!isEditable}
onChange={event => setCommon(event.target.checked)} setValue={value => setCommon(value)}
/> />
<Checkbox id='canonical' label='Неизменная схема' <Checkbox id='canonical' label='Неизменная схема'
widthClass='w-fit' widthClass='w-fit'
value={canonical} value={canonical}
tooltip='Только администраторы могут присваивать схемам неизменный статус' tooltip='Только администраторы могут присваивать схемам неизменный статус'
disabled={!isEditable || !isForceAdmin} disabled={!isEditable || !isForceAdmin}
onChange={event => setCanonical(event.target.checked)} setValue={value => setCanonical(value)}
/> />
</div> </div>

View File

@ -5,7 +5,7 @@ import { GraphCanvas, GraphCanvasRef, GraphEdge,
import Button from '../../components/Common/Button'; import Button from '../../components/Common/Button';
import Checkbox from '../../components/Common/Checkbox'; import Checkbox from '../../components/Common/Checkbox';
import ConceptSelect from '../../components/Common/ConceptSelect'; import ConceptSelectSingle from '../../components/Common/ConceptSelectSingle';
import ConceptTooltip from '../../components/Common/ConceptTooltip'; import ConceptTooltip from '../../components/Common/ConceptTooltip';
import Divider from '../../components/Common/Divider'; import Divider from '../../components/Common/Divider';
import MiniButton from '../../components/Common/MiniButton'; import MiniButton from '../../components/Common/MiniButton';
@ -16,7 +16,7 @@ import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import useLocalStorage from '../../hooks/useLocalStorage'; import useLocalStorage from '../../hooks/useLocalStorage';
import { graphDarkT, graphLightT, IColorTheme } from '../../utils/color'; import { graphDarkT, graphLightT, IColorTheme } from '../../utils/color';
import { prefixes, resources } from '../../utils/constants'; import { prefixes, resources, TIMEOUT_GRAPH_REFRESH } from '../../utils/constants';
import { Graph } from '../../utils/Graph'; import { Graph } from '../../utils/Graph';
import { CstType, IConstituenta, ICstCreateData } from '../../utils/models'; import { CstType, IConstituenta, ICstCreateData } from '../../utils/models';
import { getCstClassColor, getCstStatusColor, import { getCstClassColor, getCstStatusColor,
@ -220,9 +220,9 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
}, [selectedDismissed, selections]); }, [selectedDismissed, selections]);
const nothingSelected = useMemo(() => allSelected.length === 0, [allSelected]); const nothingSelected = useMemo(() => allSelected.length === 0, [allSelected]);
const handleRecreate = useCallback( const handleResetViewpoint = useCallback(
() => { () => {
graphRef.current?.resetControls(); graphRef.current?.resetControls(true);
graphRef.current?.centerGraph(); graphRef.current?.centerGraph();
}, []); }, []);
@ -293,6 +293,16 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
}); });
} }
function handleChangeLayout(newLayout: LayoutTypes) {
if (newLayout === layout) {
return;
}
setLayout(newLayout);
setTimeout(() => {
handleResetViewpoint();
}, TIMEOUT_GRAPH_REFRESH);
}
function getOptions() { function getOptions() {
return { return {
noHermits: noHermits, noHermits: noHermits,
@ -394,39 +404,39 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
widthClass='h-full' widthClass='h-full'
onClick={() => setShowOptions(true)} onClick={() => setShowOptions(true)}
/> />
<ConceptSelect <ConceptSelectSingle
className='min-w-[9.8rem]' className='min-w-[9.8rem]'
options={GraphColoringSelector} options={GraphColoringSelector}
searchable={false} isSearchable={false}
placeholder='Выберите цвет' placeholder='Выберите цвет'
values={coloringScheme ? [{ value: coloringScheme, label: mapColoringLabels.get(coloringScheme) }] : []} value={coloringScheme ? { value: coloringScheme, label: mapColoringLabels.get(coloringScheme) } : null}
onChange={data => setColoringScheme(data.length > 0 ? data[0].value : GraphColoringSelector[0].value)} onChange={data => setColoringScheme(data?.value ?? GraphColoringSelector[0].value)}
/> />
</div> </div>
<ConceptSelect <ConceptSelectSingle
className='w-full mt-1' className='w-full mt-1'
options={GraphLayoutSelector} options={GraphLayoutSelector}
searchable={false} isSearchable={false}
placeholder='Способ расположения' placeholder='Способ расположения'
values={layout ? [{ value: layout, label: mapLayoutLabels.get(layout) }] : []} value={layout ? { value: layout, label: mapLayoutLabels.get(layout) } : null}
onChange={data => setLayout(data.length > 0 ? data[0].value : GraphLayoutSelector[0].value)} onChange={data => handleChangeLayout(data?.value ?? GraphLayoutSelector[0].value)}
/> />
<Checkbox <Checkbox
label='Скрыть текст' label='Скрыть текст'
value={noTerms} value={noTerms}
onChange={ event => setNoTerms(event.target.checked) } setValue={ value => setNoTerms(value) }
/> />
<Checkbox <Checkbox
label='Транзитивная редукция' label='Транзитивная редукция'
value={noTransitive} value={noTransitive}
onChange={ event => setNoTransitive(event.target.checked) } setValue={ value => setNoTransitive(value) }
/> />
<Checkbox <Checkbox
disabled={!is3D} disabled={!is3D}
label='Анимация вращения' label='Анимация вращения'
value={orbit} value={orbit}
onChange={ event => setOrbit(event.target.checked) } setValue={ value => setOrbit(value) }
/> />
<Divider margins='mt-3 mb-2' /> <Divider margins='mt-3 mb-2' />
@ -471,8 +481,8 @@ function EditorTermGraph({ onOpenEdit, onCreateCst, onDeleteCst }: EditorTermGra
</div> </div>
<MiniButton <MiniButton
icon={<ArrowsRotateIcon size={5} />} icon={<ArrowsRotateIcon size={5} />}
tooltip='Пересоздать граф' tooltip='Восстановить камеру'
onClick={handleRecreate} onClick={handleResetViewpoint}
/> />
</div> </div>
<ConceptTooltip anchorSelect='#items-graph-help'> <ConceptTooltip anchorSelect='#items-graph-help'>

View File

@ -14,7 +14,7 @@ import { useConceptNavigation } from '../../context/NagivationContext';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
import useModificationPrompt from '../../hooks/useModificationPrompt'; import useModificationPrompt from '../../hooks/useModificationPrompt';
import { prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants'; import { EXTEOR_TRS_FILE, prefixes, TIMEOUT_UI_REFRESH } from '../../utils/constants';
import { ICstCreateData, ICstRenameData, SyntaxTree } from '../../utils/models'; import { ICstCreateData, ICstRenameData, SyntaxTree } from '../../utils/models';
import { createAliasFor } from '../../utils/staticUI'; import { createAliasFor } from '../../utils/staticUI';
import DlgCloneRSForm from './DlgCloneRSForm'; import DlgCloneRSForm from './DlgCloneRSForm';
@ -256,7 +256,7 @@ function RSTabs() {
return; return;
} }
} }
const fileName = (schema?.alias ?? 'Schema') + '.trs'; const fileName = (schema?.alias ?? 'Schema') + EXTEOR_TRS_FILE;
download( download(
(data) => { (data) => {
try { try {

View File

@ -146,14 +146,14 @@ function RSTabsMenu({
{(isOwned || user?.is_staff) && {(isOwned || user?.is_staff) &&
<DropdownCheckbox <DropdownCheckbox
value={isReadonly} value={isReadonly}
onChange={toggleReadonly} setValue={toggleReadonly}
label='Я — читатель!' label='Я — читатель!'
tooltip='Режим чтения' tooltip='Режим чтения'
/>} />}
{user?.is_staff && {user?.is_staff &&
<DropdownCheckbox <DropdownCheckbox
value={isForceAdmin} value={isForceAdmin}
onChange={toggleForceAdmin} setValue={toggleForceAdmin}
label='Я — администратор!' label='Я — администратор!'
tooltip='Режим редактирования для администраторов' tooltip='Режим редактирования для администраторов'
/>} />}

View File

@ -1,70 +1,107 @@
export interface IColorTheme { // =========== Modules contains all dynamic color definitions ==========
red: string
green: string
blue: string
teal: string
orange: string
// ============= MAIN COLOR THEMES ==========
export interface IColorTheme {
bgDefault: string bgDefault: string
bgInput: string bgInput: string
bgControls: string bgControls: string
bgDisabled: string bgDisabled: string
bgHover: string bgPrimary: string
bgSelected: string bgSelected: string
bgHover: string
bgWarning: string bgWarning: string
border: string border: string
fgDefault: string fgDefault: string
fgSelected: string
fgDisabled: string fgDisabled: string
fgWarning: string fgWarning: string
// Hightlight syntax accents
bgRed: string
bgGreen: string
bgBlue: string
bgPurple: string
bgTeal: string
bgOrange: string
fgRed: string
fgGreen: string
fgBlue: string
fgPurple: string
fgTeal: string
fgOrange: string
} }
// =========== GENERAL THEMES ========= // ======= Light =======
export const lightT: IColorTheme = { export const lightT: IColorTheme = {
red: '#ffc9c9',
green: '#aaff80',
blue: '#b3bdff',
teal: '#a5e9fa',
orange: '#ffbb80',
bgDefault: 'var(--cl-bg-100)', bgDefault: 'var(--cl-bg-100)',
bgInput: 'var(--cl-bg-120)', bgInput: 'var(--cl-bg-120)',
bgControls: 'var(--cl-bg-80)', bgControls: 'var(--cl-bg-80)',
bgDisabled: 'var(--cl-bg-60)', bgDisabled: 'var(--cl-bg-60)',
bgHover: 'var(--cl-prim-bg-60)', bgPrimary: 'var(--cl-prim-bg-100)',
bgSelected: 'var(--cl-prim-bg-80)', bgSelected: 'var(--cl-prim-bg-80)',
bgHover: 'var(--cl-prim-bg-60)',
bgWarning: 'var(--cl-red-bg-100)', bgWarning: 'var(--cl-red-bg-100)',
border: 'var(--cl-bg-40)', border: 'var(--cl-bg-40)',
fgDefault: 'var(--cl-fg-100)', fgDefault: 'var(--cl-fg-100)',
fgDisabled: 'var(--cl-fg-60)', fgSelected: 'var(--cl-fg-100)',
fgWarning: 'var(--cl-red-fg-100)' fgDisabled: 'var(--cl-fg-80)',
fgWarning: 'var(--cl-red-fg-100)',
// Hightlight syntax accents
bgRed: 'hsl(000, 100%, 089%)',
bgGreen: 'hsl(100, 100%, 075%)',
bgBlue: 'hsl(235, 100%, 085%)',
bgPurple: 'hsl(274, 089%, 081%)',
bgTeal: 'hsl(192, 089%, 081%)',
bgOrange: 'hsl(028, 100%, 075%)',
fgRed: 'hsl(000, 090%, 045%)',
fgGreen: 'hsl(100, 090%, 035%)',
fgBlue: 'hsl(235, 100%, 050%)',
fgPurple: 'hsl(270, 100%, 070%)',
fgTeal: 'hsl(192, 090%, 040%)',
fgOrange: 'hsl(030, 090%, 055%)'
}; };
// ======= DARK ========
export const darkT: IColorTheme = { export const darkT: IColorTheme = {
red: '#bf0d00',
green: '#2b8000',
blue: '#394bbf',
teal: '#007a99',
orange: '#964600',
bgDefault: 'var(--cd-bg-100)', bgDefault: 'var(--cd-bg-100)',
bgInput: 'var(--cd-bg-120)', bgInput: 'var(--cd-bg-120)',
bgControls: 'var(--cd-bg-80)', bgControls: 'var(--cd-bg-80)',
bgDisabled: 'var(--cd-bg-60)', bgDisabled: 'var(--cd-bg-60)',
bgHover: 'var(--cd-prim-bg-60)', bgPrimary: 'var(--cd-prim-bg-100)',
bgSelected: 'var(--cd-prim-bg-80)', bgSelected: 'var(--cd-prim-bg-80)',
bgHover: 'var(--cd-prim-bg-60)',
bgWarning: 'var(--cd-red-bg-100)', bgWarning: 'var(--cd-red-bg-100)',
border: 'var(--cd-bg-40)', border: 'var(--cd-bg-40)',
fgDefault: 'var(--cd-fg-100)', fgDefault: 'var(--cd-fg-100)',
fgDisabled: 'var(--cd-fg-60)', fgSelected: 'var(--cd-fg-100)',
fgWarning: 'var(--cd-red-fg-100)' fgDisabled: 'var(--cd-fg-80)',
}; fgWarning: 'var(--cd-red-fg-100)',
// Hightlight syntax accents
bgRed: 'hsl(000, 080%, 037%)',
bgGreen: 'hsl(100, 080%, 025%)',
bgBlue: 'hsl(235, 054%, 049%)',
bgPurple: 'hsl(270, 080%, 050%)',
bgTeal: 'hsl(192, 080%, 030%)',
bgOrange: 'hsl(035, 100%, 035%)',
fgRed: 'hsl(000, 080%, 050%)',
fgGreen: 'hsl(100, 080%, 040%)',
fgBlue: 'hsl(235, 100%, 080%)',
fgPurple: 'hsl(270, 100%, 080%)',
fgTeal: 'hsl(192, 100%, 030%)',
fgOrange: 'hsl(035, 100%, 050%)'
};
// ========= DATA TABLE THEMES ======== // ========= DATA TABLE THEMES ========
export const dataTableLightT = { export const dataTableLightT = {
@ -119,6 +156,51 @@ export const dataTableDarkT = {
} }
}; };
// ============ SELECT THEMES ==========
export const selectLightT = {
primary: lightT.bgPrimary,
primary75: lightT.bgSelected,
primary50: lightT.bgHover,
primary25: lightT.bgHover,
danger: lightT.fgWarning,
dangerLight: lightT.bgWarning,
neutral0: lightT.bgInput,
neutral5: lightT.bgDefault,
neutral10: lightT.border,
neutral20: lightT.border,
neutral30: lightT.border,
neutral40: lightT.fgDisabled,
neutral50: lightT.fgWarning,
neutral60: lightT.fgDefault,
neutral70: lightT.fgWarning,
neutral80: lightT.fgDefault,
neutral90: lightT.fgWarning
}
export const selectDarkT = {
primary: darkT.bgPrimary,
primary75: darkT.bgSelected,
primary50: darkT.bgHover,
primary25: darkT.bgHover,
danger: darkT.fgWarning,
dangerLight: darkT.bgWarning,
neutral0: darkT.bgInput,
neutral5: darkT.bgDefault,
neutral10: darkT.border,
neutral20: darkT.border,
neutral30: darkT.border,
neutral40: darkT.fgDisabled,
neutral50: darkT.fgWarning,
neutral60: darkT.fgDefault,
neutral70: darkT.fgWarning,
neutral80: darkT.fgDefault,
neutral90: darkT.fgWarning
}
// ============ GRAPH THEMES ========== // ============ GRAPH THEMES ==========
export const graphLightT = { export const graphLightT = {
canvas: { canvas: {
@ -221,20 +303,22 @@ export const graphDarkT = {
// ======== Bracket Matching Themes =========== // ======== Bracket Matching Themes ===========
export const bracketsLightT = { export const bracketsLightT = {
'.cc-nonmatchingBracket': { '.cc-nonmatchingBracket': {
color: lightT.fgWarning, color: lightT.fgRed,
fontWeight: 700, fontWeight: 700,
}, },
'&.cm-focused .cc-matchingBracket': { '&.cm-focused .cc-matchingBracket': {
backgroundColor: lightT.bgSelected, backgroundColor: lightT.bgSelected,
color: lightT.fgSelected
}, },
}; };
export const bracketsDarkT = { export const bracketsDarkT = {
'.cc-nonmatchingBracket': { '.cc-nonmatchingBracket': {
color: darkT.fgWarning, color: darkT.fgRed,
fontWeight: 700, fontWeight: 700,
}, },
'&.cm-focused .cc-matchingBracket': { '&.cm-focused .cc-matchingBracket': {
backgroundColor: darkT.bgSelected, backgroundColor: darkT.bgSelected,
color: darkT.fgSelected
}, },
}; };

View File

@ -3,6 +3,9 @@ export const config = {
backend: import.meta.env.VITE_PORTAL_BACKEND as string backend: import.meta.env.VITE_PORTAL_BACKEND as string
}; };
export const TIMEOUT_UI_REFRESH = 100; export const TIMEOUT_UI_REFRESH = 100;
export const TIMEOUT_GRAPH_REFRESH = 200;
export const EXTEOR_TRS_FILE = '.trs';
export const youtube = { export const youtube = {
intro: '0Ty9mu9sOJo' intro: '0Ty9mu9sOJo'

View File

@ -315,12 +315,12 @@ export const GraphColoringSelector: {value: ColoringScheme, label: string}[] = [
export function getCstStatusColor(status: ExpressionStatus, colors: IColorTheme): string { export function getCstStatusColor(status: ExpressionStatus, colors: IColorTheme): string {
switch (status) { switch (status) {
case ExpressionStatus.VERIFIED: return colors.green; case ExpressionStatus.VERIFIED: return colors.bgGreen;
case ExpressionStatus.INCORRECT: return colors.red; case ExpressionStatus.INCORRECT: return colors.bgRed;
case ExpressionStatus.INCALCULABLE: return colors.orange; case ExpressionStatus.INCALCULABLE: return colors.bgOrange;
case ExpressionStatus.PROPERTY: return colors.teal; case ExpressionStatus.PROPERTY: return colors.bgTeal;
case ExpressionStatus.UNKNOWN: return colors.blue; case ExpressionStatus.UNKNOWN: return colors.bgBlue;
case ExpressionStatus.UNDEFINED: return colors.blue; case ExpressionStatus.UNDEFINED: return colors.bgBlue;
} }
} }
@ -392,10 +392,10 @@ export const mapTopicInfo: Map<HelpTopic, IDescriptor> = new Map([
export function getCstClassColor(cstClass: CstClass, colors: IColorTheme): string { export function getCstClassColor(cstClass: CstClass, colors: IColorTheme): string {
switch (cstClass) { switch (cstClass) {
case CstClass.BASIC: return colors.green; case CstClass.BASIC: return colors.bgGreen;
case CstClass.DERIVED: return colors.blue; case CstClass.DERIVED: return colors.bgBlue;
case CstClass.STATEMENT: return colors.red; case CstClass.STATEMENT: return colors.bgRed;
case CstClass.TEMPLATE: return colors.teal; case CstClass.TEMPLATE: return colors.bgTeal;
} }
} }
@ -684,7 +684,7 @@ export function getASTNodeColor(node: ISyntaxTreeNode, colors: IColorTheme): str
case TokenID.PUNC_DEFINE: case TokenID.PUNC_DEFINE:
case TokenID.PUNC_STRUCT: case TokenID.PUNC_STRUCT:
case TokenID.ID_LOCAL: case TokenID.ID_LOCAL:
return colors.green; return colors.bgGreen;
case TokenID.ID_GLOBAL: case TokenID.ID_GLOBAL:
case TokenID.ID_FUNCTION: case TokenID.ID_FUNCTION:
@ -693,7 +693,7 @@ export function getASTNodeColor(node: ISyntaxTreeNode, colors: IColorTheme): str
case TokenID.LIT_INTEGER: case TokenID.LIT_INTEGER:
case TokenID.LIT_EMPTYSET: case TokenID.LIT_EMPTYSET:
case TokenID.LIT_INTSET: case TokenID.LIT_INTSET:
return colors.teal; return colors.bgTeal;
case TokenID.FORALL: case TokenID.FORALL:
case TokenID.EXISTS: case TokenID.EXISTS:
@ -713,7 +713,7 @@ export function getASTNodeColor(node: ISyntaxTreeNode, colors: IColorTheme): str
case TokenID.SUBSET_OR_EQ: case TokenID.SUBSET_OR_EQ:
case TokenID.SUBSET: case TokenID.SUBSET:
case TokenID.NOTSUBSET: case TokenID.NOTSUBSET:
return colors.orange; return colors.bgOrange;
case TokenID.NT_TUPLE: case TokenID.NT_TUPLE:
case TokenID.NT_ENUMERATION: case TokenID.NT_ENUMERATION:
@ -733,7 +733,7 @@ export function getASTNodeColor(node: ISyntaxTreeNode, colors: IColorTheme): str
case TokenID.CARD: case TokenID.CARD:
case TokenID.BOOL: case TokenID.BOOL:
case TokenID.DEBOOL: case TokenID.DEBOOL:
return colors.blue; return colors.bgBlue;
case TokenID.NT_FUNC_DEFINITION: case TokenID.NT_FUNC_DEFINITION:
case TokenID.NT_DECLARATIVE_EXPR: case TokenID.NT_DECLARATIVE_EXPR:
@ -752,8 +752,8 @@ export function getASTNodeColor(node: ISyntaxTreeNode, colors: IColorTheme): str
case TokenID.PUNC_ASSIGN: case TokenID.PUNC_ASSIGN:
case TokenID.PUNC_ITERATE: case TokenID.PUNC_ITERATE:
return colors.red; return colors.bgRed;
} }
// node // node
return colors.red; return colors.bgRed;
} }

View File

@ -1,7 +1,7 @@
# Create venv and install dependencies + imports # Create venv and install dependencies + imports
$backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend" $backend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\backend"
$frontend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\fronted" $frontend = Resolve-Path -Path "$PSScriptRoot\..\..\rsconcept\frontend"
$envPath = "$backend\venv" $envPath = "$backend\venv"
$python = "$envPath\Scripts\python.exe" $python = "$envPath\Scripts\python.exe"
@ -12,7 +12,7 @@ function LocalDevelopmentSetup() {
function FrontendSetup() { function FrontendSetup() {
Set-Location $frontend Set-Location $frontend
& npm install & npm install --only=dev
} }
function BackendSetup() { function BackendSetup() {

View File

@ -46,4 +46,7 @@ DjangoDump()
EnsureLocation EnsureLocation
PostgreDump PostgreDump
DjangoDump DjangoDump
echo "Backup created at: $destination"
green="\033[0;32m"
noColor='\033[0m'
echo -e "${green}Backup created at: ${destination}${noColor}"

View File

@ -1,3 +1,4 @@
git pull git pull
/bin/bash ./scripts/prod/CreateBackup.sh
docker compose -f "docker-compose-prod.yml" up --build -d docker compose -f "docker-compose-prod.yml" up --build -d
docker image prune -a -f docker image prune -a -f