UI improvements

This commit is contained in:
IRBorisov 2024-04-01 11:13:50 +03:00
parent 852f32e085
commit 99f9bdb856
12 changed files with 222 additions and 97 deletions

View File

@ -1,3 +1,5 @@
import clsx from 'clsx';
import { IConstituenta } from '@/models/rsform'; import { IConstituenta } from '@/models/rsform';
import { labelCstTypification } from '@/utils/labels'; import { labelCstTypification } from '@/utils/labels';
@ -7,9 +9,9 @@ interface InfoConstituentaProps extends CProps.Div {
data: IConstituenta; data: IConstituenta;
} }
function InfoConstituenta({ data, ...restProps }: InfoConstituentaProps) { function InfoConstituenta({ data, className, ...restProps }: InfoConstituentaProps) {
return ( return (
<div {...restProps}> <div className={clsx('dense', className)} {...restProps}>
<h2>Конституента {data.alias}</h2> <h2>Конституента {data.alias}</h2>
<p> <p>
<b>Типизация: </b> <b>Типизация: </b>

View File

@ -14,7 +14,7 @@ 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 dense'>
{header ? <h1>{header}</h1> : null} {header ? <h1>{header}</h1> : null}
{Object.values(CstClass).map((cstClass, index) => { {Object.values(CstClass).map((cstClass, index) => {
return ( return (

View File

@ -14,7 +14,7 @@ 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 dense'>
{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)

View File

@ -7,14 +7,29 @@ import { CProps } from '../props';
interface AnimateFadeProps extends CProps.AnimatedDiv { interface AnimateFadeProps extends CProps.AnimatedDiv {
noFadeIn?: boolean; noFadeIn?: boolean;
noFadeOut?: boolean; noFadeOut?: boolean;
removeContent?: boolean;
hideContent?: boolean;
} }
function AnimateFade({ noFadeIn, noFadeOut, children, ...restProps }: AnimateFadeProps) { function AnimateFade({
style,
noFadeIn,
noFadeOut,
children,
removeContent,
hideContent,
...restProps
}: AnimateFadeProps) {
if (removeContent) {
return null;
}
return ( return (
<motion.div <motion.div
initial={{ ...(!noFadeIn ? animateFade.initial : {}) }} initial={{ ...(!noFadeIn ? animateFade.initial : {}) }}
animate={{ ...animateFade.animate }} animate={hideContent ? 'hidden' : 'active'}
variants={animateFade.variants}
exit={{ ...(!noFadeOut ? animateFade.exit : {}) }} exit={{ ...(!noFadeOut ? animateFade.exit : {}) }}
style={{ display: hideContent ? 'none' : '', ...style }}
{...restProps} {...restProps}
> >
{children} {children}

View File

@ -1,9 +1,9 @@
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import AnimateFade from './AnimateFade';
import InfoError, { ErrorData } from '../info/InfoError'; import InfoError, { ErrorData } from '../info/InfoError';
import { CProps } from '../props'; import { CProps } from '../props';
import Loader from '../ui/Loader'; import Loader from '../ui/Loader';
import AnimateFade from './AnimateFade';
interface DataLoaderProps extends CProps.AnimatedDiv { interface DataLoaderProps extends CProps.AnimatedDiv {
id: string; id: string;
@ -20,16 +20,12 @@ function DataLoader({ id, isLoading, hasNoData, error, children, ...restProps }:
<AnimatePresence mode='wait'> <AnimatePresence mode='wait'>
{isLoading ? <Loader key={`${id}-loader`} /> : null} {isLoading ? <Loader key={`${id}-loader`} /> : null}
{error ? <InfoError key={`${id}-error`} error={error} /> : null} {error ? <InfoError key={`${id}-error`} error={error} /> : null}
{!isLoading && !error && !hasNoData ? ( <AnimateFade id={id} key={`${id}-data`} removeContent={isLoading || !!error || hasNoData} {...restProps}>
<AnimateFade id={id} key={`${id}-data`} {...restProps}>
{children} {children}
</AnimateFade> </AnimateFade>
) : null} <AnimateFade id={id} key={`${id}-data`} removeContent={isLoading || !!error || !hasNoData} {...restProps}>
{!isLoading && !error && hasNoData ? (
<AnimateFade id={id} key={`${id}-data`} {...restProps}>
Данные не загружены Данные не загружены
</AnimateFade> </AnimateFade>
) : null}
</AnimatePresence> </AnimatePresence>
); );
} }

View File

@ -239,7 +239,7 @@ export function applyFilterCategory(start: IConstituenta, schema: IRSFormData):
/** /**
* Prefix for alias indicating {@link CstType}. * Prefix for alias indicating {@link CstType}.
*/ */
export function getCstTypePrefix(type: CstType) { export function getCstTypePrefix(type: CstType): string {
// prettier-ignore // prettier-ignore
switch (type) { switch (type) {
case CstType.BASE: return 'X'; case CstType.BASE: return 'X';
@ -256,7 +256,7 @@ export function getCstTypePrefix(type: CstType) {
/** /**
* Guess {@link CstType} from user input hint. * Guess {@link CstType} from user input hint.
*/ */
export function guessCstType(hint: string, defaultType: CstType = CstType.TERM) { export function guessCstType(hint: string, defaultType: CstType = CstType.TERM): CstType {
if (hint.length !== 1) { if (hint.length !== 1) {
return defaultType; return defaultType;
} }
@ -274,6 +274,57 @@ export function guessCstType(hint: string, defaultType: CstType = CstType.TERM)
return defaultType; return defaultType;
} }
/**
* Evaluate if {@link CstType} is basic concept.
*/
export function isBasicConcept(type: CstType): boolean {
// prettier-ignore
switch (type) {
case CstType.BASE: return true;
case CstType.CONSTANT: return true;
case CstType.STRUCTURED: return true;
case CstType.AXIOM: return true;
case CstType.TERM: return false;
case CstType.FUNCTION: return false;
case CstType.PREDICATE: return false;
case CstType.THEOREM: return false;
}
}
/**
* Evaluate if {@link CstType} is base set or constant set.
*/
export function isBaseSet(type: CstType): boolean {
// prettier-ignore
switch (type) {
case CstType.BASE: return true;
case CstType.CONSTANT: return true;
case CstType.STRUCTURED: return false;
case CstType.AXIOM: return false;
case CstType.TERM: return false;
case CstType.FUNCTION: return false;
case CstType.PREDICATE: return false;
case CstType.THEOREM: return false;
}
}
/**
* Evaluate if {@link CstType} is function.
*/
export function isFunctional(type: CstType): boolean {
// prettier-ignore
switch (type) {
case CstType.BASE: return false;
case CstType.CONSTANT: return false;
case CstType.STRUCTURED: return false;
case CstType.AXIOM: return false;
case CstType.TERM: return false;
case CstType.FUNCTION: return true;
case CstType.PREDICATE: return true;
case CstType.THEOREM: return false;
}
}
/** /**
* Validate new alias against {@link CstType} and {@link IRSForm}. * Validate new alias against {@link CstType} and {@link IRSForm}.
*/ */

View File

@ -1,15 +1,18 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useLayoutEffect, useState } from 'react'; import { AnimatePresence } from 'framer-motion';
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import RefsInput from '@/components/RefsInput'; import RefsInput from '@/components/RefsInput';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import { IConstituenta, ICstUpdateData } from '@/models/rsform'; import { CstType, IConstituenta, ICstUpdateData } from '@/models/rsform';
import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI';
import { labelCstTypification } from '@/utils/labels'; import { labelCstTypification } from '@/utils/labels';
import EditorRSExpression from '../EditorRSExpression'; import EditorRSExpression from '../EditorRSExpression';
@ -40,9 +43,11 @@ function FormConstituenta({
disabled, disabled,
showList, showList,
id, id,
constituenta,
isModified, isModified,
setIsModified, setIsModified,
constituenta,
toggleReset, toggleReset,
onRename, onRename,
onEditTerm, onEditTerm,
@ -56,6 +61,14 @@ function FormConstituenta({
const [expression, setExpression] = useState(''); const [expression, setExpression] = useState('');
const [convention, setConvention] = useState(''); const [convention, setConvention] = useState('');
const [typification, setTypification] = useState('N/A'); const [typification, setTypification] = useState('N/A');
const [forceComment, setForceComment] = useState(false);
const isBasic = useMemo(() => !!constituenta && isBasicConcept(constituenta.cst_type), [constituenta]);
const isElementary = useMemo(() => !!constituenta && isBaseSet(constituenta.cst_type), [constituenta]);
const showConvention = useMemo(
() => !constituenta || !!constituenta.convention || forceComment || isBasic,
[constituenta, forceComment, isBasic]
);
useEffect(() => { useEffect(() => {
if (!constituenta) { if (!constituenta) {
@ -90,6 +103,7 @@ function FormConstituenta({
setTextDefinition(constituenta.definition_raw || ''); setTextDefinition(constituenta.definition_raw || '');
setExpression(constituenta.definition_formal || ''); setExpression(constituenta.definition_formal || '');
setTypification(constituenta ? labelCstTypification(constituenta) : 'N/A'); setTypification(constituenta ? labelCstTypification(constituenta) : 'N/A');
setForceComment(false);
} }
}, [constituenta, schema, toggleReset]); }, [constituenta, schema, toggleReset]);
@ -126,6 +140,7 @@ function FormConstituenta({
className={clsx('cc-column', 'mt-1 w-full md:w-[47.8rem] shrink-0', 'px-4 py-1')} className={clsx('cc-column', 'mt-1 w-full md:w-[47.8rem] shrink-0', 'px-4 py-1')}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<AnimatePresence>
<RefsInput <RefsInput
id='cst_term' id='cst_term'
label='Термин' label='Термин'
@ -150,10 +165,21 @@ function FormConstituenta({
resize: 'none' resize: 'none'
}} }}
/> />
<AnimateFade hideContent={!!constituenta && !constituenta?.definition_formal && isElementary}>
<EditorRSExpression <EditorRSExpression
id='cst_expression' id='cst_expression'
label='Формальное определение' label={
placeholder='Родоструктурное выражение' constituenta?.cst_type === CstType.STRUCTURED
? 'Область определения'
: !!constituenta && isFunctional(constituenta.cst_type)
? 'Определение функции'
: 'Формальное определение'
}
placeholder={
constituenta?.cst_type !== CstType.STRUCTURED
? 'Родоструктурное выражение'
: 'Определение множества, которому принадлежат элементы родовой структуры'
}
value={expression} value={expression}
activeCst={constituenta} activeCst={constituenta}
showList={showList} showList={showList}
@ -163,6 +189,8 @@ function FormConstituenta({
onChange={newValue => setExpression(newValue)} onChange={newValue => setExpression(newValue)}
setTypification={setTypification} setTypification={setTypification}
/> />
</AnimateFade>
<AnimateFade hideContent={!!constituenta && !constituenta?.definition_raw && isElementary}>
<RefsInput <RefsInput
id='cst_definition' id='cst_definition'
label='Текстовое определение' label='Текстовое определение'
@ -175,16 +203,29 @@ function FormConstituenta({
disabled={disabled} disabled={disabled}
onChange={newValue => setTextDefinition(newValue)} onChange={newValue => setTextDefinition(newValue)}
/> />
</AnimateFade>
<AnimateFade hideContent={!showConvention}>
<TextArea <TextArea
id='cst_convention' id='cst_convention'
spellCheck spellCheck
label='Конвенция / Комментарий' className='h-[3.8rem]'
label={isBasic ? 'Конвенция' : 'Комментарий'}
placeholder='Договоренность об интерпретации или пояснение' placeholder='Договоренность об интерпретации или пояснение'
value={convention} value={convention}
disabled={disabled} disabled={disabled}
rows={convention.length > 2 * ROW_SIZE_IN_CHARACTERS ? 3 : 2} rows={convention.length > 2 * ROW_SIZE_IN_CHARACTERS ? 3 : 2}
onChange={event => setConvention(event.target.value)} onChange={event => setConvention(event.target.value)}
/> />
</AnimateFade>
{!showConvention && (!disabled || processing) ? (
<button
type='button'
className='self-start cc-label clr-text-url hover:underline'
onClick={() => setForceComment(true)}
>
Добавить комментарий
</button>
) : null}
{!disabled || processing ? ( {!disabled || processing ? (
<SubmitButton <SubmitButton
text='Сохранить изменения' text='Сохранить изменения'
@ -193,6 +234,7 @@ function FormConstituenta({
icon={<FiSave size='1.25rem' />} icon={<FiSave size='1.25rem' />}
/> />
) : null} ) : null}
</AnimatePresence>
</form> </form>
</div> </div>
); );

View File

@ -49,7 +49,7 @@ const MAIN_THIRD_ROW: TokenID[] = [
TokenID.LOGIC_EQUIVALENT, TokenID.LOGIC_EQUIVALENT,
TokenID.SET_SYMMETRIC_MINUS, TokenID.SET_SYMMETRIC_MINUS,
TokenID.PUNCTUATION_ASSIGN, TokenID.PUNCTUATION_ASSIGN,
TokenID.EQUAL, TokenID.MULTIPLY,
TokenID.GREATER_OR_EQ, TokenID.GREATER_OR_EQ,
TokenID.LESSER_OR_EQ TokenID.LESSER_OR_EQ
]; ];

View File

@ -65,7 +65,11 @@ function RegisterPage() {
} }
if (user) { if (user) {
return <ExpectedAnonymous />; return (
<AnimateFade>
<ExpectedAnonymous />
</AnimateFade>
);
} }
return ( return (
<AnimateFade> <AnimateFade>

View File

@ -216,7 +216,8 @@ export const animateFade = {
initial: { initial: {
opacity: 0 opacity: 0
}, },
animate: { variants: {
active: {
opacity: 1, opacity: 1,
transition: { transition: {
type: 'tween', type: 'tween',
@ -224,6 +225,15 @@ export const animateFade = {
duration: 0.3 duration: 0.3
} }
}, },
hidden: {
opacity: 0,
transition: {
type: 'tween',
ease: 'linear',
duration: 0.3
}
}
},
exit: { exit: {
opacity: 0, opacity: 0,
transition: { transition: {

View File

@ -129,10 +129,11 @@ export function domTooltipConstituenta(cst?: IConstituenta) {
dom.className = clsx( dom.className = clsx(
'z-modal-tooltip', 'z-modal-tooltip',
'max-h-[25rem] max-w-[25rem] min-w-[10rem]', 'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
'dense',
'p-2', 'p-2',
'border shadow-md', 'border shadow-md',
'overflow-y-auto', 'overflow-y-auto',
'text-sm' 'text-sm font-main'
); );
if (cst) { if (cst) {
@ -179,6 +180,7 @@ export function domTooltipEntityReference(ref: IEntityReference, cst: IConstitue
dom.className = clsx( dom.className = clsx(
'z-tooltip', 'z-tooltip',
'max-h-[25rem] max-w-[25rem] min-w-[10rem]', 'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
'dense',
'p-2 flex flex-col', 'p-2 flex flex-col',
'border shadow-md', 'border shadow-md',
'overflow-y-auto', 'overflow-y-auto',
@ -225,6 +227,7 @@ export function domTooltipSyntacticReference(ref: ISyntacticReference, masterRef
dom.className = clsx( dom.className = clsx(
'z-tooltip', 'z-tooltip',
'max-h-[25rem] max-w-[25rem] min-w-[10rem]', 'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
'dense',
'p-2 flex flex-col', 'p-2 flex flex-col',
'border shadow-md', 'border shadow-md',
'overflow-y-auto', 'overflow-y-auto',

View File

@ -96,6 +96,7 @@ export function labelToken(id: TokenID): string {
case TokenID.LOGIC_EQUIVALENT: return '⇔'; case TokenID.LOGIC_EQUIVALENT: return '⇔';
case TokenID.LIT_EMPTYSET: return '∅'; case TokenID.LIT_EMPTYSET: return '∅';
case TokenID.LIT_WHOLE_NUMBERS: return 'Z'; case TokenID.LIT_WHOLE_NUMBERS: return 'Z';
case TokenID.MULTIPLY: return '*';
case TokenID.EQUAL: return '='; case TokenID.EQUAL: return '=';
case TokenID.NOTEQUAL: return '≠'; case TokenID.NOTEQUAL: return '≠';
case TokenID.GREATER_OR_EQ: return '≥'; case TokenID.GREATER_OR_EQ: return '≥';
@ -163,6 +164,7 @@ export function describeToken(id: TokenID): string {
case TokenID.LIT_EMPTYSET: return prepareTooltip('Пустое множество', 'Alt + X'); case TokenID.LIT_EMPTYSET: return prepareTooltip('Пустое множество', 'Alt + X');
case TokenID.LIT_WHOLE_NUMBERS: return prepareTooltip('Целые числа', 'Alt + Z'); case TokenID.LIT_WHOLE_NUMBERS: return prepareTooltip('Целые числа', 'Alt + Z');
case TokenID.EQUAL: return prepareTooltip('Равенство'); case TokenID.EQUAL: return prepareTooltip('Равенство');
case TokenID.MULTIPLY: return prepareTooltip('Умножение чисел', 'Alt + 8');
case TokenID.NOTEQUAL: return prepareTooltip('Неравенство', 'Alt + Shift + `'); case TokenID.NOTEQUAL: return prepareTooltip('Неравенство', 'Alt + Shift + `');
case TokenID.GREATER_OR_EQ: return prepareTooltip('Больше или равно', 'Alt + Shift + 7'); case TokenID.GREATER_OR_EQ: return prepareTooltip('Больше или равно', 'Alt + Shift + 7');
case TokenID.LESSER_OR_EQ: return prepareTooltip('Меньше или равно', 'Alt + Shift + 8'); case TokenID.LESSER_OR_EQ: return prepareTooltip('Меньше или равно', 'Alt + Shift + 8');