Refactor and simplify UI

This commit is contained in:
IRBorisov 2023-12-17 20:19:28 +03:00
parent 219bf4a111
commit 1009a2ec98
51 changed files with 554 additions and 648 deletions

View File

@ -9,7 +9,7 @@ Styling conventions
<pre> <pre>
- layer: z-position - layer: z-position
- outer layout: fixed bottom-1/2 left-0 -translate-x-1/2 - outer layout: fixed bottom-1/2 left-0 -translate-x-1/2
- rectangle: mt-3 w-full min-w-10 h-fit - rectangle: mt-3 w-full min-w-10 h-fit flex-grow
- inner layout: px-3 py-2 flex flex-col gap-3 justify-start items-center - inner layout: px-3 py-2 flex flex-col gap-3 justify-start items-center
- overflow behavior: overflow-auto - overflow behavior: overflow-auto
- border: borer-2 outline-none shadow-md - border: borer-2 outline-none shadow-md

View File

@ -1,4 +1,3 @@
import clsx from 'clsx';
import { createBrowserRouter, Outlet, RouterProvider } from 'react-router-dom'; import { createBrowserRouter, Outlet, RouterProvider } from 'react-router-dom';
import ConceptToaster from './components/ConceptToaster'; import ConceptToaster from './components/ConceptToaster';
@ -19,14 +18,10 @@ import UserProfilePage from './pages/UserProfilePage';
import { globalIDs } from './utils/constants'; import { globalIDs } from './utils/constants';
function Root() { function Root() {
const { noNavigation, noFooter, viewportHeight, mainHeight, showScroll } = useConceptTheme(); const { viewportHeight, mainHeight, showScroll } = useConceptTheme();
return ( return (
<NavigationState> <NavigationState>
<div className={clsx( <div className='min-w-[30rem] clr-app antialiased'>
'w-screen min-w-[30rem]',
'clr-app',
'antialiased'
)}>
<ConceptToaster <ConceptToaster
className='mt-[4rem] text-sm' className='mt-[4rem] text-sm'
@ -38,23 +33,20 @@ function Root() {
<Navigation /> <Navigation />
<div id={globalIDs.main_scroll} <div id={globalIDs.main_scroll}
className='w-full overflow-x-auto overscroll-none' className='overflow-x-auto overscroll-none'
style={{ style={{
maxHeight: viewportHeight, maxHeight: viewportHeight,
overflowY: showScroll ? 'scroll': 'auto' overflowY: showScroll ? 'scroll': 'auto'
}} }}
> >
<main <main
className={clsx( className='flex flex-col items-center'
'w-full h-full min-w-fit',
'flex flex-col items-center'
)}
style={{minHeight: mainHeight}} style={{minHeight: mainHeight}}
> >
<Outlet /> <Outlet />
</main> </main>
{(!noNavigation && !noFooter) ? <Footer /> : null} <Footer />
</div> </div>
</div> </div>
</NavigationState>); </NavigationState>);

View File

@ -18,7 +18,7 @@ function Dropdown({
layer='z-modal-tooltip' layer='z-modal-tooltip'
position='mt-3' position='mt-3'
className={clsx( className={clsx(
'flex flex-col items-stretch justify-start', 'flex flex-col items-stretch',
'border rounded-md shadow-lg', 'border rounded-md shadow-lg',
'text-sm', 'text-sm',
'clr-input', 'clr-input',

View File

@ -59,7 +59,7 @@ function Modal({
'clr-app' 'clr-app'
)} )}
> >
<Overlay position='right-[0.3rem] top-2' className='text-disabled'> <Overlay position='right-[0.3rem] top-2'>
<MiniButton <MiniButton
tooltip='Закрыть диалоговое окно [ESC]' tooltip='Закрыть диалоговое окно [ESC]'
icon={<BiX size='1.25rem'/>} icon={<BiX size='1.25rem'/>}

View File

@ -1,7 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
interface SubmitButtonProps interface SubmitButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children' | 'title'> { extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'title'> {
text?: string text?: string
tooltip?: string tooltip?: string
loading?: boolean loading?: boolean
@ -10,21 +10,21 @@ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'child
} }
function SubmitButton({ function SubmitButton({
text = 'ОК', icon, disabled, tooltip, loading, text = 'ОК', icon, disabled, tooltip, loading, className,
dimensions = 'w-fit h-fit', ...restProps dimensions = 'w-fit h-fit', ...restProps
}: SubmitButtonProps) { }: SubmitButtonProps) {
return ( return (
<button type='submit' <button type='submit'
title={tooltip} title={tooltip}
className={clsx( className={clsx(
'px-3 py-2', 'px-3 py-2 inline-flex items-center gap-2 align-middle justify-center',
'inline-flex items-center gap-2 align-middle justify-center',
'border', 'border',
'font-semibold', 'font-semibold',
'clr-btn-primary', 'clr-btn-primary',
'select-none disabled:cursor-not-allowed', 'select-none disabled:cursor-not-allowed',
loading && 'cursor-progress', loading && 'cursor-progress',
dimensions dimensions,
className
)} )}
disabled={disabled ?? loading} disabled={disabled ?? loading}
{...restProps} {...restProps}

View File

@ -1,19 +1,20 @@
'use client'; 'use client';
import { import {
Cell, ColumnSort, ColumnSort,
createColumnHelper, flexRender, getCoreRowModel, createColumnHelper, getCoreRowModel,
getPaginationRowModel, getSortedRowModel, Header, HeaderGroup, getPaginationRowModel, getSortedRowModel,
PaginationState, Row, RowData, type RowSelectionState, PaginationState, RowData, type RowSelectionState,
SortingState, TableOptions, useReactTable, type VisibilityState SortingState, TableOptions, useReactTable, type VisibilityState
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import DefaultNoData from './DefaultNoData'; import DefaultNoData from './DefaultNoData';
import PaginationTools from './PaginationTools'; import PaginationTools from './PaginationTools';
import SelectAll from './SelectAll'; import TableBody from './TableBody';
import SelectRow from './SelectRow'; import TableFooter from './TableFooter';
import SortingIcon from './SortingIcon'; import TableHeader from './TableHeader';
export { createColumnHelper, type ColumnSort, type RowSelectionState, type VisibilityState }; export { createColumnHelper, type ColumnSort, type RowSelectionState, type VisibilityState };
@ -27,14 +28,19 @@ extends Pick<TableOptions<TData>,
'data' | 'columns' | 'data' | 'columns' |
'onRowSelectionChange' | 'onColumnVisibilityChange' 'onRowSelectionChange' | 'onColumnVisibilityChange'
> { > {
style?: React.CSSProperties
className?: string
dense?: boolean dense?: boolean
headPosition?: string headPosition?: string
noHeader?: boolean noHeader?: boolean
noFooter?: boolean noFooter?: boolean
conditionalRowStyles?: IConditionalStyle<TData>[] conditionalRowStyles?: IConditionalStyle<TData>[]
noDataComponent?: React.ReactNode
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
noDataComponent?: React.ReactNode
enableRowSelection?: boolean enableRowSelection?: boolean
rowSelection?: RowSelectionState rowSelection?: RowSelectionState
@ -58,6 +64,7 @@ extends Pick<TableOptions<TData>,
* No sticky header if omitted * No sticky header if omitted
*/ */
function DataTable<TData extends RowData>({ function DataTable<TData extends RowData>({
style, className,
dense, headPosition, conditionalRowStyles, noFooter, noHeader, dense, headPosition, conditionalRowStyles, noFooter, noHeader,
onRowClicked, onRowDoubleClicked, noDataComponent, onRowClicked, onRowDoubleClicked, noDataComponent,
@ -104,104 +111,30 @@ function DataTable<TData extends RowData>({
const isEmpty = tableImpl.getRowModel().rows.length === 0; const isEmpty = tableImpl.getRowModel().rows.length === 0;
function getRowStyles(row: Row<TData>) {
return ({...conditionalRowStyles!
.filter(item => item.when(row.original))
.reduce((prev, item) => ({...prev, ...item.style}), {})
});
}
return ( return (
<div className='w-full'> <div className={clsx(className)} style={style}>
<div className='flex flex-col items-stretch'> <table className='w-full'>
<table>
{!noHeader ? {!noHeader ?
<thead <TableHeader
className={`clr-app shadow-border`} table={tableImpl}
style={{ enableRowSelection={enableRowSelection}
top: headPosition, enableSorting={enableSorting}
position: 'sticky' headPosition={headPosition}
}} />: null}
>
{tableImpl.getHeaderGroups().map(
(headerGroup: HeaderGroup<TData>) => (
<tr key={headerGroup.id}>
{enableRowSelection ?
<th className='pl-3 pr-1'>
<SelectAll table={tableImpl} />
</th> : null}
{headerGroup.headers.map(
(header: Header<TData, unknown>) => (
<th key={header.id}
colSpan={header.colSpan}
className='px-2 py-2 text-xs font-semibold select-none whitespace-nowrap'
style={{
textAlign: header.getSize() > 100 ? 'left': 'center',
width: header.getSize(),
cursor: enableSorting && header.column.getCanSort() ? 'pointer': 'auto',
}}
onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined}
>
{!header.isPlaceholder ? (
<div className='flex gap-1'>
{flexRender(header.column.columnDef.header, header.getContext())}
{(enableSorting && header.column.getCanSort()) ? <SortingIcon column={header.column} /> : null}
</div>) : null}
</th>
))}
</tr>
))}
</thead> : null}
<tbody> <TableBody
{tableImpl.getRowModel().rows.map( table={tableImpl}
(row: Row<TData>, index) => ( dense={dense}
<tr conditionalRowStyles={conditionalRowStyles}
key={row.id} enableRowSelection={enableRowSelection}
className={ onRowClicked={onRowClicked}
row.getIsSelected() ? 'clr-selected clr-hover' : onRowDoubleClicked={onRowDoubleClicked}
index % 2 === 0 ? 'clr-controls clr-hover' : 'clr-app clr-hover' />
}
style={conditionalRowStyles && getRowStyles(row)}
>
{enableRowSelection ?
<td key={`select-${row.id}`} className='pl-3 pr-1 border-y'>
<SelectRow row={row} />
</td> : null}
{row.getVisibleCells().map(
(cell: Cell<TData, unknown>) => (
<td
key={cell.id}
className='px-2 border-y'
style={{
cursor: onRowClicked || onRowDoubleClicked ? 'pointer': 'auto',
paddingBottom: dense ? '0.25rem': '0.5rem',
paddingTop: dense ? '0.25rem': '0.5rem'
}}
onClick={event => onRowClicked ? onRowClicked(row.original, event) : undefined}
onDoubleClick={event => onRowDoubleClicked ? onRowDoubleClicked(row.original, event) : undefined}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
{!noFooter ? {!noFooter ?
<tfoot> <TableFooter
{tableImpl.getFooterGroups().map( table={tableImpl}
(footerGroup: HeaderGroup<TData>) => ( />: null}
<tr key={footerGroup.id}>
{footerGroup.headers.map(
(header: Header<TData, unknown>) => (
<th key={header.id}>
{!header.isPlaceholder ? flexRender(header.column.columnDef.footer, header.getContext()) : null}
</th>
))}
</tr>
))}
</tfoot> : null}
</table> </table>
{(enablePagination && !isEmpty) ? {(enablePagination && !isEmpty) ?
@ -210,8 +143,7 @@ function DataTable<TData extends RowData>({
paginationOptions={paginationOptions} paginationOptions={paginationOptions}
onChangePaginationOption={onChangePaginationOption} onChangePaginationOption={onChangePaginationOption}
/> : null} /> : null}
</div> {isEmpty ? (noDataComponent ?? <DefaultNoData />) : null}
{isEmpty ? (noDataComponent ?? <DefaultNoData />) : null}
</div>); </div>);
} }

View File

@ -1,6 +1,5 @@
import { Column } from '@tanstack/react-table'; import { Column } from '@tanstack/react-table';
import { BiCaretDown, BiCaretUp } from 'react-icons/bi';
import { AscendingIcon, DescendingIcon } from '@/components/Icons';
interface SortingIconProps<TData> { interface SortingIconProps<TData> {
column: Column<TData> column: Column<TData>
@ -9,10 +8,10 @@ interface SortingIconProps<TData> {
function SortingIcon<TData>({ column }: SortingIconProps<TData>) { function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
return (<> return (<>
{{ {{
desc: <DescendingIcon size='1rem' />, desc: <BiCaretDown size='1rem' />,
asc: <AscendingIcon size='1rem'/>, asc: <BiCaretUp size='1rem'/>,
}[column.getIsSorted() as string] ?? }[column.getIsSorted() as string] ??
<DescendingIcon size='1rem' className='opacity-0 hover:opacity-50' /> <BiCaretDown size='1rem' className='opacity-0 hover:opacity-50' />
} }
</>); </>);
} }

View File

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

View File

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

View File

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

View File

@ -1,14 +1,19 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useConceptTheme } from '@/context/ThemeContext';
import { urls } from '@/utils/constants'; import { urls } from '@/utils/constants';
import TextURL from './Common/TextURL'; import TextURL from './Common/TextURL';
function Footer() { function Footer() {
const { noNavigation, noFooter } = useConceptTheme();
if (noNavigation || noFooter) {
return null;
}
return ( return (
<footer tabIndex={-1} <footer tabIndex={-1}
className={clsx( className={clsx(
'w-full z-navigation', 'z-navigation',
'px-4 py-2 flex flex-col items-center gap-1', 'px-4 py-2 flex flex-col items-center gap-1',
'text-sm select-none whitespace-nowrap' 'text-sm select-none whitespace-nowrap'
)} )}

View File

@ -1,4 +1,5 @@
import { EducationIcon, GroupIcon,SubscribedIcon } from '@/components/Icons'; import { BiCheckShield, BiShareAlt } from 'react-icons/bi';
import { FiBell } from 'react-icons/fi';
function HelpLibrary() { function HelpLibrary() {
return ( return (
@ -9,15 +10,15 @@ function HelpLibrary() {
<p>На текущем этапе происходит наполнение Библиотеки концептуальными схемами.</p> <p>На текущем этапе происходит наполнение Библиотеки концептуальными схемами.</p>
<p>Поиск осуществлеяется с помощью инструментов в верхней части страницы.</p> <p>Поиск осуществлеяется с помощью инструментов в верхней части страницы.</p>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<SubscribedIcon size='1rem'/> <FiBell size='1rem'/>
<p>Аттрибут <b>отслеживаемая</b> обозначает отслеживание схемы.</p> <p>Аттрибут <b>отслеживаемая</b> обозначает отслеживание схемы.</p>
</div> </div>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<GroupIcon size='1rem'/> <BiShareAlt size='1rem'/>
<p>Аттрибут <b>общедоступная</b> делает схему видимой в разделе библиотека.</p> <p>Аттрибут <b>общедоступная</b> делает схему видимой в разделе библиотека.</p>
</div> </div>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<EducationIcon size='1rem'/> <BiCheckShield size='1rem'/>
<p>Аттрибут <b>неизменная</b> выделяет стандартные схемы.</p> <p>Аттрибут <b>неизменная</b> выделяет стандартные схемы.</p>
</div> </div>
</div>); </div>);

View File

@ -27,62 +27,6 @@ function IconSVG({ viewbox, size = '1.5rem', className, props, children }: IconS
</svg>); </svg>);
} }
export function SubscribedIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M19 13.586V10c0-3.217-2.185-5.927-5.145-6.742C13.562 2.52 12.846 2 12 2s-1.562.52-1.855 1.258C7.185 4.074 5 6.783 5 10v3.586l-1.707 1.707A.996.996 0 003 16v2a1 1 0 001 1h16a1 1 0 001-1v-2a.996.996 0 00-.293-.707L19 13.586zM19 17H5v-.586l1.707-1.707A.996.996 0 007 14v-4c0-2.757 2.243-5 5-5s5 2.243 5 5v4c0 .266.105.52.293.707L19 16.414V17zm-7 5a2.98 2.98 0 002.818-2H9.182A2.98 2.98 0 0012 22z' />
</IconSVG>
);
}
export function NotSubscribedIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12 22a2.98 2.98 0 002.818-2H9.182A2.98 2.98 0 0012 22zm9-4v-2a.996.996 0 00-.293-.707L19 13.586V10c0-3.217-2.185-5.927-5.145-6.742C13.562 2.52 12.846 2 12 2s-1.562.52-1.855 1.258c-1.323.364-2.463 1.128-3.346 2.127L3.707 2.293 2.293 3.707l18 18 1.414-1.414-1.362-1.362A.993.993 0 0021 18zM12 5c2.757 0 5 2.243 5 5v4c0 .266.105.52.293.707L19 16.414V17h-.586L8.207 6.793C9.12 5.705 10.471 5 12 5zm-5.293 9.707A.996.996 0 007 14v-2.879L5.068 9.189C5.037 9.457 5 9.724 5 10v3.586l-1.707 1.707A.996.996 0 003 16v2a1 1 0 001 1h10.879l-2-2H5v-.586l1.707-1.707z'/>
</IconSVG>
);
}
export function ASTNetworkIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12 1a2.5 2.5 0 00-2.5 2.5A2.5 2.5 0 0011 5.79V7H7a2 2 0 00-2 2v.71A2.5 2.5 0 003.5 12 2.5 2.5 0 005 14.29V15H4a2 2 0 00-2 2v1.21A2.5 2.5 0 00.5 20.5 2.5 2.5 0 003 23a2.5 2.5 0 002.5-2.5A2.5 2.5 0 004 18.21V17h4v1.21a2.5 2.5 0 00-1.5 2.29A2.5 2.5 0 009 23a2.5 2.5 0 002.5-2.5 2.5 2.5 0 00-1.5-2.29V17a2 2 0 00-2-2H7v-.71A2.5 2.5 0 008.5 12 2.5 2.5 0 007 9.71V9h10v.71A2.5 2.5 0 0015.5 12a2.5 2.5 0 001.5 2.29V15h-1a2 2 0 00-2 2v1.21a2.5 2.5 0 00-1.5 2.29A2.5 2.5 0 0015 23a2.5 2.5 0 002.5-2.5 2.5 2.5 0 00-1.5-2.29V17h4v1.21a2.5 2.5 0 00-1.5 2.29A2.5 2.5 0 0021 23a2.5 2.5 0 002.5-2.5 2.5 2.5 0 00-1.5-2.29V17a2 2 0 00-2-2h-1v-.71A2.5 2.5 0 0020.5 12 2.5 2.5 0 0019 9.71V9a2 2 0 00-2-2h-4V5.79a2.5 2.5 0 001.5-2.29A2.5 2.5 0 0012 1m0 1.5a1 1 0 011 1 1 1 0 01-1 1 1 1 0 01-1-1 1 1 0 011-1M6 11a1 1 0 011 1 1 1 0 01-1 1 1 1 0 01-1-1 1 1 0 011-1m12 0a1 1 0 011 1 1 1 0 01-1 1 1 1 0 01-1-1 1 1 0 011-1M3 19.5a1 1 0 011 1 1 1 0 01-1 1 1 1 0 01-1-1 1 1 0 011-1m6 0a1 1 0 011 1 1 1 0 01-1 1 1 1 0 01-1-1 1 1 0 011-1m6 0a1 1 0 011 1 1 1 0 01-1 1 1 1 0 01-1-1 1 1 0 011-1m6 0a1 1 0 011 1 1 1 0 01-1 1 1 1 0 01-1-1 1 1 0 011-1z'/>
</IconSVG>
);
}
export function GroupIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z' />
</IconSVG>
);
}
export function ShareIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M5.5 15a3.51 3.51 0 002.36-.93l6.26 3.58a3.06 3.06 0 00-.12.85 3.53 3.53 0 101.14-2.57l-6.26-3.58a2.74 2.74 0 00.12-.76l6.15-3.52A3.49 3.49 0 1014 5.5a3.35 3.35 0 00.12.85L8.43 9.6A3.5 3.5 0 105.5 15zm12 2a1.5 1.5 0 11-1.5 1.5 1.5 1.5 0 011.5-1.5zm0-13A1.5 1.5 0 1116 5.5 1.5 1.5 0 0117.5 4zm-12 6A1.5 1.5 0 114 11.5 1.5 1.5 0 015.5 10z' />
</IconSVG>
);
}
export function SortIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M8 16H4l6 6V2H8zm6-11v17h2V8h4l-6-6z' />
</IconSVG>
);
}
export function UserIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 512 512' {...props}>
<path d='M399 384.2c-22.1-38.4-63.6-64.2-111-64.2h-64c-47.4 0-88.9 25.8-111 64.2 35.2 39.2 86.2 63.8 143 63.8s107.8-24.7 143-63.8zM512 256c0 141.4-114.6 256-256 256S0 397.4 0 256 114.6 0 256 0s256 114.6 256 256zm-256 16c39.8 0 72-32.2 72-72s-32.2-72-72-72-72 32.2-72 72 32.2 72 72 72z' />
</IconSVG>
);
}
export function EducationIcon(props: IconProps) { export function EducationIcon(props: IconProps) {
return ( return (
<IconSVG viewbox='0 0 20 20' {...props}> <IconSVG viewbox='0 0 20 20' {...props}>
@ -91,56 +35,6 @@ export function EducationIcon(props: IconProps) {
); );
} }
export function LibraryIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 512 512' {...props}>
<path d='M64 480H48a32 32 0 01-32-32V112a32 32 0 0132-32h16a32 32 0 0132 32v336a32 32 0 01-32 32zM240 176a32 32 0 00-32-32h-64a32 32 0 00-32 32v28a4 4 0 004 4h120a4 4 0 004-4zM112 448a32 32 0 0032 32h64a32 32 0 0032-32v-30a2 2 0 00-2-2H114a2 2 0 00-2 2z' />
<path d='M114 240 H238 A2 2 0 0 1 240 242 V382 A2 2 0 0 1 238 384 H114 A2 2 0 0 1 112 382 V242 A2 2 0 0 1 114 240 z' />
<path d='M320 480h-32a32 32 0 01-32-32V64a32 32 0 0132-32h32a32 32 0 0132 32v384a32 32 0 01-32 32zM495.89 445.45l-32.23-340c-1.48-15.65-16.94-27-34.53-25.31l-31.85 3c-17.59 1.67-30.65 15.71-29.17 31.36l32.23 340c1.48 15.65 16.94 27 34.53 25.31l31.85-3c17.59-1.67 30.65-15.71 29.17-31.36z' />
</IconSVG>
);
}
export function PlusIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 1024 1024' {...props}>
<path d='M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zM704 536c0 4.4-3.6 8-8 8H544v152c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V544H328c-4.4 0-8-3.6-8-8v-48c0-4.4 3.6-8 8-8h152V328c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v152h152c4.4 0 8 3.6 8 8v48z' />
</IconSVG>
);
}
export function ArrowLeftIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12.707 17.293L8.414 13H18v-2H8.414l4.293-4.293-1.414-1.414L4.586 12l6.707 6.707z' />
</IconSVG>
);
}
export function ArrowRightIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M11.293 17.293l1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z' />
</IconSVG>
);
}
export function LetterAIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M11.307 4l-6 16h2.137l1.875-5h6.363l1.875 5h2.137l-6-16h-2.387zm-1.239 9L12.5 6.515 14.932 13h-4.864z' />
</IconSVG>
);
}
export function LetterALinesIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M15 4h7v2h-7zm1 4h6v2h-6zm2 4h4v2h-4zM9.307 4l-6 16h2.137l1.875-5h6.363l1.875 5h2.137l-6-16H9.307zm-1.239 9L10.5 6.515 12.932 13H8.068z' />
</IconSVG>
);
}
export function InDoorIcon(props: IconProps) { export function InDoorIcon(props: IconProps) {
return ( return (
<IconSVG viewbox='0 0 24 24' {...props}> <IconSVG viewbox='0 0 24 24' {...props}>
@ -150,22 +44,6 @@ export function InDoorIcon(props: IconProps) {
); );
} }
export function DescendingIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M11.998 17l7-8h-14z' />
</IconSVG>
);
}
export function AscendingIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M5 15h14l-7-8z' />
</IconSVG>
);
}
export function CheckboxCheckedIcon() { export function CheckboxCheckedIcon() {
return ( return (
<svg <svg

View File

@ -23,7 +23,7 @@ function DescribeError({error} : {error: ErrorData}) {
} }
if (error.response.status === 404) { if (error.response.status === 404) {
return ( return (
<div className='flex flex-col justify-start'> <div>
<p>{'Обращение к несуществующему API'}</p> <p>{'Обращение к несуществующему API'}</p>
<PrettyJson data={error} /> <PrettyJson data={error} />
</div>); </div>);
@ -32,7 +32,7 @@ function DescribeError({error} : {error: ErrorData}) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const isHtml = isResponseHtml(error.response); const isHtml = isResponseHtml(error.response);
return ( return (
<div className='flex flex-col justify-start'> <div>
<p className='underline'>Ошибка</p> <p className='underline'>Ошибка</p>
<p>{error.message}</p> <p>{error.message}</p>
{error.response.data && (<> {error.response.data && (<>

View File

@ -1,6 +1,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { FaSquarePlus } from 'react-icons/fa6';
import { IoLibrary } from 'react-icons/io5';
import { EducationIcon, LibraryIcon, PlusIcon } from '@/components/Icons'; import { EducationIcon } from '@/components/Icons';
import { useConceptNavigation } from '@/context/NagivationContext'; import { useConceptNavigation } from '@/context/NagivationContext';
import { useConceptTheme } from '@/context/ThemeContext'; import { useConceptTheme } from '@/context/ThemeContext';
@ -41,13 +43,13 @@ function Navigation () {
<NavigationButton <NavigationButton
text='Новая схема' text='Новая схема'
description='Создать новую схему' description='Создать новую схему'
icon={<PlusIcon />} icon={<FaSquarePlus size='1.5rem' />}
onClick={navigateCreateNew} onClick={navigateCreateNew}
/> />
<NavigationButton <NavigationButton
text='Библиотека' text='Библиотека'
description='Библиотека концептуальных схем' description='Библиотека концептуальных схем'
icon={<LibraryIcon />} icon={<IoLibrary size='1.5rem' />}
onClick={navigateLibrary} onClick={navigateLibrary}
/> />
<NavigationButton <NavigationButton

View File

@ -1,4 +1,6 @@
import { InDoorIcon, UserIcon } from '@/components/Icons'; import { FaCircleUser } from 'react-icons/fa6';
import { InDoorIcon } from '@/components/Icons';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NagivationContext'; import { useConceptNavigation } from '@/context/NagivationContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
@ -24,7 +26,7 @@ function UserMenu() {
{user ? {user ?
<NavigationButton <NavigationButton
description={`Пользователь ${user?.username}`} description={`Пользователь ${user?.username}`}
icon={<UserIcon />} icon={<FaCircleUser size='1.5rem' />}
onClick={menu.toggle} onClick={menu.toggle}
/> : null} /> : null}
</div> </div>

View File

@ -87,23 +87,20 @@ function ConstituentaPicker({
value={filterText} value={filterText}
onChange={newValue => setFilterText(newValue)} onChange={newValue => setFilterText(newValue)}
/> />
<div <DataTable dense noHeader noFooter
className='overflow-y-auto text-sm border select-none' className='overflow-y-auto text-sm border select-none'
style={{ maxHeight: size, minHeight: size }} style={{ maxHeight: size, minHeight: size }}
> data={filteredData}
<DataTable dense noHeader noFooter columns={columns}
data={filteredData} conditionalRowStyles={conditionalRowStyles}
columns={columns} noDataComponent={
conditionalRowStyles={conditionalRowStyles} <span className='p-2 min-h-[5rem] flex flex-col justify-center text-center'>
noDataComponent={ <p>Список конституент пуст</p>
<span className='p-2 min-h-[5rem] flex flex-col justify-center text-center'> <p>Измените параметры фильтра</p>
<p>Список конституент пуст</p> </span>
<p>Измените параметры фильтра</p> }
</span> onRowClicked={onSelectValue}
} />
onRowClicked={onSelectValue}
/>
</div>
</div>); </div>);
} }

View File

@ -149,29 +149,28 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
return ( return (
<div className='flex flex-col gap-3'> <div className='flex flex-col gap-3'>
<div className={clsx( <DataTable dense noFooter
'max-h-[5.8rem] min-h-[5.8rem]', className={clsx(
'overflow-y-auto', 'max-h-[5.8rem] min-h-[5.8rem]',
'text-sm', 'overflow-y-auto',
'border', 'text-sm',
'select-none' 'border',
)}> 'select-none'
<DataTable dense noFooter )}
data={state.arguments} data={state.arguments}
columns={columns} columns={columns}
conditionalRowStyles={conditionalRowStyles} conditionalRowStyles={conditionalRowStyles}
noDataComponent={ noDataComponent={
<p className={clsx( <p className={clsx(
'min-h-[3.6rem] w-full', 'min-h-[3.6rem] w-full',
'p-2', 'p-2',
'text-center' 'text-center'
)}> )}>
Аргументы отсутствуют Аргументы отсутствуют
</p> </p>
} }
onRowClicked={handleSelectArgument} onRowClicked={handleSelectArgument}
/> />
</div>
<div className={clsx( <div className={clsx(
'py-1 flex gap-2 justify-center items-center', 'py-1 flex gap-2 justify-center items-center',
@ -192,7 +191,7 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
<div className='flex'> <div className='flex'>
<MiniButton <MiniButton
tooltip='Подставить значение аргумента' tooltip='Подставить значение аргумента'
icon={<BiCheck size='1.25rem' className={!argumentValue || !selectedArgument ? 'text-disabled' : 'clr-text-success'} />} icon={<BiCheck size='1.25rem' className={!!argumentValue && !!selectedArgument ? 'clr-text-success' : ''} />}
disabled={!argumentValue || !selectedArgument} disabled={!argumentValue || !selectedArgument}
onClick={() => handleAssignArgument(selectedArgument!, argumentValue)} onClick={() => handleAssignArgument(selectedArgument!, argumentValue)}
/> />
@ -205,7 +204,7 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
<MiniButton <MiniButton
tooltip='Очистить значение аргумента' tooltip='Очистить значение аргумента'
disabled={!selectedClearable} disabled={!selectedClearable}
icon={<BiX size='1.25rem' className={!selectedClearable ? 'text-disabled' : 'clr-text-warning'}/>} icon={<BiX size='1.25rem' className={selectedClearable ? 'clr-text-warning': ''}/>}
onClick={() => selectedArgument ? handleClearArgument(selectedArgument) : undefined} onClick={() => selectedArgument ? handleClearArgument(selectedArgument) : undefined}
/> />
</div> </div>

View File

@ -76,24 +76,22 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
</div> </div>
</TabList> </TabList>
<div className='w-full'> <TabPanel>
<TabPanel> <EntityTab
<EntityTab initial={initial}
initial={initial} items={items}
items={items} setReference={setReference}
setReference={setReference} setIsValid={setIsValid}
setIsValid={setIsValid} />
/> </TabPanel>
</TabPanel>
<TabPanel> <TabPanel>
<SyntacticTab <SyntacticTab
initial={initial} initial={initial}
setReference={setReference} setReference={setReference}
setIsValid={setIsValid} setIsValid={setIsValid}
/> />
</TabPanel> </TabPanel>
</div>
</Tabs> </Tabs>
</Modal>); </Modal>);
} }

View File

@ -2,7 +2,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useLayoutEffect, useState } from 'react'; import { useLayoutEffect, useState } from 'react';
import { BiCheck, BiChevronsDown } from 'react-icons/bi'; import { BiCheck, BiChevronsDown, BiLeftArrow, BiRightArrow, BiX } from 'react-icons/bi';
import Label from '@/components/Common/Label'; import Label from '@/components/Common/Label';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
@ -10,7 +10,6 @@ import Modal from '@/components/Common/Modal';
import Overlay from '@/components/Common/Overlay'; import Overlay from '@/components/Common/Overlay';
import TextArea from '@/components/Common/TextArea'; import TextArea from '@/components/Common/TextArea';
import HelpButton from '@/components/Help/HelpButton'; import HelpButton from '@/components/Help/HelpButton';
import { ArrowLeftIcon, ArrowRightIcon } from '@/components/Icons';
import SelectGrammeme from '@/components/Shared/SelectGrammeme'; import SelectGrammeme from '@/components/Shared/SelectGrammeme';
import useConceptText from '@/hooks/useConceptText'; import useConceptText from '@/hooks/useConceptText';
import { Grammeme, ITextRequest, IWordForm, IWordFormPlain } from '@/models/language'; import { Grammeme, ITextRequest, IWordForm, IWordFormPlain } from '@/models/language';
@ -118,6 +117,10 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
}); });
} }
function handleResetAll() {
setForms([]);
}
return ( return (
<Modal canSubmit <Modal canSubmit
title='Редактирование словоформ' title='Редактирование словоформ'
@ -150,22 +153,22 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
value={inputText} value={inputText}
onChange={event => setInputText(event.target.value)} onChange={event => setInputText(event.target.value)}
/> />
<div className='max-w-min'> <div className='flex flex-col gap-1'>
<MiniButton
tooltip='Генерировать словоформу'
icon={<ArrowLeftIcon size='1.25rem' className={inputGrams.length == 0 ? 'text-disabled' : 'clr-text-primary'} />}
disabled={textProcessor.loading || inputGrams.length == 0}
onClick={handleInflect}
/>
<MiniButton <MiniButton
tooltip='Определить граммемы' tooltip='Определить граммемы'
icon={<ArrowRightIcon icon={<BiRightArrow
size='1.25rem' size='1.25rem'
className={!inputText ? 'text-disabled' : 'clr-text-primary'} className={inputText ? 'clr-text-primary' : ''}
/>} />}
disabled={textProcessor.loading || !inputText} disabled={textProcessor.loading || !inputText}
onClick={handleParse} onClick={handleParse}
/> />
<MiniButton
tooltip='Генерировать словоформу'
icon={<BiLeftArrow size='1.25rem' className={inputGrams.length !== 0 ? 'clr-text-primary' : ''} />}
disabled={textProcessor.loading || inputGrams.length == 0}
onClick={handleInflect}
/>
</div> </div>
</div> </div>
<SelectGrammeme <SelectGrammeme
@ -182,14 +185,14 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
tooltip='Внести словоформу' tooltip='Внести словоформу'
icon={<BiCheck icon={<BiCheck
size='1.25rem' size='1.25rem'
className={!inputText || inputGrams.length == 0 ? 'text-disabled' : 'clr-text-success'} className={inputText && inputGrams.length !== 0 ? 'clr-text-success' : ''}
/>} />}
disabled={textProcessor.loading || !inputText || inputGrams.length == 0} disabled={textProcessor.loading || !inputText || inputGrams.length == 0}
onClick={handleAddForm} onClick={handleAddForm}
/> />
<MiniButton <MiniButton
tooltip='Генерировать стандартные словоформы' tooltip='Генерировать стандартные словоформы'
icon={<BiChevronsDown size='1.25rem' className={!inputText ? 'text-disabled' : 'clr-text-primary'} icon={<BiChevronsDown size='1.25rem' className={inputText ? 'clr-text-primary' : ''}
/>} />}
disabled={textProcessor.loading || !inputText} disabled={textProcessor.loading || !inputText}
onClick={handleGenerateLexeme} onClick={handleGenerateLexeme}
@ -198,24 +201,23 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
<div className={clsx( <div className={clsx(
'mt-3 mb-2', 'mt-3 mb-2',
'flex justify-center items-center',
'text-sm text-center font-semibold' 'text-sm text-center font-semibold'
)}> )}>
Заданные вручную словоформы [{forms.length}] <span>Заданные вручную словоформы [{forms.length}]</span>
<MiniButton noHover
tooltip='Сбросить все словоформы'
icon={<BiX size='1rem' className={forms.length !== 0 ? 'clr-text-warning' : ''} />}
disabled={textProcessor.loading || forms.length === 0}
onClick={handleResetAll}
/>
</div> </div>
<div className={clsx(
'mb-2',
'max-h-[17.4rem] min-h-[17.4rem]',
'border',
'overflow-y-auto'
)}>
<WordFormsTable <WordFormsTable
forms={forms} forms={forms}
setForms={setForms} setForms={setForms}
onFormSelect={handleSelectForm} onFormSelect={handleSelectForm}
loading={textProcessor.loading}
/> />
</div>
</Modal>); </Modal>);
} }

View File

@ -1,10 +1,10 @@
'use client'; 'use client';
import clsx from 'clsx';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { BiX } from 'react-icons/bi'; import { BiX } from 'react-icons/bi';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
import Overlay from '@/components/Common/Overlay';
import DataTable, { createColumnHelper } from '@/components/DataTable'; import DataTable, { createColumnHelper } from '@/components/DataTable';
import WordFormBadge from '@/components/Shared/WordFormBadge'; import WordFormBadge from '@/components/Shared/WordFormBadge';
import { IWordForm } from '@/models/language'; import { IWordForm } from '@/models/language';
@ -13,12 +13,11 @@ interface WordFormsTableProps {
forms: IWordForm[] forms: IWordForm[]
setForms: React.Dispatch<React.SetStateAction<IWordForm[]>> setForms: React.Dispatch<React.SetStateAction<IWordForm[]>>
onFormSelect?: (form: IWordForm) => void onFormSelect?: (form: IWordForm) => void
loading?: boolean
} }
const columnHelper = createColumnHelper<IWordForm>(); const columnHelper = createColumnHelper<IWordForm>();
function WordFormsTable({ forms, setForms, onFormSelect, loading }: WordFormsTableProps) { function WordFormsTable({ forms, setForms, onFormSelect }: WordFormsTableProps) {
const handleDeleteRow = useCallback( const handleDeleteRow = useCallback(
(row: number) => { (row: number) => {
setForms( setForms(
@ -34,10 +33,6 @@ function WordFormsTable({ forms, setForms, onFormSelect, loading }: WordFormsTab
}); });
}, [setForms]); }, [setForms]);
function handleResetAll() {
setForms([]);
}
const columns = useMemo( const columns = useMemo(
() => [ () => [
columnHelper.accessor('text', { columnHelper.accessor('text', {
@ -68,35 +63,31 @@ function WordFormsTable({ forms, setForms, onFormSelect, loading }: WordFormsTab
cell: props => cell: props =>
<MiniButton noHover <MiniButton noHover
tooltip='Удалить словоформу' tooltip='Удалить словоформу'
icon={<BiX size='1rem' className='text-warning'/>} icon={<BiX size='1rem' className='clr-text-warning'/>}
onClick={() => handleDeleteRow(props.row.index)} onClick={() => handleDeleteRow(props.row.index)}
/> />
}) })
], [handleDeleteRow]); ], [handleDeleteRow]);
return ( return (
<> <DataTable dense noFooter
<Overlay position='top-1 right-4'> className={clsx(
<MiniButton 'mb-2',
tooltip='Сбросить все словоформы' 'max-h-[17.4rem] min-h-[17.4rem]',
icon={<BiX size='1rem' className={forms.length === 0 ? 'text-disabled' : 'text-warning'} />} 'border',
disabled={loading || forms.length === 0} 'overflow-y-auto'
onClick={handleResetAll} )}
/> data={forms}
</Overlay> columns={columns}
<DataTable dense noFooter headPosition='0'
data={forms} noDataComponent={
columns={columns} <span className='p-2 text-center min-h-[2rem]'>
headPosition='0' <p>Список пуст</p>
noDataComponent={ <p>Добавьте словоформу</p>
<span className='p-2 text-center min-h-[2rem]'> </span>
<p>Список пуст</p> }
<p>Добавьте словоформу</p> onRowClicked={onFormSelect}
</span> />);
}
onRowClicked={onFormSelect}
/>
</>);
} }
export default WordFormsTable; export default WordFormsTable;

View File

@ -39,7 +39,7 @@ export function inferTemplatedType(templateType: CstType, args: IArgumentValue[]
* closing bracket ']' to determine the head and body parts. * closing bracket ']' to determine the head and body parts.
* *
* @example * @example
* const template = "[header] body content"; * const template = '[header] body content';
* const result = splitTemplateDefinition(template); * const result = splitTemplateDefinition(template);
* // result: `{ head: 'header', body: 'body content' }` * // result: `{ head: 'header', body: 'body content' }`
*/ */

View File

@ -127,7 +127,7 @@ function CreateRSFormPage() {
value={common} value={common}
setValue={value => setCommon(value ?? false)} setValue={value => setCommon(value ?? false)}
/> />
<div className='flex items-center justify-around py-2'> <div className='flex justify-around gap-6 py-3'>
<SubmitButton <SubmitButton
text='Создать схему' text='Создать схему'
loading={processing} loading={processing}

View File

@ -1,4 +1,3 @@
import clsx from 'clsx';
import { useLayoutEffect } from 'react'; import { useLayoutEffect } from 'react';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
@ -22,10 +21,7 @@ function HomePage() {
}, [router, user]) }, [router, user])
return ( return (
<div className={clsx( <div className='flex flex-col items-center justify-center px-4 py-2'>
'w-full',
'px-4 py-2 flex flex-col justify-center items-center'
)}>
{user?.is_staff ? {user?.is_staff ?
<p> <p>
Лендинг находится в разработке. Данная страница видна только пользователям с правами администратора. Лендинг находится в разработке. Данная страница видна только пользователям с правами администратора.

View File

@ -1,6 +1,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { BiCheckShield, BiShareAlt } from 'react-icons/bi';
import { FiBell } from 'react-icons/fi';
import { EducationIcon, GroupIcon, SubscribedIcon } from '@/components/Icons';
import { ICurrentUser, ILibraryItem } from '@/models/library'; import { ICurrentUser, ILibraryItem } from '@/models/library';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
@ -20,15 +21,15 @@ function ItemIcons({ user, item }: ItemIconsProps) {
> >
{(user && user.subscriptions.includes(item.id)) ? {(user && user.subscriptions.includes(item.id)) ?
<span title='Отслеживаемая'> <span title='Отслеживаемая'>
<SubscribedIcon size='0.75rem' /> <FiBell size='0.75rem' />
</span> : null} </span> : null}
{item.is_common ? {item.is_common ?
<span title='Общедоступная'> <span title='Общедоступная'>
<GroupIcon size='0.75rem'/> <BiShareAlt size='0.75rem'/>
</span> : null} </span> : null}
{item.is_canonical ? {item.is_canonical ?
<span title='Неизменная'> <span title='Неизменная'>
<EducationIcon size='0.75rem'/> <BiCheckShield size='0.75rem'/>
</span> : null} </span> : null}
</div>); </div>);
} }

View File

@ -45,7 +45,7 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setFilter }:
<div className={clsx( <div className={clsx(
'sticky top-0', 'sticky top-0',
'w-full max-h-[2.3rem]', 'w-full max-h-[2.3rem]',
'pr-40 flex justify-start items-stretch', 'pr-40 flex items-stretch',
'border-b', 'border-b',
'clr-input' 'clr-input'
)}> )}>
@ -61,7 +61,7 @@ function SearchPanel({ total, filtered, query, setQuery, strategy, setFilter }:
</span> </span>
</div> </div>
<div className={clsx( <div className={clsx(
'w-full', 'flex-grow',
'flex gap-1 justify-center items-center' 'flex gap-1 justify-center items-center'
)}> )}>
<ConceptSearch noBorder <ConceptSearch noBorder

View File

@ -87,14 +87,13 @@ function LoginPage() {
onChange={event => setPassword(event.target.value)} onChange={event => setPassword(event.target.value)}
/> />
<div className='flex justify-center w-full py-2'> <SubmitButton
<SubmitButton text='Войти'
text='Войти' dimensions='w-[12rem] mt-3'
dimensions='w-[12rem]' className='self-center'
loading={loading} loading={loading}
disabled={!username || !password} disabled={!username || !password}
/> />
</div>
<div className='flex flex-col text-sm'> <div className='flex flex-col text-sm'>
<TextURL text='Восстановить пароль...' href='/restore-password' /> <TextURL text='Восстановить пароль...' href='/restore-password' />
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' /> <TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />

View File

@ -7,7 +7,7 @@ interface ViewTopicProps {
function ViewTopic({ topic }: ViewTopicProps) { function ViewTopic({ topic }: ViewTopicProps) {
return ( return (
<div className='w-full px-2 py-2 max-w-[80rem]'> <div className='px-2 py-2 max-w-[80rem]'>
<InfoTopic topic={topic}/> <InfoTopic topic={topic}/>
</div>); </div>);
} }

View File

@ -1,8 +1,8 @@
'use client'; 'use client';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { BiDiamond, BiDuplicate, BiPlusCircle, BiReset, BiTrash } from 'react-icons/bi'; import { BiDuplicate, BiPlusCircle, BiReset, BiTrash } from 'react-icons/bi';
import { FiSave } from "react-icons/fi"; import { FiSave } from 'react-icons/fi';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
import Overlay from '@/components/Common/Overlay'; import Overlay from '@/components/Common/Overlay';
@ -19,17 +19,16 @@ interface ConstituentaToolbarProps {
onDelete: () => void onDelete: () => void
onClone: () => void onClone: () => void
onCreate: () => void onCreate: () => void
onTemplates: () => void
} }
function ConstituentaToolbar({ function ConstituentaToolbar({
isMutable, isModified, isMutable, isModified,
onSubmit, onReset, onSubmit, onReset,
onDelete, onClone, onCreate, onTemplates onDelete, onClone, onCreate
}: ConstituentaToolbarProps) { }: ConstituentaToolbarProps) {
const canSave = useMemo(() => (isModified && isMutable), [isModified, isMutable]); const canSave = useMemo(() => (isModified && isMutable), [isModified, isMutable]);
return ( return (
<Overlay position='right-1/2 translate-x-1/2 top-1 flex items-start'> <Overlay position='top-1 right-1/2 translate-x-1/2' className='flex'>
<MiniButton <MiniButton
tooltip='Сохранить изменения [Ctrl + S]' tooltip='Сохранить изменения [Ctrl + S]'
disabled={!canSave} disabled={!canSave}
@ -54,12 +53,6 @@ function ConstituentaToolbar({
onClick={onClone} onClick={onClone}
icon={<BiDuplicate size='1.25rem' className={isMutable ? 'clr-text-success' : ''} />} icon={<BiDuplicate size='1.25rem' className={isMutable ? 'clr-text-success' : ''} />}
/> />
<MiniButton
tooltip='Создать конституенту из шаблона [Alt + E]'
icon={<BiDiamond className={isMutable ? 'clr-text-primary': ''} size={'1.25rem'}/>}
disabled={!isMutable}
onClick={onTemplates}
/>
<MiniButton <MiniButton
tooltip='Удалить редактируемую конституенту' tooltip='Удалить редактируемую конституенту'
disabled={!isMutable} disabled={!isMutable}

View File

@ -30,12 +30,11 @@ interface EditorConstituentaProps {
onRenameCst: (initial: ICstRenameData) => void onRenameCst: (initial: ICstRenameData) => void
onEditTerm: () => void onEditTerm: () => void
onDeleteCst: (selected: number[], callback?: (items: number[]) => void) => void onDeleteCst: (selected: number[], callback?: (items: number[]) => void) => void
onTemplates: (insertAfter?: number) => void
} }
function EditorConstituenta({ function EditorConstituenta({
isMutable, isModified, setIsModified, activeID, activeCst, onEditTerm, isMutable, isModified, setIsModified, activeID, activeCst, onEditTerm,
onCreateCst, onRenameCst, onOpenEdit, onDeleteCst, onTemplates onCreateCst, onRenameCst, onOpenEdit, onDeleteCst
}: EditorConstituentaProps) { }: EditorConstituentaProps) {
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const { schema } = useRSForm(); const { schema } = useRSForm();
@ -114,13 +113,13 @@ function EditorConstituenta({
function processAltKey(code: string): boolean { function processAltKey(code: string): boolean {
switch (code) { switch (code) {
case 'KeyE': onTemplates(); return true;
case 'KeyV': handleClone(); return true; case 'KeyV': handleClone(); return true;
} }
return false; return false;
} }
return (<> return (
<>
<ConstituentaToolbar <ConstituentaToolbar
isMutable={!disabled} isMutable={!disabled}
isModified={isModified} isModified={isModified}
@ -131,34 +130,29 @@ function EditorConstituenta({
onDelete={handleDelete} onDelete={handleDelete}
onClone={handleClone} onClone={handleClone}
onCreate={handleCreate} onCreate={handleCreate}
onTemplates={() => onTemplates(activeID)}
/> />
<div tabIndex={-1} <div tabIndex={-1}
className='max-w-[1500px] flex justify-start w-full' className='flex max-w-[95rem]'
onKeyDown={handleInput} onKeyDown={handleInput}
> >
<div className='min-w-[47.8rem] max-w-[47.8rem] px-4 py-1'> <FormConstituenta disabled={disabled}
<FormConstituenta disabled={disabled} id={globalIDs.constituenta_editor}
id={globalIDs.constituenta_editor} constituenta={activeCst}
constituenta={activeCst} isModified={isModified}
isModified={isModified} toggleReset={toggleReset}
toggleReset={toggleReset}
setIsModified={setIsModified} setIsModified={setIsModified}
onEditTerm={onEditTerm} onEditTerm={onEditTerm}
onRenameCst={onRenameCst} onRenameCst={onRenameCst}
/> />
</div>
{(windowSize.width && windowSize.width >= SIDELIST_HIDE_THRESHOLD) ? {(windowSize.width && windowSize.width >= SIDELIST_HIDE_THRESHOLD) ?
<div className='w-full mt-[2.25rem] border h-fit'> <ViewConstituents
<ViewConstituents schema={schema}
schema={schema} expression={activeCst?.definition_formal ?? ''}
expression={activeCst?.definition_formal ?? ''} baseHeight={UNFOLDED_HEIGHT}
baseHeight={UNFOLDED_HEIGHT} activeID={activeID}
activeID={activeID} onOpenEdit={onOpenEdit}
onOpenEdit={onOpenEdit} />: null}
/>
</div> : null}
</div> </div>
</>); </>);
} }

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import { Dispatch, SetStateAction, useLayoutEffect, useState } from 'react'; import clsx from 'clsx';
import { Dispatch, SetStateAction, useEffect, useLayoutEffect, useState } from 'react';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
import { LiaEdit } from 'react-icons/lia'; import { LiaEdit } from 'react-icons/lia';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -45,7 +46,7 @@ function FormConstituenta({
const [convention, setConvention] = useState(''); const [convention, setConvention] = useState('');
const [typification, setTypification] = useState('N/A'); const [typification, setTypification] = useState('N/A');
useLayoutEffect( useEffect(
() => { () => {
if (!constituenta) { if (!constituenta) {
setIsModified(false); setIsModified(false);
@ -105,7 +106,10 @@ function FormConstituenta({
} }
return (<> return (<>
<Overlay position='top-0 left-[3rem]' className='flex justify-start select-none' > <Overlay
position='top-1 left-[4rem]'
className='flex select-none'
>
<MiniButton <MiniButton
tooltip={`Редактировать словоформы термина: ${constituenta?.term_forms.length ?? 0}`} tooltip={`Редактировать словоформы термина: ${constituenta?.term_forms.length ?? 0}`}
disabled={disabled} disabled={disabled}
@ -113,7 +117,7 @@ function FormConstituenta({
onClick={onEditTerm} onClick={onEditTerm}
icon={<LiaEdit size='1rem' className={!disabled ? 'clr-text-primary' : ''} />} icon={<LiaEdit size='1rem' className={!disabled ? 'clr-text-primary' : ''} />}
/> />
<div className='pt-1 pl-[1.375rem] text-sm font-semibold w-fit'> <div className='pt-1 pl-[1.375rem] text-sm font-semibold whitespace-nowrap w-fit'>
<span>Имя </span> <span>Имя </span>
<span className='ml-1'>{constituenta?.alias ?? ''}</span> <span className='ml-1'>{constituenta?.alias ?? ''}</span>
</div> </div>
@ -125,7 +129,10 @@ function FormConstituenta({
/> />
</Overlay> </Overlay>
<form id={id} <form id={id}
className='flex flex-col gap-3 mt-1' className={clsx(
'mt-1 min-w-[47.8rem] max-w-[47.8rem]',
'px-4 py-1 flex flex-col gap-3'
)}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<RefsInput <RefsInput
@ -138,16 +145,14 @@ function FormConstituenta({
disabled={disabled} disabled={disabled}
onChange={newValue => setTerm(newValue)} onChange={newValue => setTerm(newValue)}
/> />
<TextArea dense noBorder <TextArea dense noBorder disabled
label='Типизация' label='Типизация'
rows={typification.length > 70 ? 2 : 1} rows={typification.length > 70 ? 2 : 1}
value={typification} value={typification}
colors='clr-app' colors='clr-app'
dimensions='w-full'
style={{ style={{
resize: 'none' resize: 'none'
}} }}
disabled
/> />
<EditorRSExpression <EditorRSExpression
label='Формальное определение' label='Формальное определение'
@ -176,13 +181,12 @@ function FormConstituenta({
disabled={disabled} disabled={disabled}
onChange={event => setConvention(event.target.value)} onChange={event => setConvention(event.target.value)}
/> />
<div className='flex justify-center w-full'> <SubmitButton
<SubmitButton text='Сохранить изменения'
text='Сохранить изменения' className='self-center'
disabled={!isModified || disabled} disabled={!isModified || disabled}
icon={<FiSave size='1.5rem' />} icon={<FiSave size='1.5rem' />}
/> />
</div>
</form> </form>
</>); </>);
} }

View File

@ -123,14 +123,15 @@ function EditorRSExpression({
}); });
} }
return ( return (<>
<div className='flex flex-col items-start w-full'> {showAST ?
{showAST ? <DlgShowAST
<DlgShowAST expression={expression}
expression={expression} syntaxTree={syntaxTree}
syntaxTree={syntaxTree} hideWindow={() => setShowAST(false)}
hideWindow={() => setShowAST(false)} /> : null}
/> : null}
<div>
<Overlay position='top-[-0.375rem] left-[11rem]'> <Overlay position='top-[-0.375rem] left-[11rem]'>
<MiniButton noHover <MiniButton noHover
tooltip='Дерево разбора выражения' tooltip='Дерево разбора выражения'
@ -160,7 +161,8 @@ function EditorRSExpression({
onCheckExpression={handleCheckExpression} onCheckExpression={handleCheckExpression}
onShowError={onShowError} onShowError={onShowError}
/> />
</div>); </div>
</>);
} }
export default EditorRSExpression; export default EditorRSExpression;

View File

@ -33,7 +33,7 @@ function StatusBar({ isModified, constituenta, parseData }: StatusBarProps) {
return ( return (
<div title={describeExpressionStatus(status)} <div title={describeExpressionStatus(status)}
className={clsx( className={clsx(
'w-full h-full', 'h-full',
'border rounded-none', 'border rounded-none',
'text-sm font-semibold small-caps text-center', 'text-sm font-semibold small-caps text-center',
'select-none' 'select-none'

View File

@ -49,7 +49,7 @@ function EditorRSForm({
} }
return ( return (
<div tabIndex={-1} onKeyDown={handleInput}> <>
<RSFormToolbar <RSFormToolbar
isMutable={isMutable} isMutable={isMutable}
processing={processing} processing={processing}
@ -65,26 +65,25 @@ function EditorRSForm({
onDestroy={onDestroy} onDestroy={onDestroy}
onToggleSubscribe={onToggleSubscribe} onToggleSubscribe={onToggleSubscribe}
/> />
<div className='flex w-full'> <div tabIndex={-1}
<div className='flex-grow max-w-[40rem] min-w-[30rem] px-4 pb-2'> className='flex'
<div className='flex flex-col gap-3'> onKeyDown={handleInput}
<FormRSForm disabled={!isMutable} >
id={globalIDs.library_item_editor} <div className='flex flex-col gap-3 px-4 pb-2'>
isModified={isModified} <FormRSForm disabled={!isMutable}
setIsModified={setIsModified} id={globalIDs.library_item_editor}
/> isModified={isModified}
setIsModified={setIsModified}
/>
<Divider margins='my-2' /> <Divider margins='my-2' />
<InfoLibraryItem item={schema} /> <InfoLibraryItem item={schema} />
</div>
</div> </div>
<Divider vertical />
<RSFormStats stats={schema?.stats}/> <RSFormStats stats={schema?.stats}/>
</div> </div>
</div>); </>);
} }
export default EditorRSForm; export default EditorRSForm;

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { Dispatch, SetStateAction, useLayoutEffect, useState } from 'react'; import { Dispatch, SetStateAction, useEffect, useLayoutEffect, 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';
@ -33,7 +33,7 @@ function FormRSForm({
const [common, setCommon] = useState(false); const [common, setCommon] = useState(false);
const [canonical, setCanonical] = useState(false); const [canonical, setCanonical] = useState(false);
useLayoutEffect( useEffect(
() => { () => {
if (!schema) { if (!schema) {
setIsModified(false); setIsModified(false);
@ -79,7 +79,7 @@ function FormRSForm({
return ( return (
<form id={id} <form id={id}
className='flex flex-col gap-3 mt-2' className='flex flex-col gap-3 mt-1 py-1 min-w-[22rem] w-[30rem]'
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<TextInput required <TextInput required
@ -121,15 +121,14 @@ function FormRSForm({
setValue={value => setCanonical(value)} setValue={value => setCanonical(value)}
/> />
</div> </div>
<div className='flex justify-center w-full'> <SubmitButton
<SubmitButton text='Сохранить изменения'
text='Сохранить изменения' className='self-center'
loading={processing} dimensions='my-2 w-fit'
disabled={!isModified || disabled} loading={processing}
icon={<FiSave size='1.5rem' />} disabled={!isModified || disabled}
dimensions='my-2 w-fit' icon={<FiSave size='1.5rem' />}
/> />
</div>
</form>); </form>);
} }

View File

@ -11,7 +11,7 @@ function RSFormStats({ stats }: RSFormStatsProps) {
return null; return null;
} }
return ( return (
<div className='flex flex-col gap-1 px-4 mt-8 min-w-[16rem]'> <div className='flex flex-col gap-1 px-4 mt-8 min-w-[16rem] max-w-[16rem]'>
<LabeledValue id='count_all' <LabeledValue id='count_all'
label='Всего конституент ' label='Всего конституент '
text={stats.count_all} text={stats.count_all}

View File

@ -1,14 +1,13 @@
'use client'; 'use client';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { BiDownload, BiTrash } from 'react-icons/bi'; import { BiDownload, BiShareAlt, BiTrash } from 'react-icons/bi';
import { FiSave } from 'react-icons/fi'; import { FiBell, FiBellOff, FiSave } from 'react-icons/fi';
import { LuCrown } from 'react-icons/lu'; import { LuCrown } from 'react-icons/lu';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
import Overlay from '@/components/Common/Overlay'; import Overlay from '@/components/Common/Overlay';
import HelpButton from '@/components/Help/HelpButton'; import HelpButton from '@/components/Help/HelpButton';
import { NotSubscribedIcon, ShareIcon, SubscribedIcon } from '@/components/Icons';
import { HelpTopic } from '@/models/miscelanious'; import { HelpTopic } from '@/models/miscelanious';
interface RSFormToolbarProps { interface RSFormToolbarProps {
@ -35,7 +34,7 @@ function RSFormToolbar({
}: RSFormToolbarProps) { }: RSFormToolbarProps) {
const canSave = useMemo(() => (modified && isMutable), [modified, isMutable]); const canSave = useMemo(() => (modified && isMutable), [modified, isMutable]);
return ( return (
<Overlay position='w-full top-1 flex items-start justify-center'> <Overlay position='top-1 right-1/2 translate-x-1/2' className='flex'>
<MiniButton <MiniButton
tooltip='Сохранить изменения [Ctrl + S]' tooltip='Сохранить изменения [Ctrl + S]'
disabled={!canSave} disabled={!canSave}
@ -44,7 +43,7 @@ function RSFormToolbar({
/> />
<MiniButton <MiniButton
tooltip='Поделиться схемой' tooltip='Поделиться схемой'
icon={<ShareIcon size='1.25rem' className='clr-text-primary'/>} icon={<BiShareAlt size='1.25rem' className='clr-text-primary'/>}
onClick={onShare} onClick={onShare}
/> />
<MiniButton <MiniButton
@ -56,10 +55,10 @@ function RSFormToolbar({
tooltip={'отслеживание: ' + (isSubscribed ? '[включено]' : '[выключено]')} tooltip={'отслеживание: ' + (isSubscribed ? '[включено]' : '[выключено]')}
disabled={anonymous || processing} disabled={anonymous || processing}
icon={isSubscribed icon={isSubscribed
? <SubscribedIcon size='1.25rem' className='clr-text-primary' /> ? <FiBell size='1.25rem' className='clr-text-primary' />
: <NotSubscribedIcon size='1.25rem' className='clr-text-controls' /> : <FiBellOff size='1.25rem' className='clr-text-controls' />
} }
dimensions='h-full w-fit pr-2' dimensions='h-full w-fit'
style={{outlineColor: 'transparent'}} style={{outlineColor: 'transparent'}}
onClick={onToggleSubscribe} onClick={onToggleSubscribe}
/> />

View File

@ -13,16 +13,14 @@ import RSTable from './RSTable';
interface EditorRSListProps { interface EditorRSListProps {
isMutable: boolean isMutable: boolean
onOpenEdit: (cstID: number) => void onOpenEdit: (cstID: number) => void
onTemplates: (insertAfter?: number) => void
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void
onReindex: () => void
} }
function EditorRSList({ function EditorRSList({
isMutable, isMutable,
onOpenEdit, onCreateCst, onOpenEdit, onCreateCst,
onDeleteCst, onTemplates, onReindex onDeleteCst
}: EditorRSListProps) { }: EditorRSListProps) {
const { schema, cstMoveTo } = useRSForm(); const { schema, cstMoveTo } = useRSForm();
const [selected, setSelected] = useState<number[]>([]); const [selected, setSelected] = useState<number[]>([]);
@ -185,8 +183,6 @@ function EditorRSList({
} }
switch (code) { switch (code) {
case 'Backquote': handleCreateCst(); return true; case 'Backquote': handleCreateCst(); return true;
case 'KeyE': onTemplates(); return true;
case 'KeyR': onReindex(); return true;
case 'Digit1': handleCreateCst(CstType.BASE); return true; case 'Digit1': handleCreateCst(CstType.BASE); return true;
case 'Digit2': handleCreateCst(CstType.STRUCTURED); return true; case 'Digit2': handleCreateCst(CstType.STRUCTURED); return true;
@ -202,7 +198,7 @@ function EditorRSList({
return ( return (
<div tabIndex={-1} <div tabIndex={-1}
className='w-full outline-none' className='outline-none'
onKeyDown={handleTableKey} onKeyDown={handleTableKey}
> >
<RSListToolbar <RSListToolbar
@ -213,8 +209,6 @@ function EditorRSList({
onClone={handleClone} onClone={handleClone}
onCreate={handleCreateCst} onCreate={handleCreateCst}
onDelete={handleDelete} onDelete={handleDelete}
onTemplates={() => onTemplates(selected.length !== 0 ? selected[selected.length-1] : undefined)}
onReindex={onReindex}
/> />
<SelectedCounter <SelectedCounter
total={schema?.stats?.count_all ?? 0} total={schema?.stats?.count_all ?? 0}

View File

@ -1,7 +1,10 @@
'use client'; 'use client';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { BiAnalyse, BiDiamond, BiDownArrowCircle, BiDownvote, BiDuplicate, BiPlusCircle, BiTrash, BiUpvote } from "react-icons/bi"; import {
BiDownArrowCircle, BiDownvote, BiDuplicate,
BiPlusCircle, BiTrash, BiUpvote
} from 'react-icons/bi';
import Dropdown from '@/components/Common/Dropdown'; import Dropdown from '@/components/Common/Dropdown';
import DropdownButton from '@/components/Common/DropdownButton'; import DropdownButton from '@/components/Common/DropdownButton';
@ -24,29 +27,27 @@ interface RSListToolbarProps {
onDelete: () => void onDelete: () => void
onClone: () => void onClone: () => void
onCreate: (type?: CstType) => void onCreate: (type?: CstType) => void
onTemplates: () => void
onReindex: () => void
} }
function RSListToolbar({ function RSListToolbar({
selectedCount, isMutable, selectedCount, isMutable,
onMoveUp, onMoveDown, onDelete, onClone, onMoveUp, onMoveDown, onDelete, onClone,
onCreate, onTemplates, onReindex onCreate
}: RSListToolbarProps) { }: RSListToolbarProps) {
const insertMenu = useDropdown(); const insertMenu = useDropdown();
const nothingSelected = useMemo(() => selectedCount === 0, [selectedCount]); const nothingSelected = useMemo(() => selectedCount === 0, [selectedCount]);
return ( return (
<Overlay position='w-full top-1 flex items-start justify-center'> <Overlay position='top-1 right-1/2 translate-x-1/2' className='flex'>
<MiniButton <MiniButton
tooltip='Переместить вверх [Alt + вверх]' tooltip='Переместить вверх [Alt + вверх]'
icon={<BiUpvote size='1.25rem'/>} icon={<BiUpvote size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-primary': ''}/>}
disabled={!isMutable || nothingSelected} disabled={!isMutable || nothingSelected}
onClick={onMoveUp} onClick={onMoveUp}
/> />
<MiniButton <MiniButton
tooltip='Переместить вниз [Alt + вниз]' tooltip='Переместить вниз [Alt + вниз]'
icon={<BiDownvote size='1.25rem'/>} icon={<BiDownvote size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-primary': ''}/>}
disabled={!isMutable || nothingSelected} disabled={!isMutable || nothingSelected}
onClick={onMoveDown} onClick={onMoveDown}
/> />
@ -84,18 +85,6 @@ function RSListToolbar({
</Dropdown> : null} </Dropdown> : null}
</div> </div>
</div> </div>
<MiniButton
tooltip='Создать конституенту из шаблона [Alt + E]'
icon={<BiDiamond size='1.25rem' className={isMutable ? 'clr-text-primary': ''} />}
disabled={!isMutable}
onClick={onTemplates}
/>
<MiniButton
tooltip='Сброс имён: присвоить порядковые имена [Alt + R]'
icon={<BiAnalyse size='1.25rem' className={isMutable ? 'clr-text-primary': ''} />}
disabled={!isMutable}
onClick={onReindex}
/>
<MiniButton <MiniButton
tooltip='Удалить выбранные [Delete]' tooltip='Удалить выбранные [Delete]'
icon={<BiTrash size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-warning' : ''} />} icon={<BiTrash size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-warning' : ''} />}

View File

@ -125,16 +125,15 @@ function RSTable({
}, [noNavigation]); }, [noNavigation]);
return ( return (
<div <DataTable dense noFooter
className={clsx( className={clsx(
'w-full h-full min-h-[20rem]', 'min-h-[20rem]',
'overflow-auto', 'overflow-auto',
'text-sm', 'text-sm',
'select-none' 'select-none'
)} )}
style={{maxHeight: tableHeight}} style={{maxHeight: tableHeight}}
>
<DataTable dense noFooter
data={items ?? []} data={items ?? []}
columns={columns} columns={columns}
headPosition='0rem' headPosition='0rem'
@ -161,8 +160,7 @@ function RSTable({
</p> </p>
</span> </span>
} }
/> />);
</div>);
} }
export default RSTable; export default RSTable;

View File

@ -18,7 +18,7 @@ function GraphSidebar({
layout, setLayout layout, setLayout
} : GraphSidebarProps) { } : GraphSidebarProps) {
return ( return (
<div className='flex flex-col px-2 text-sm select-none mt-9 h-fit'> <div className='px-2 text-sm select-none mt-9'>
<SelectSingle <SelectSingle
placeholder='Выберите цвет' placeholder='Выберите цвет'
options={SelectorGraphColoring} options={SelectorGraphColoring}
@ -28,7 +28,6 @@ function GraphSidebar({
/> />
<SelectSingle <SelectSingle
placeholder='Способ расположения' placeholder='Способ расположения'
className='w-full'
options={SelectorGraphLayout} options={SelectorGraphLayout}
isSearchable={false} isSearchable={false}
value={layout ? { value: layout, label: mapLableLayout.get(layout) } : null} value={layout ? { value: layout, label: mapLableLayout.get(layout) } : null}

View File

@ -1,11 +1,10 @@
'use client'; 'use client';
import { BiCollapse, BiFilterAlt, BiPlanet, BiPlusCircle, BiTrash } from 'react-icons/bi'; import { BiCollapse, BiFilterAlt, BiFont, BiFontFamily, BiPlanet, BiPlusCircle, BiTrash } from 'react-icons/bi';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
import Overlay from '@/components/Common/Overlay'; import Overlay from '@/components/Common/Overlay';
import HelpButton from '@/components/Help/HelpButton'; import HelpButton from '@/components/Help/HelpButton';
import { LetterAIcon, LetterALinesIcon } from '@/components/Icons';
import { HelpTopic } from '@/models/miscelanious'; import { HelpTopic } from '@/models/miscelanious';
interface GraphToolbarProps { interface GraphToolbarProps {
@ -33,7 +32,7 @@ function GraphToolbar({
onCreate, onDelete, onResetViewpoint onCreate, onDelete, onResetViewpoint
} : GraphToolbarProps) { } : GraphToolbarProps) {
return ( return (
<Overlay position='w-full top-1 right-0 flex items-start justify-center'> <Overlay position='top-1 right-1/2 translate-x-1/2' className='flex'>
<MiniButton <MiniButton
tooltip='Настройки фильтрации узлов и связей' tooltip='Настройки фильтрации узлов и связей'
icon={<BiFilterAlt size='1.25rem' className='clr-text-primary' />} icon={<BiFilterAlt size='1.25rem' className='clr-text-primary' />}
@ -43,8 +42,8 @@ function GraphToolbar({
tooltip={!noText ? 'Скрыть текст' : 'Отобразить текст'} tooltip={!noText ? 'Скрыть текст' : 'Отобразить текст'}
icon={ icon={
!noText !noText
? <LetterALinesIcon size='1.25rem' className='clr-text-success' /> ? <BiFontFamily size='1.25rem' className='clr-text-success' />
: <LetterAIcon size='1.25rem' className='clr-text-primary' /> : <BiFont size='1.25rem' className='clr-text-primary' />
} }
onClick={toggleNoText} onClick={toggleNoText}
/> />

View File

@ -46,7 +46,7 @@ export enum RSTabID {
function ProcessError({error}: {error: ErrorData}): React.ReactElement { function ProcessError({error}: {error: ErrorData}): React.ReactElement {
if (axios.isAxiosError(error) && error.response && error.response.status === 404) { if (axios.isAxiosError(error) && error.response && error.response.status === 404) {
return ( return (
<div className='flex flex-col items-center justify-center w-full p-2'> <div className='p-2 text-center'>
<p>Схема с указанным идентификатором отсутствует на портале.</p> <p>Схема с указанным идентификатором отсутствует на портале.</p>
<TextURL text='Перейти в Библиотеку' href='/library'/> <TextURL text='Перейти в Библиотеку' href='/library'/>
</div> </div>
@ -403,11 +403,10 @@ function RSTabs() {
onSelect={onSelectTab} onSelect={onSelectTab}
defaultFocus defaultFocus
selectedTabClassName='clr-selected' selectedTabClassName='clr-selected'
className='flex flex-col w-full' className='flex flex-col items-center min-w-[45rem]'
> >
<div className='flex justify-center w-[100vw]'>
<TabList className={clsx( <TabList className={clsx(
'w-fit h-[1.9rem]', 'h-[1.9rem]',
'flex justify-stretch', 'flex justify-stretch',
'border-b-2 border-x-2 divide-x-2' 'border-b-2 border-x-2 divide-x-2'
)}> )}>
@ -435,63 +434,52 @@ function RSTabs() {
<ConceptTab label='Редактор' /> <ConceptTab label='Редактор' />
<ConceptTab label='Граф термов' /> <ConceptTab label='Граф термов' />
</TabList> </TabList>
</div>
<div <div className='overflow-y-auto' style={{ maxHeight: panelHeight}}>
className={clsx(
'min-w-[48rem] w-[100vw]',
'flex justify-center',
'overflow-y-auto'
)}
style={{ maxHeight: panelHeight}}
>
<TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '': 'none' }}> <TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '': 'none' }}>
<EditorRSForm <EditorRSForm
isMutable={isMutable} isMutable={isMutable}
isModified={isModified} isModified={isModified}
setIsModified={setIsModified} setIsModified={setIsModified}
onToggleSubscribe={handleToggleSubscribe} onToggleSubscribe={handleToggleSubscribe}
onDownload={onDownloadSchema} onDownload={onDownloadSchema}
onDestroy={onDestroySchema} onDestroy={onDestroySchema}
onClaim={onClaimSchema} onClaim={onClaimSchema}
onShare={onShareSchema} onShare={onShareSchema}
/> />
</TabPanel> </TabPanel>
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '': 'none' }}> <TabPanel forceRender style={{ display: activeTab === RSTabID.CST_LIST ? '': 'none' }}>
<EditorRSList <EditorRSList
isMutable={isMutable} isMutable={isMutable}
onOpenEdit={onOpenCst} onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst} onCreateCst={promptCreateCst}
onDeleteCst={promptDeleteCst} onDeleteCst={promptDeleteCst}
onTemplates={onShowTemplates} />
onReindex={onReindex}
/>
</TabPanel> </TabPanel>
<TabPanel forceRender style={{ display: activeTab === RSTabID.CST_EDIT ? '': 'none' }}> <TabPanel forceRender style={{ display: activeTab === RSTabID.CST_EDIT ? '': 'none' }}>
<EditorConstituenta <EditorConstituenta
isMutable={isMutable} isMutable={isMutable}
isModified={isModified} isModified={isModified}
setIsModified={setIsModified} setIsModified={setIsModified}
activeID={activeID} activeID={activeID}
activeCst={activeCst} activeCst={activeCst}
onOpenEdit={onOpenCst} onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst} onCreateCst={promptCreateCst}
onDeleteCst={promptDeleteCst} onDeleteCst={promptDeleteCst}
onRenameCst={promptRenameCst} onRenameCst={promptRenameCst}
onEditTerm={promptShowEditTerm} onEditTerm={promptShowEditTerm}
onTemplates={onShowTemplates} />
/>
</TabPanel> </TabPanel>
<TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '': 'none' }}> <TabPanel style={{ display: activeTab === RSTabID.TERM_GRAPH ? '': 'none' }}>
<EditorTermGraph <EditorTermGraph
isMutable={isMutable} isMutable={isMutable}
onOpenEdit={onOpenCst} onOpenEdit={onOpenCst}
onCreateCst={promptCreateCst} onCreateCst={promptCreateCst}
onDeleteCst={promptDeleteCst} onDeleteCst={promptDeleteCst}
/> />
</TabPanel> </TabPanel>
</div> </div>
</Tabs> : null} </Tabs> : null}

View File

@ -1,13 +1,12 @@
'use client'; 'use client';
import { BiAnalyse, BiDiamond, BiDownload, BiDuplicate, BiMenu, BiMeteor, BiPlusCircle, BiTrash, BiUpload } from 'react-icons/bi'; import { BiAnalyse, BiDiamond, BiDownload, BiDuplicate, BiMenu, BiMeteor, BiPlusCircle, BiShareAlt, BiTrash, BiUpload } from 'react-icons/bi';
import { FiEdit } from 'react-icons/fi'; import { FiEdit } from 'react-icons/fi';
import { LuCrown, LuGlasses } from 'react-icons/lu'; import { LuCrown, LuGlasses } from 'react-icons/lu';
import Button from '@/components/Common/Button'; import Button from '@/components/Common/Button';
import Dropdown from '@/components/Common/Dropdown'; import Dropdown from '@/components/Common/Dropdown';
import DropdownButton from '@/components/Common/DropdownButton'; import DropdownButton from '@/components/Common/DropdownButton';
import { ShareIcon } from '@/components/Icons';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NagivationContext'; import { useConceptNavigation } from '@/context/NagivationContext';
@ -115,7 +114,7 @@ function RSTabsMenu({
/> />
<DropdownButton <DropdownButton
text='Поделиться' text='Поделиться'
icon={<ShareIcon size='1rem' className='clr-text-primary' />} icon={<BiShareAlt size='1rem' className='clr-text-primary' />}
onClick={handleShare} onClick={handleShare}
/> />
<DropdownButton disabled={!user} <DropdownButton disabled={!user}

View File

@ -17,12 +17,14 @@ interface ConstituentsTableProps {
activeID?: number activeID?: number
onOpenEdit: (cstID: number) => void onOpenEdit: (cstID: number) => void
denseThreshold?: number denseThreshold?: number
maxHeight: string
} }
const columnHelper = createColumnHelper<IConstituenta>(); const columnHelper = createColumnHelper<IConstituenta>();
function ConstituentsTable({ function ConstituentsTable({
items, activeID, onOpenEdit, items, activeID, onOpenEdit,
maxHeight,
denseThreshold = 9999 denseThreshold = 9999
}: ConstituentsTableProps) { }: ConstituentsTableProps) {
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
@ -107,6 +109,8 @@ function ConstituentsTable({
return ( return (
<DataTable dense noFooter <DataTable dense noFooter
className='overflow-y-auto text-sm select-none overscroll-none'
style={{maxHeight : maxHeight}}
data={items} data={items}
columns={columns} columns={columns}
conditionalRowStyles={conditionalRowStyles} conditionalRowStyles={conditionalRowStyles}
@ -119,7 +123,7 @@ function ConstituentsTable({
noDataComponent={ noDataComponent={
<div className={clsx( <div className={clsx(
'min-h-[5rem]', 'min-h-[5rem]',
'p-2 flex flex-col justify-center', 'p-2',
'text-center', 'text-center',
'select-none' 'select-none'
)}> )}>

View File

@ -35,22 +35,21 @@ function ViewConstituents({ expression, baseHeight, schema, activeID, onOpenEdit
: `calc(min(100vh - 11.7rem, ${siblingHeight}))`); : `calc(min(100vh - 11.7rem, ${siblingHeight}))`);
}, [noNavigation, baseHeight]); }, [noNavigation, baseHeight]);
return (<> return (
<div className='mt-[2.25rem] border'>
<ConstituentsSearch <ConstituentsSearch
schema={schema} schema={schema}
activeID={activeID} activeID={activeID}
activeExpression={expression} activeExpression={expression}
setFiltered={setFilteredData} setFiltered={setFilteredData}
/> />
<div className='overflow-y-auto text-sm select-none overscroll-none' style={{maxHeight : `${maxHeight}`}}> <ConstituentsTable maxHeight={maxHeight}
<ConstituentsTable
items={filteredData} items={filteredData}
activeID={activeID} activeID={activeID}
onOpenEdit={onOpenEdit} onOpenEdit={onOpenEdit}
denseThreshold={COLUMN_EXPRESSION_HIDE_THRESHOLD} denseThreshold={COLUMN_EXPRESSION_HIDE_THRESHOLD}
/> />
</div> </div>);
</>);
} }
export default ViewConstituents; export default ViewConstituents;

View File

@ -130,7 +130,7 @@ function RegisterPage() {
</div> </div>
</div> </div>
<div className='flex text-sm'> <div className='flex gap-1 text-sm'>
<Checkbox <Checkbox
label='Принимаю условия' label='Принимаю условия'
value={acceptPrivacy} value={acceptPrivacy}
@ -142,7 +142,7 @@ function RegisterPage() {
/> />
</div> </div>
<div className='flex items-center justify-around w-full my-3'> <div className='flex justify-around my-3'>
<SubmitButton <SubmitButton
text='Регистрировать' text='Регистрировать'
dimensions='min-w-[10rem]' dimensions='min-w-[10rem]'

View File

@ -100,15 +100,14 @@ function EditorPassword() {
setNewPasswordRepeat(event.target.value); setNewPasswordRepeat(event.target.value);
}} }}
/> />
{error ? <ProcessError error={error} /> : null}
</div> </div>
{error ? <ProcessError error={error} /> : null} <SubmitButton
<div className='flex justify-center w-full'> text='Сменить пароль'
<SubmitButton className='self-center'
text='Сменить пароль' disabled={!canSubmit}
disabled={!canSubmit} loading={loading}
loading={loading} />
/>
</div>
</form>); </form>);
} }

View File

@ -53,37 +53,34 @@ function EditorProfile() {
return ( return (
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className='px-6 py-2 flex flex-col gap-8 min-w-[18rem]' className='px-6 py-2 flex flex-col gap-3 min-w-[18rem]'
> >
<div className='flex flex-col gap-3'> <TextInput id='username' disabled
<TextInput id='username' disabled label='Логин'
label='Логин' tooltip='Логин изменить нельзя'
tooltip='Логин изменить нельзя' value={username}
value={username} />
/> <TextInput id='first_name' allowEnter
<TextInput id='first_name' allowEnter label='Имя'
label='Имя' value={first_name}
value={first_name} onChange={event => setFirstName(event.target.value)}
onChange={event => setFirstName(event.target.value)} />
/> <TextInput id='last_name' allowEnter
<TextInput id='last_name' allowEnter label='Фамилия'
label='Фамилия' value={last_name}
value={last_name} onChange={event => setLastName(event.target.value)}
onChange={event => setLastName(event.target.value)} />
/> <TextInput id='email' allowEnter
<TextInput id='email' allowEnter label='Электронная почта'
label='Электронная почта' value={email}
value={email} onChange={event => setEmail(event.target.value)}
onChange={event => setEmail(event.target.value)} />
/> <SubmitButton
</div> className='self-center mt-6'
<div className='flex justify-center w-full'> text='Сохранить данные'
<SubmitButton loading={processing}
text='Сохранить данные' disabled={!isModified}
loading={processing} />
disabled={!isModified}
/>
</div>
</form>); </form>);
} }

View File

@ -1,11 +1,11 @@
'use client'; 'use client';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { FiBell, FiBellOff } from 'react-icons/fi';
import { ConceptLoader } from '@/components/Common/ConceptLoader'; import { ConceptLoader } from '@/components/Common/ConceptLoader';
import MiniButton from '@/components/Common/MiniButton'; import MiniButton from '@/components/Common/MiniButton';
import Overlay from '@/components/Common/Overlay'; import Overlay from '@/components/Common/Overlay';
import { NotSubscribedIcon,SubscribedIcon } from '@/components/Icons';
import InfoError from '@/components/InfoError'; import InfoError from '@/components/InfoError';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
@ -32,27 +32,27 @@ function UserTabs() {
{loading ? <ConceptLoader /> : null} {loading ? <ConceptLoader /> : null}
{error ? <InfoError error={error} /> : null} {error ? <InfoError error={error} /> : null}
{user ? {user ?
<div className='flex justify-center gap-2 py-2'> <div className='flex gap-6 py-2'>
<div className='flex flex-col gap-2 min-w-max'> <div>
<Overlay position='mt-2 top-0 right-0'> <Overlay position='top-0 right-0'>
<MiniButton <MiniButton
tooltip='Показать/Скрыть список отслеживаний' tooltip='Показать/Скрыть список отслеживаний'
icon={showSubs icon={showSubs
? <SubscribedIcon size='1.25rem' className='clr-text-primary' /> ? <FiBell size='1.25rem' className='clr-text-primary' />
: <NotSubscribedIcon size='1.25rem' className='clr-text-primary' /> : <FiBellOff size='1.25rem' className='clr-text-primary' />
} }
onClick={() => setShowSubs(prev => !prev)} onClick={() => setShowSubs(prev => !prev)}
/> />
</Overlay> </Overlay>
<h1>Учетные данные пользователя</h1> <h1 className='mb-4'>Учетные данные пользователя</h1>
<div className='flex justify-center py-2 max-w-fit'> <div className='flex py-2 max-w-fit'>
<EditorProfile /> <EditorProfile />
<EditorPassword /> <EditorPassword />
</div> </div>
</div> </div>
{(subscriptions.length > 0 && showSubs) ? {(subscriptions.length > 0 && showSubs) ?
<div className='flex flex-col w-full gap-6 pl-4'> <div>
<h1>Отслеживаемые схемы</h1> <h1 className='mb-6'>Отслеживаемые схемы</h1>
<ViewSubscriptions items={subscriptions} /> <ViewSubscriptions items={subscriptions} />
</div> : null} </div> : null}
</div> : null} </div> : null}

View File

@ -52,8 +52,8 @@ function ViewSubscriptions({items}: ViewSubscriptionsProps) {
], [intl]); ], [intl]);
return ( return (
<div className='max-h-[23.8rem] w-fit overflow-auto text-sm border'>
<DataTable dense noFooter <DataTable dense noFooter
className='max-h-[23.8rem] overflow-auto text-sm border'
columns={columns} columns={columns}
data={items} data={items}
headPosition='0' headPosition='0'
@ -63,11 +63,14 @@ function ViewSubscriptions({items}: ViewSubscriptionsProps) {
id: 'time_update', id: 'time_update',
desc: true desc: true
}} }}
noDataComponent={<div className='h-[10rem]'>Отслеживаемые схемы отсутствуют</div>} noDataComponent={
<div className='h-[10rem]'>
Отслеживаемые схемы отсутствуют
</div>
}
onRowClicked={openRSForm} onRowClicked={openRSForm}
/> />);
</div>);
} }
export default ViewSubscriptions; export default ViewSubscriptions;