Refactor help topics UI

This commit is contained in:
IRBorisov 2024-05-15 02:51:50 +03:00
parent 22ef6b1886
commit 114f5d75cc
16 changed files with 258 additions and 75 deletions

View File

@ -34,14 +34,14 @@ function InfoTopic({ topic }: InfoTopicProps) {
if (topic === HelpTopic.MAIN) return <HelpMain />;
if (topic === HelpTopic.INTERFACE) return <HelpInterface />;
if (topic === HelpTopic.LIBRARY) return <HelpLibrary />;
if (topic === HelpTopic.RSFORM_UI) return <HelpRSFormUI />;
if (topic === HelpTopic.RSFORM_CARD) return <HelpRSFormMeta />;
if (topic === HelpTopic.RSFORM_LIST) return <HelpRSFormItems />;
if (topic === HelpTopic.RSFORM_EDITOR) return <HelpCstEditor />;
if (topic === HelpTopic.GRAPH_TERM) return <HelpTermGraph />;
if (topic === HelpTopic.CST_STATUS) return <HelpCstStatus />;
if (topic === HelpTopic.CST_CLASS) return <HelpCstClass />;
if (topic === HelpTopic.UI_LIBRARY) return <HelpLibrary />;
if (topic === HelpTopic.UI_RSFORM) return <HelpRSFormUI />;
if (topic === HelpTopic.UI_RSFORM_CARD) return <HelpRSFormMeta />;
if (topic === HelpTopic.UI_RSFORM_LIST) return <HelpRSFormItems />;
if (topic === HelpTopic.UI_RSFORM_EDITOR) return <HelpCstEditor />;
if (topic === HelpTopic.UI_GRAPH_TERM) return <HelpTermGraph />;
if (topic === HelpTopic.UI_CST_STATUS) return <HelpCstStatus />;
if (topic === HelpTopic.UI_CST_CLASS) return <HelpCstClass />;
if (topic === HelpTopic.CONCEPTUAL) return <HelpConcept />;
if (topic === HelpTopic.CC_SYSTEM) return <HelpConceptSystem />;

View File

@ -45,14 +45,14 @@ export enum HelpTopic {
MAIN = 'main',
INTERFACE = 'user-interface',
LIBRARY = 'ui-library',
RSFORM_UI = 'ui-rsform',
RSFORM_CARD = 'ui-rsform-card',
RSFORM_LIST = 'ui-rsform-list',
RSFORM_EDITOR = 'ui-rsform-editor',
GRAPH_TERM = 'ui-rsform-graph',
CST_STATUS = 'ui-rsform-cst-status',
CST_CLASS = 'ui-rsform-cst-class',
UI_LIBRARY = 'ui-library',
UI_RSFORM = 'ui-rsform',
UI_RSFORM_CARD = 'ui-rsform-card',
UI_RSFORM_LIST = 'ui-rsform-list',
UI_RSFORM_EDITOR = 'ui-rsform-editor',
UI_GRAPH_TERM = 'ui-rsform-graph',
UI_CST_STATUS = 'ui-rsform-cst-status',
UI_CST_CLASS = 'ui-rsform-cst-class',
CONCEPTUAL = 'concept',
CC_SYSTEM = 'rslang-rsform',
@ -73,6 +73,46 @@ export enum HelpTopic {
PRIVACY = 'privacy'
}
/**
* Manual topics hierarchy.
*/
export const topicParent: Map<HelpTopic, HelpTopic> = new Map([
[HelpTopic.MAIN, HelpTopic.MAIN],
[HelpTopic.INTERFACE, HelpTopic.INTERFACE],
[HelpTopic.UI_LIBRARY, HelpTopic.INTERFACE],
[HelpTopic.UI_RSFORM, HelpTopic.INTERFACE],
[HelpTopic.UI_RSFORM_CARD, HelpTopic.UI_RSFORM],
[HelpTopic.UI_RSFORM_LIST, HelpTopic.UI_RSFORM],
[HelpTopic.UI_RSFORM_EDITOR, HelpTopic.UI_RSFORM],
[HelpTopic.UI_GRAPH_TERM, HelpTopic.UI_RSFORM],
[HelpTopic.UI_CST_STATUS, HelpTopic.UI_RSFORM],
[HelpTopic.UI_CST_CLASS, HelpTopic.UI_RSFORM],
[HelpTopic.CONCEPTUAL, HelpTopic.CONCEPTUAL],
[HelpTopic.CC_SYSTEM, HelpTopic.CONCEPTUAL],
[HelpTopic.CC_CONSTITUENTA, HelpTopic.CONCEPTUAL],
[HelpTopic.CC_RELATIONS, HelpTopic.CONCEPTUAL],
[HelpTopic.RSLANG, HelpTopic.RSLANG],
[HelpTopic.RSL_TYPES, HelpTopic.RSLANG],
[HelpTopic.RSL_CORRECT, HelpTopic.RSLANG],
[HelpTopic.RSL_INTERPRET, HelpTopic.RSLANG],
[HelpTopic.RSL_TEMPLATES, HelpTopic.RSLANG],
[HelpTopic.RSL_OPERATIONS, HelpTopic.RSLANG],
[HelpTopic.TERM_CONTROL, HelpTopic.TERM_CONTROL],
[HelpTopic.VERSIONS, HelpTopic.VERSIONS],
[HelpTopic.EXTEOR, HelpTopic.EXTEOR],
[HelpTopic.API, HelpTopic.API],
[HelpTopic.PRIVACY, HelpTopic.PRIVACY]
]);
/**
* Topics that can be folded.
*/
export const foldableTopics = [HelpTopic.INTERFACE, HelpTopic.UI_RSFORM, HelpTopic.RSLANG];
/**
* Represents {@link IConstituenta} matching mode.
*/

View File

@ -124,7 +124,7 @@ function ViewLibrary({ items, resetQuery }: ViewLibraryProps) {
'flex gap-1'
)}
>
<BadgeHelp topic={HelpTopic.LIBRARY} className='max-w-[30rem] text-sm' offset={5} place='right-start' />
<BadgeHelp topic={HelpTopic.UI_LIBRARY} className='max-w-[30rem] text-sm' offset={5} place='right-start' />
</div>
</div>
<DataTable

View File

@ -1,10 +1,12 @@
'use client';
import { useCallback, useState } from 'react';
import { urls } from '@/app/urls';
import { useConceptNavigation } from '@/context/NavigationContext';
import { useConceptOptions } from '@/context/OptionsContext';
import useQueryStrings from '@/hooks/useQueryStrings';
import { HelpTopic } from '@/models/miscellaneous';
import { HelpTopic, topicParent } from '@/models/miscellaneous';
import TopicsList from './TopicsList';
import ViewTopic from './ViewTopic';
@ -12,18 +14,63 @@ import ViewTopic from './ViewTopic';
function ManualsPage() {
const router = useConceptNavigation();
const query = useQueryStrings();
const topic = (query.get('topic') || HelpTopic.MAIN) as HelpTopic;
const activeTopic = (query.get('topic') || HelpTopic.MAIN) as HelpTopic;
const [topicFolded, setFolded] = useState<Map<HelpTopic, boolean>>(
new Map(
Object.values(HelpTopic).map(value => {
const topic = value as HelpTopic;
return [
topic,
topicParent.get(activeTopic) !== topic && topicParent.get(topicParent.get(activeTopic)!) !== topic
];
})
)
);
const { mainHeight } = useConceptOptions();
const onFoldTopic = useCallback(
(target: HelpTopic, showChildren: boolean) => {
if (topicFolded.get(target) === !showChildren) {
return;
}
setFolded(
new Map(
Object.values(HelpTopic).map(value => {
const topic = value as HelpTopic;
if (topic === target) {
return [topic, !showChildren];
}
if (
!showChildren &&
(topicParent.get(topic) === target || topicParent.get(topicParent.get(topic)!) === target)
) {
return [topic, true];
}
const oldValue = topicFolded.get(topic)!;
return [topic, oldValue];
})
)
);
},
[topicFolded]
);
function onSelectTopic(newTopic: HelpTopic) {
router.push(urls.help_topic(newTopic));
}
const onSelectTopic = useCallback(
(newTopic: HelpTopic) => {
router.push(urls.help_topic(newTopic));
},
[router]
);
return (
<div className='flex w-full gap-2' style={{ minHeight: mainHeight }}>
<TopicsList activeTopic={topic} onChangeTopic={topic => onSelectTopic(topic)} />
<ViewTopic topic={topic} />
<TopicsList
activeTopic={activeTopic}
onChangeTopic={topic => onSelectTopic(topic)}
topicFolded={topicFolded}
onFoldTopic={onFoldTopic}
/>
<ViewTopic topic={activeTopic} />
</div>
);
}

View File

@ -15,10 +15,12 @@ import TopicsTree from './TopicsTree';
interface TopicsDropdownProps {
activeTopic: HelpTopic;
topicFolded: Map<HelpTopic, boolean>;
onChangeTopic: (newTopic: HelpTopic) => void;
onFoldTopic: (target: HelpTopic, showChildren: boolean) => void;
}
function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
function TopicsDropdown({ activeTopic, topicFolded, onChangeTopic, onFoldTopic }: TopicsDropdownProps) {
const menu = useDropdown();
const { noNavigation, calculateHeight } = useConceptOptions();
@ -34,7 +36,7 @@ function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
<div
ref={menu.ref}
className={clsx(
'absolute left-0 w-[13rem]', // prettier: split-lines
'absolute left-0 w-[13.5rem]', // prettier: split-lines
'flex flex-col',
'z-modal-tooltip',
'text-xs sm:text-sm',
@ -55,13 +57,22 @@ function TopicsDropdown({ activeTopic, onChangeTopic }: TopicsDropdownProps) {
onClick={menu.toggle}
/>
<motion.div
className='border divide-y rounded-none cc-scroll-y'
className={clsx(
'border divide-y rounded-none', // prettier: split-lines
'cc-scroll-y',
'clr-controls'
)}
style={{ maxHeight: calculateHeight('4rem + 2px') }}
initial={false}
animate={menu.isOpen ? 'open' : 'closed'}
variants={animateSlideLeft}
>
<TopicsTree activeTopic={activeTopic} onChangeTopic={selectTheme} />
<TopicsTree
activeTopic={activeTopic}
onChangeTopic={selectTheme}
topicFolded={topicFolded}
onFoldTopic={onFoldTopic}
/>
</motion.div>
</div>
);

View File

@ -8,16 +8,32 @@ import TopicsStatic from './TopicsStatic';
interface TopicsListProps {
activeTopic: HelpTopic;
topicFolded: Map<HelpTopic, boolean>;
onChangeTopic: (newTopic: HelpTopic) => void;
onFoldTopic: (target: HelpTopic, showChildren: boolean) => void;
}
function TopicsList({ activeTopic, onChangeTopic }: TopicsListProps) {
function TopicsList({ activeTopic, topicFolded, onChangeTopic, onFoldTopic }: TopicsListProps) {
const size = useWindowSize();
if (!size.isSmall) {
return <TopicsStatic activeTopic={activeTopic} onChangeTopic={onChangeTopic} />;
return (
<TopicsStatic
activeTopic={activeTopic}
onChangeTopic={onChangeTopic}
topicFolded={topicFolded}
onFoldTopic={onFoldTopic}
/>
);
} else {
return <TopicsDropdown activeTopic={activeTopic} onChangeTopic={onChangeTopic} />;
return (
<TopicsDropdown
activeTopic={activeTopic}
onChangeTopic={onChangeTopic}
topicFolded={topicFolded}
onFoldTopic={onFoldTopic}
/>
);
}
}

View File

@ -7,16 +7,18 @@ import TopicsTree from './TopicsTree';
interface TopicsStaticProps {
activeTopic: HelpTopic;
topicFolded: Map<HelpTopic, boolean>;
onChangeTopic: (newTopic: HelpTopic) => void;
onFoldTopic: (target: HelpTopic, showChildren: boolean) => void;
}
function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps) {
function TopicsStatic({ activeTopic, topicFolded, onChangeTopic, onFoldTopic }: TopicsStaticProps) {
const { calculateHeight } = useConceptOptions();
return (
<div
className={clsx(
'sticky top-0 left-0',
'w-[14rem] cc-scroll-y',
'w-[14.5rem] cc-scroll-y',
'self-start',
'border divide-y rounded-none',
'clr-controls',
@ -25,7 +27,12 @@ function TopicsStatic({ activeTopic, onChangeTopic }: TopicsStaticProps) {
)}
style={{ maxHeight: calculateHeight('2.25rem + 2px') }}
>
<TopicsTree activeTopic={activeTopic} onChangeTopic={onChangeTopic} />
<TopicsTree
activeTopic={activeTopic}
onChangeTopic={onChangeTopic}
topicFolded={topicFolded}
onFoldTopic={onFoldTopic}
/>
</div>
);
}

View File

@ -1,35 +1,75 @@
'use client';
import clsx from 'clsx';
import { AnimatePresence } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { useCallback } from 'react';
import { HelpTopic } from '@/models/miscellaneous';
import { IconDropArrow, IconPageRight } from '@/components/Icons';
import { CProps } from '@/components/props';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
import { foldableTopics, HelpTopic, topicParent } from '@/models/miscellaneous';
import { animateSideAppear } from '@/styling/animations';
import { prefixes } from '@/utils/constants';
import { describeHelpTopic, labelHelpTopic } from '@/utils/labels';
interface TopicsTreeProps {
activeTopic: HelpTopic;
topicFolded: Map<HelpTopic, boolean>;
onChangeTopic: (newTopic: HelpTopic) => void;
onFoldTopic: (target: HelpTopic, showChildren: boolean) => void;
}
function TopicsTree({ activeTopic, onChangeTopic }: TopicsTreeProps) {
function TopicsTree({ activeTopic, topicFolded, onChangeTopic, onFoldTopic }: TopicsTreeProps) {
const handleClickFold = useCallback(
(event: CProps.EventMouse, topic: HelpTopic, showChildren: boolean) => {
event.preventDefault();
event.stopPropagation();
onFoldTopic(topic, showChildren);
},
[onFoldTopic]
);
return (
<AnimatePresence initial={false}>
{Object.values(HelpTopic).map((topic, index) => (
<div
key={`${prefixes.topic_list}${index}`}
className={clsx(
'px-3 py-1 cc-scroll-row',
'clr-controls clr-hover',
'cursor-pointer',
activeTopic === topic && 'clr-selected'
)}
title={describeHelpTopic(topic)}
onClick={() => onChangeTopic(topic)}
>
{labelHelpTopic(topic)}
</div>
))}
{Object.values(HelpTopic).map((topic, index) => {
const parent = topicParent.get(topic);
if (parent !== topic && topicFolded.get(topicParent.get(topic)!)) {
return null;
}
const isFoldable = !!foldableTopics.find(id => id === topic);
const isFolded = topicFolded.get(topic)!;
return (
<motion.div
tabIndex={-1}
key={`${prefixes.topic_list}${index}`}
className={clsx(
'pr-3 pl-6 py-1',
'cc-scroll-row',
'clr-controls clr-hover',
'cursor-pointer',
activeTopic === topic && 'clr-selected'
)}
title={describeHelpTopic(topic)}
onClick={() => onChangeTopic(topic)}
initial={{ ...animateSideAppear.initial }}
animate={{ ...animateSideAppear.animate }}
exit={{ ...animateSideAppear.exit }}
>
{isFoldable ? (
<Overlay position='left-[-1.3rem]' className={clsx(!isFolded && 'top-[0.1rem]')}>
<MiniButton
tabIndex={-1}
noPadding
noHover
icon={!isFolded ? <IconDropArrow size='1rem' /> : <IconPageRight size='1.25rem' />}
onClick={event => handleClickFold(event, topic, isFolded)}
/>
</Overlay>
) : null}
{labelHelpTopic(topic)}
</motion.div>
);
})}
</AnimatePresence>
);
}

View File

@ -74,7 +74,7 @@ function ConstituentaToolbar({
disabled={disabled || modified}
onClick={onMoveDown}
/>
<BadgeHelp topic={HelpTopic.RSFORM_EDITOR} offset={4} />
<BadgeHelp topic={HelpTopic.UI_RSFORM_EDITOR} offset={4} />
</Overlay>
);
}

View File

@ -205,7 +205,7 @@ function EditorRSExpression({
parseData={parser.parseData}
onAnalyze={() => handleCheckExpression()}
/>
<BadgeHelp topic={HelpTopic.CST_STATUS} offset={4} />
<BadgeHelp topic={HelpTopic.UI_CST_STATUS} offset={4} />
</Overlay>
<RSInput

View File

@ -81,7 +81,7 @@ function RSFormToolbar({ modified, anonymous, subscribed, claimable, onSubmit, o
onClick={onDestroy}
/>
) : null}
<BadgeHelp topic={HelpTopic.RSFORM_CARD} offset={4} className='max-w-[32rem]' />
<BadgeHelp topic={HelpTopic.UI_RSFORM_CARD} offset={4} className='max-w-[32rem]' />
</Overlay>
);
}

View File

@ -68,7 +68,7 @@ function RSListToolbar() {
disabled={controller.isProcessing || controller.nothingSelected}
onClick={controller.deleteCst}
/>
<BadgeHelp topic={HelpTopic.RSFORM_LIST} offset={5} />
<BadgeHelp topic={HelpTopic.UI_RSFORM_LIST} offset={5} />
</Overlay>
);
}

View File

@ -28,8 +28,8 @@ function GraphSelectors({ coloring, setColoring, layout, setLayout, sizing, setS
onChange={data => setLayout(data?.value ?? SelectorGraphLayout[0].value)}
/>
<Overlay position='right-[2.5rem] top-[0.5rem]'>
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.CST_STATUS} className='min-w-[25rem]' /> : null}
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.CST_CLASS} className='min-w-[25rem]' /> : null}
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} className='min-w-[25rem]' /> : null}
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} className='min-w-[25rem]' /> : null}
</Overlay>
<SelectSingle
className='my-1'

