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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,20 @@
'use client';
import {
Cell, ColumnSort,
createColumnHelper, flexRender, getCoreRowModel,
getPaginationRowModel, getSortedRowModel, Header, HeaderGroup,
PaginationState, Row, RowData, type RowSelectionState,
ColumnSort,
createColumnHelper, getCoreRowModel,
getPaginationRowModel, getSortedRowModel,
PaginationState, RowData, type RowSelectionState,
SortingState, TableOptions, useReactTable, type VisibilityState
} from '@tanstack/react-table';
import clsx from 'clsx';
import { useState } from 'react';
import DefaultNoData from './DefaultNoData';
import PaginationTools from './PaginationTools';
import SelectAll from './SelectAll';
import SelectRow from './SelectRow';
import SortingIcon from './SortingIcon';
import TableBody from './TableBody';
import TableFooter from './TableFooter';
import TableHeader from './TableHeader';
export { createColumnHelper, type ColumnSort, type RowSelectionState, type VisibilityState };
@ -27,14 +28,19 @@ extends Pick<TableOptions<TData>,
'data' | 'columns' |
'onRowSelectionChange' | 'onColumnVisibilityChange'
> {
style?: React.CSSProperties
className?: string
dense?: boolean
headPosition?: string
noHeader?: boolean
noFooter?: boolean
conditionalRowStyles?: IConditionalStyle<TData>[]
noDataComponent?: React.ReactNode
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element, MouseEvent>) => void
noDataComponent?: React.ReactNode
enableRowSelection?: boolean
rowSelection?: RowSelectionState
@ -58,6 +64,7 @@ extends Pick<TableOptions<TData>,
* No sticky header if omitted
*/
function DataTable<TData extends RowData>({
style, className,
dense, headPosition, conditionalRowStyles, noFooter, noHeader,
onRowClicked, onRowDoubleClicked, noDataComponent,
@ -104,104 +111,30 @@ function DataTable<TData extends RowData>({
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 (
<div className='w-full'>
<div className='flex flex-col items-stretch'>
<table>
<div className={clsx(className)} style={style}>
<table className='w-full'>
{!noHeader ?
<thead
className={`clr-app shadow-border`}
style={{
top: headPosition,
position: 'sticky'
}}
>
{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}
<TableHeader
table={tableImpl}
enableRowSelection={enableRowSelection}
enableSorting={enableSorting}
headPosition={headPosition}
/>: null}
<tbody>
{tableImpl.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 => onRowClicked ? onRowClicked(row.original, event) : undefined}
onDoubleClick={event => onRowDoubleClicked ? onRowDoubleClicked(row.original, event) : undefined}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
<TableBody
table={tableImpl}
dense={dense}
conditionalRowStyles={conditionalRowStyles}
enableRowSelection={enableRowSelection}
onRowClicked={onRowClicked}
onRowDoubleClicked={onRowDoubleClicked}
/>
{!noFooter ?
<tfoot>
{tableImpl.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> : null}
<TableFooter
table={tableImpl}
/>: null}
</table>
{(enablePagination && !isEmpty) ?
@ -210,8 +143,7 @@ function DataTable<TData extends RowData>({
paginationOptions={paginationOptions}
onChangePaginationOption={onChangePaginationOption}
/> : null}
</div>
{isEmpty ? (noDataComponent ?? <DefaultNoData />) : null}
{isEmpty ? (noDataComponent ?? <DefaultNoData />) : null}
</div>);
}

View File

@ -1,6 +1,5 @@
import { Column } from '@tanstack/react-table';
import { AscendingIcon, DescendingIcon } from '@/components/Icons';
import { BiCaretDown, BiCaretUp } from 'react-icons/bi';
interface SortingIconProps<TData> {
column: Column<TData>
@ -9,10 +8,10 @@ interface SortingIconProps<TData> {
function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
return (<>
{{
desc: <DescendingIcon size='1rem' />,
asc: <AscendingIcon size='1rem'/>,
desc: <BiCaretDown size='1rem' />,
asc: <BiCaretUp size='1rem'/>,
}[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 { useConceptTheme } from '@/context/ThemeContext';
import { urls } from '@/utils/constants';
import TextURL from './Common/TextURL';
function Footer() {
const { noNavigation, noFooter } = useConceptTheme();
if (noNavigation || noFooter) {
return null;
}
return (
<footer tabIndex={-1}
className={clsx(
'w-full z-navigation',
'z-navigation',
'px-4 py-2 flex flex-col items-center gap-1',
'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() {
return (
@ -9,15 +10,15 @@ function HelpLibrary() {
<p>На текущем этапе происходит наполнение Библиотеки концептуальными схемами.</p>
<p>Поиск осуществлеяется с помощью инструментов в верхней части страницы.</p>
<div className='flex items-center gap-2'>
<SubscribedIcon size='1rem'/>
<FiBell size='1rem'/>
<p>Аттрибут <b>отслеживаемая</b> обозначает отслеживание схемы.</p>
</div>
<div className='flex items-center gap-2'>
<GroupIcon size='1rem'/>
<BiShareAlt size='1rem'/>
<p>Аттрибут <b>общедоступная</b> делает схему видимой в разделе библиотека.</p>
</div>
<div className='flex items-center gap-2'>
<EducationIcon size='1rem'/>
<BiCheckShield size='1rem'/>
<p>Аттрибут <b>неизменная</b> выделяет стандартные схемы.</p>
</div>
</div>);

View File

@ -27,62 +27,6 @@ function IconSVG({ viewbox, size = '1.5rem', className, props, children }: IconS
</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) {
return (
<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) {
return (
<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() {
return (
<svg

View File

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

View File

@ -1,6 +1,8 @@
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 { useConceptTheme } from '@/context/ThemeContext';
@ -41,13 +43,13 @@ function Navigation () {
<NavigationButton
text='Новая схема'
description='Создать новую схему'
icon={<PlusIcon />}
icon={<FaSquarePlus size='1.5rem' />}
onClick={navigateCreateNew}
/>
<NavigationButton
text='Библиотека'
description='Библиотека концептуальных схем'
icon={<LibraryIcon />}
icon={<IoLibrary size='1.5rem' />}
onClick={navigateLibrary}
/>
<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 { useConceptNavigation } from '@/context/NagivationContext';
import useDropdown from '@/hooks/useDropdown';
@ -24,7 +26,7 @@ function UserMenu() {
{user ?
<NavigationButton
description={`Пользователь ${user?.username}`}
icon={<UserIcon />}
icon={<FaCircleUser size='1.5rem' />}
onClick={menu.toggle}
/> : null}
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
'use client';
import { useMemo } from 'react';
import { BiDiamond, BiDuplicate, BiPlusCircle, BiReset, BiTrash } from 'react-icons/bi';
import { FiSave } from "react-icons/fi";
import { BiDuplicate, BiPlusCircle, BiReset, BiTrash } from 'react-icons/bi';
import { FiSave } from 'react-icons/fi';
import MiniButton from '@/components/Common/MiniButton';
import Overlay from '@/components/Common/Overlay';
@ -19,17 +19,16 @@ interface ConstituentaToolbarProps {
onDelete: () => void
onClone: () => void
onCreate: () => void
onTemplates: () => void
}
function ConstituentaToolbar({
isMutable, isModified,
onSubmit, onReset,
onDelete, onClone, onCreate, onTemplates
onDelete, onClone, onCreate
}: ConstituentaToolbarProps) {
const canSave = useMemo(() => (isModified && isMutable), [isModified, isMutable]);
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
tooltip='Сохранить изменения [Ctrl + S]'
disabled={!canSave}
@ -54,12 +53,6 @@ function ConstituentaToolbar({
onClick={onClone}
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
tooltip='Удалить редактируемую конституенту'
disabled={!isMutable}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ function RSFormStats({ stats }: RSFormStatsProps) {
return null;
}
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'
label='Всего конституент '
text={stats.count_all}

View File

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

View File

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

View File

@ -1,7 +1,10 @@
'use client';
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 DropdownButton from '@/components/Common/DropdownButton';
@ -24,29 +27,27 @@ interface RSListToolbarProps {
onDelete: () => void
onClone: () => void
onCreate: (type?: CstType) => void
onTemplates: () => void
onReindex: () => void
}
function RSListToolbar({
selectedCount, isMutable,
onMoveUp, onMoveDown, onDelete, onClone,
onCreate, onTemplates, onReindex
onCreate
}: RSListToolbarProps) {
const insertMenu = useDropdown();
const nothingSelected = useMemo(() => selectedCount === 0, [selectedCount]);
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
tooltip='Переместить вверх [Alt + вверх]'
icon={<BiUpvote size='1.25rem'/>}
icon={<BiUpvote size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-primary': ''}/>}
disabled={!isMutable || nothingSelected}
onClick={onMoveUp}
/>
<MiniButton
tooltip='Переместить вниз [Alt + вниз]'
icon={<BiDownvote size='1.25rem'/>}
icon={<BiDownvote size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-primary': ''}/>}
disabled={!isMutable || nothingSelected}
onClick={onMoveDown}
/>
@ -84,18 +85,6 @@ function RSListToolbar({
</Dropdown> : null}
</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
tooltip='Удалить выбранные [Delete]'
icon={<BiTrash size='1.25rem' className={isMutable && !nothingSelected ? 'clr-text-warning' : ''} />}

View File

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

View File

@ -18,7 +18,7 @@ function GraphSidebar({
layout, setLayout
} : GraphSidebarProps) {
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
placeholder='Выберите цвет'
options={SelectorGraphColoring}
@ -28,7 +28,6 @@ function GraphSidebar({
/>
<SelectSingle
placeholder='Способ расположения'
className='w-full'
options={SelectorGraphLayout}
isSearchable={false}
value={layout ? { value: layout, label: mapLableLayout.get(layout) } : null}

View File

@ -1,11 +1,10 @@
'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 Overlay from '@/components/Common/Overlay';
import HelpButton from '@/components/Help/HelpButton';
import { LetterAIcon, LetterALinesIcon } from '@/components/Icons';
import { HelpTopic } from '@/models/miscelanious';
interface GraphToolbarProps {
@ -33,7 +32,7 @@ function GraphToolbar({
onCreate, onDelete, onResetViewpoint
} : GraphToolbarProps) {
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
tooltip='Настройки фильтрации узлов и связей'
icon={<BiFilterAlt size='1.25rem' className='clr-text-primary' />}
@ -43,8 +42,8 @@ function GraphToolbar({
tooltip={!noText ? 'Скрыть текст' : 'Отобразить текст'}
icon={
!noText
? <LetterALinesIcon size='1.25rem' className='clr-text-success' />
: <LetterAIcon size='1.25rem' className='clr-text-primary' />
? <BiFontFamily size='1.25rem' className='clr-text-success' />
: <BiFont size='1.25rem' className='clr-text-primary' />
}
onClick={toggleNoText}
/>

View File

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

View File

@ -1,13 +1,12 @@
'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 { LuCrown, LuGlasses } from 'react-icons/lu';
import Button from '@/components/Common/Button';
import Dropdown from '@/components/Common/Dropdown';
import DropdownButton from '@/components/Common/DropdownButton';
import { ShareIcon } from '@/components/Icons';
import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NagivationContext';
@ -115,7 +114,7 @@ function RSTabsMenu({
/>
<DropdownButton
text='Поделиться'
icon={<ShareIcon size='1rem' className='clr-text-primary' />}
icon={<BiShareAlt size='1rem' className='clr-text-primary' />}
onClick={handleShare}
/>
<DropdownButton disabled={!user}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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