View File

@ -111,7 +111,7 @@ function GraphToolbar({
title='Сохранить изображение'
onClick={onSaveImage}
/>
<BadgeHelp topic={HelpTopic.GRAPH_TERM} className='max-w-[calc(100vw-4rem)]' offset={4} />
<BadgeHelp topic={HelpTopic.UI_GRAPH_TERM} className='max-w-[calc(100vw-4rem)]' offset={4} />
</div>
);
}

View File

@ -208,6 +208,28 @@ export const animateSideView = {
}
};
export const animateSideAppear = {
initial: {
clipPath: 'inset(0% 100% 0% 0%)'
},
animate: {
clipPath: 'inset(0% 0% 0% 0%)',
transition: {
type: 'spring',
bounce: 0,
duration: 0.3
}
},
exit: {
clipPath: 'inset(0% 100% 0% 0%)',
transition: {
type: 'spring',
bounce: 0,
duration: 0.3
}
}
};
export const animateModal = {
initial: {
clipPath: 'inset(50% 50% 50% 50%)',

View File

@ -361,14 +361,14 @@ export function labelHelpTopic(topic: HelpTopic): string {
case HelpTopic.MAIN: return 'Портал';
case HelpTopic.INTERFACE: return 'Интерфейс';
case HelpTopic.LIBRARY: return '- библиотека';
case HelpTopic.RSFORM_UI: return '- концептуальная схема';
case HelpTopic.RSFORM_CARD: return '= карточка схемы';
case HelpTopic.RSFORM_LIST: return '= список конституент';
case HelpTopic.RSFORM_EDITOR: return '= редактор конституенты';
case HelpTopic.GRAPH_TERM: return '= граф термов';
case HelpTopic.CST_STATUS: return '= статус конституенты';
case HelpTopic.CST_CLASS: return '= класс конституенты';
case HelpTopic.UI_LIBRARY: return '- библиотека';
case HelpTopic.UI_RSFORM: return '- концептуальная схема';
case HelpTopic.UI_RSFORM_CARD: return '= карточка схемы';
case HelpTopic.UI_RSFORM_LIST: return '= список конституент';
case HelpTopic.UI_RSFORM_EDITOR: return '= редактор конституенты';
case HelpTopic.UI_GRAPH_TERM: return '= граф термов';
case HelpTopic.UI_CST_STATUS: return '= статус конституенты';
case HelpTopic.UI_CST_CLASS: return '= класс конституенты';
case HelpTopic.CONCEPTUAL: return 'Концептуализация';
case HelpTopic.CC_SYSTEM: return '- система определений';
@ -399,14 +399,14 @@ export function describeHelpTopic(topic: HelpTopic): string {
case HelpTopic.MAIN: return 'Общая справка по порталу';
case HelpTopic.INTERFACE: return 'Описание интерфейса пользователя';
case HelpTopic.LIBRARY: return 'Интерфейс Библиотеки схем';
case HelpTopic.RSFORM_UI: return 'Просмотр и редактирование концептуальной схемы';
case HelpTopic.RSFORM_CARD: return 'Интерфейс Карточки схемы';
case HelpTopic.RSFORM_LIST: return 'Интерфейс Списка конституент';
case HelpTopic.RSFORM_EDITOR: return 'Интерфейс редактирования конституенты';
case HelpTopic.GRAPH_TERM: return 'Интерфейс графа термов';
case HelpTopic.CST_STATUS: return 'Нотация отображения статуса конституенты';
case HelpTopic.CST_CLASS: return 'Нотация отображения класса конституенты';
case HelpTopic.UI_LIBRARY: return 'Интерфейс Библиотеки схем';
case HelpTopic.UI_RSFORM: return 'Просмотр и редактирование концептуальной схемы';
case HelpTopic.UI_RSFORM_CARD: return 'Интерфейс Карточки схемы';
case HelpTopic.UI_RSFORM_LIST: return 'Интерфейс Списка конституент';
case HelpTopic.UI_RSFORM_EDITOR: return 'Интерфейс редактирования конституенты';
case HelpTopic.UI_GRAPH_TERM: return 'Интерфейс графа термов';
case HelpTopic.UI_CST_STATUS: return 'Нотация отображения статуса конституенты';
case HelpTopic.UI_CST_CLASS: return 'Нотация отображения класса конституенты';
case HelpTopic.CONCEPTUAL: return 'Основы концептуализации и концептуального мышления';
case HelpTopic.CC_SYSTEM: return 'Концептуальная схема как система понятий';