Compare commits

..

24 Commits

Author SHA1 Message Date
Ivan
65c210b047 M: Fix react-toaster size for small screens
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled
2025-03-13 14:54:28 +03:00
Ivan
ed89591af9 R: Simplify inline styles pt3 2025-03-13 14:40:56 +03:00
Ivan
a60f63455c R: Simplify inline styles pt2 2025-03-13 13:30:31 +03:00
Ivan
503e447073 R: Simplify inline styles pt1 2025-03-13 01:20:46 +03:00
Ivan
a2dd94637b M: Improve table styling 2025-03-12 23:38:24 +03:00
Ivan
4ec2272c5f B: Fix navigation bar animation 2025-03-12 23:27:05 +03:00
Ivan
023987d511 M: Remove unnecessary will-change attributes 2025-03-12 23:10:38 +03:00
Ivan
342c315c45 F: Add masking blur effects for edges of graphs 2025-03-12 23:08:01 +03:00
Ivan
ab551a3ece Update data-table.tsx 2025-03-12 22:40:29 +03:00
Ivan
ea97a7a075 R: Improve data-table internals 2025-03-12 21:07:01 +03:00
Ivan
85faab2078 R: Migrate to snake-case pt3 2025-03-12 12:12:45 +03:00
Ivan
aac1b072bc R: Migrate to snake-case pt2 2025-03-12 12:04:23 +03:00
Ivan
cb178e69cd R: Migrate to snake-case pt1 2025-03-12 11:54:32 +03:00
Ivan
fb23c32ca8 R: Move pagination handling to main body 2025-03-12 00:18:34 +03:00
Ivan
2b0f074ffa M: Improve checkboxes 2025-03-11 23:37:03 +03:00
Ivan
f5cf5af7d7 M: Prevent blockNavigation to cause rerenders 2025-03-11 23:24:45 +03:00
Ivan
af415d27b4 Update TODO.txt 2025-03-11 23:15:35 +03:00
Ivan
15cd3fb306 M: Add distinguishing border for focusCst 2025-03-11 22:56:16 +03:00
Ivan
92d3d2676b M: Improve location picker 2025-03-11 22:24:51 +03:00
Ivan
c04ea8993e M: Small layout fixes 2025-03-11 14:42:27 +03:00
Ivan
1643cd737c B: Fix modal layout 2025-03-11 12:56:32 +03:00
Ivan
3d63c25845 R: Refactor layout definitions 2025-03-11 12:48:32 +03:00
Ivan
aea9dececf R: Refactor tabs layout 2025-03-11 11:35:22 +03:00
Ivan
7c89a04255 B: Fix modals and EditVersions 2025-03-11 11:00:52 +03:00
534 changed files with 2369 additions and 2404 deletions

View File

@ -114,6 +114,7 @@
"lezer",
"Litr",
"loct",
"mgraph",
"moprho",
"multiword",
"mypy",
@ -154,6 +155,8 @@
"rsforms",
"rsgraph",
"rslang",
"rslist",
"rstabs",
"rstemplates",
"setexpr",
"SIDELIST",

View File

@ -67,3 +67,5 @@ https://drf-standardized-errors.readthedocs.io/en/latest/error_response.html
https://stackoverflow.com/questions/28838170/multilevel-json-diff-in-python
- Documentation platform. Consider diplodoc
- nuqs useQueryState

View File

@ -4,7 +4,7 @@
"version": "1.0.0",
"type": "module",
"scripts": {
"generate": "lezer-generator src/components/RSInput/rslang/rslangFast.grammar -o src/components/RSInput/rslang/parser.ts && lezer-generator src/components/RSInput/rslang/rslangAST.grammar -o src/components/RSInput/rslang/parserAST.ts && lezer-generator src/components/RefsInput/parse/refsText.grammar -o src/components/RefsInput/parse/parser.ts",
"generate": "lezer-generator src/components/rs-input/rslang/rslang-fast.grammar -o src/components/rs-input/rslang/parser.ts && lezer-generator src/components/rs-input/rslang/rslang-ast.grammar -o src/components/rs-input/rslang/parserAST.ts && lezer-generator src/components/refs-input/parse/refs-text.grammar -o src/components/refs-input/parse/parser.ts",
"test": "jest",
"test:e2e": "playwright test",
"dev": "vite --host",

View File

@ -1 +0,0 @@
export { Navigation } from './Navigation';

View File

@ -1,23 +1,23 @@
import { Suspense } from 'react';
import { Outlet } from 'react-router';
import clsx from 'clsx';
import { ModalLoader } from '@/components/Modal';
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout';
import { ModalLoader } from '@/components/modal';
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/app-layout';
import { useDialogsStore } from '@/stores/dialogs';
import { NavigationState } from './Navigation/NavigationContext';
import { Footer } from './Footer';
import { GlobalDialogs } from './GlobalDialogs';
import { GlobalLoader } from './GlobalLoader';
import { ToasterThemed } from './GlobalToaster';
import { GlobalTooltips } from './GlobalTooltips';
import { MutationErrors } from './MutationErrors';
import { Navigation } from './Navigation';
import { NavigationState } from './navigation/navigation-context';
import { Footer } from './footer';
import { GlobalDialogs } from './global-dialogs';
import { GlobalLoader } from './global-Loader';
import { ToasterThemed } from './global-toaster';
import { GlobalTooltips } from './global-tooltips';
import { MutationErrors } from './mutation-errors';
import { Navigation } from './navigation';
export function ApplicationLayout() {
const mainHeight = useMainHeight();
const viewportHeight = useViewportHeight();
const showScroll = useAppLayoutStore(state => !state.noScroll);
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
const noNavigation = useAppLayoutStore(state => state.noNavigation);
const noFooter = useAppLayoutStore(state => state.noFooter);
@ -27,8 +27,7 @@ export function ApplicationLayout() {
<NavigationState>
<div className='min-w-80 antialiased h-full max-w-480 mx-auto'>
<ToasterThemed
className='text-[14px]'
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
className={clsx('sm:text-[14px]/[20px] text-[12px]/[16px]', noNavigationAnimation ? 'mt-6' : 'mt-14')}
autoClose={3000}
draggable={false}
pauseOnFocusLoss={false}
@ -46,7 +45,7 @@ export function ApplicationLayout() {
style={{ maxHeight: viewportHeight }}
inert={activeDialog !== null}
>
<main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}>
<main className='cc-scroll-y overflow-y-auto' style={{ minHeight: mainHeight }}>
<GlobalLoader />
<MutationErrors />
<Outlet />

View File

@ -1,7 +1,7 @@
import { useNavigate, useRouteError } from 'react-router';
import { Button } from '@/components/Control';
import { InfoError } from '@/components/InfoError';
import { Button } from '@/components/control';
import { InfoError } from '@/components/info-error';
export function ErrorFallback() {
const error = useRouteError();

View File

@ -1,6 +1,6 @@
import clsx from 'clsx';
import { TextURL } from '@/components/Control';
import { TextURL } from '@/components/control';
import { external_urls } from '@/utils/constants';
export function Footer() {

View File

@ -1,8 +1,8 @@
import { useNavigation } from 'react-router';
import { useDebounce } from 'use-debounce';
import { Loader } from '@/components/Loader';
import { ModalBackdrop } from '@/components/Modal/ModalBackdrop';
import { Loader } from '@/components/loader';
import { ModalBackdrop } from '@/components/modal/modal-backdrop';
import { PARAMETER } from '@/utils/constants';
export function GlobalLoader() {
@ -18,7 +18,7 @@ export function GlobalLoader() {
return (
<div className='cc-modal-wrapper'>
<ModalBackdrop />
<div className='cc-fade-in px-10 border rounded-xl bg-prim-100'>
<div className='z-pop cc-fade-in px-10 border rounded-xl bg-prim-100'>
<Loader scale={6} />
</div>
</div>

View File

@ -5,113 +5,113 @@ import React from 'react';
import { DialogType, useDialogsStore } from '@/stores/dialogs';
const DlgChangeInputSchema = React.lazy(() =>
import('@/features/oss/dialogs/DlgChangeInputSchema').then(module => ({ default: module.DlgChangeInputSchema }))
import('@/features/oss/dialogs/dlg-change-input-schema').then(module => ({ default: module.DlgChangeInputSchema }))
);
const DlgChangeLocation = React.lazy(() =>
import('@/features/library/dialogs/DlgChangeLocation').then(module => ({
import('@/features/library/dialogs/dlg-change-location').then(module => ({
default: module.DlgChangeLocation
}))
);
const DlgCloneLibraryItem = React.lazy(() =>
import('@/features/library/dialogs/DlgCloneLibraryItem').then(module => ({
import('@/features/library/dialogs/dlg-clone-library-item').then(module => ({
default: module.DlgCloneLibraryItem
}))
);
const DlgCreateCst = React.lazy(() =>
import('@/features/rsform/dialogs/DlgCreateCst').then(module => ({ default: module.DlgCreateCst }))
import('@/features/rsform/dialogs/dlg-create-cst').then(module => ({ default: module.DlgCreateCst }))
);
const DlgCreateOperation = React.lazy(() =>
import('@/features/oss/dialogs/DlgCreateOperation').then(module => ({
import('@/features/oss/dialogs/dlg-create-operation').then(module => ({
default: module.DlgCreateOperation
}))
);
const DlgCreateVersion = React.lazy(() =>
import('@/features/library/dialogs/DlgCreateVersion').then(module => ({
import('@/features/library/dialogs/dlg-create-version').then(module => ({
default: module.DlgCreateVersion
}))
);
const DlgCstTemplate = React.lazy(() =>
import('@/features/rsform/dialogs/DlgCstTemplate').then(module => ({
import('@/features/rsform/dialogs/dlg-cst-template').then(module => ({
default: module.DlgCstTemplate
}))
);
const DlgDeleteCst = React.lazy(() =>
import('@/features/rsform/dialogs/DlgDeleteCst').then(module => ({
import('@/features/rsform/dialogs/dlg-delete-cst').then(module => ({
default: module.DlgDeleteCst
}))
);
const DlgDeleteOperation = React.lazy(() =>
import('@/features/oss/dialogs/DlgDeleteOperation').then(module => ({
import('@/features/oss/dialogs/dlg-delete-operation').then(module => ({
default: module.DlgDeleteOperation
}))
);
const DlgEditEditors = React.lazy(() =>
import('@/features/library/dialogs/DlgEditEditors').then(module => ({
import('@/features/library/dialogs/dlg-edit-editors').then(module => ({
default: module.DlgEditEditors
}))
);
const DlgEditOperation = React.lazy(() =>
import('@/features/oss/dialogs/DlgEditOperation').then(module => ({
import('@/features/oss/dialogs/dlg-edit-operation').then(module => ({
default: module.DlgEditOperation
}))
);
const DlgEditReference = React.lazy(() =>
import('@/features/rsform/dialogs/DlgEditReference').then(module => ({
import('@/features/rsform/dialogs/dlg-edit-reference').then(module => ({
default: module.DlgEditReference
}))
);
const DlgEditVersions = React.lazy(() =>
import('@/features/library/dialogs/DlgEditVersions').then(module => ({
import('@/features/library/dialogs/dlg-edit-versions').then(module => ({
default: module.DlgEditVersions
}))
);
const DlgEditWordForms = React.lazy(() =>
import('@/features/rsform/dialogs/DlgEditWordForms').then(module => ({
import('@/features/rsform/dialogs/dlg-edit-word-forms').then(module => ({
default: module.DlgEditWordForms
}))
);
const DlgInlineSynthesis = React.lazy(() =>
import('@/features/rsform/dialogs/DlgInlineSynthesis').then(module => ({
import('@/features/rsform/dialogs/dlg-inline-synthesis').then(module => ({
default: module.DlgInlineSynthesis
}))
);
const DlgRelocateConstituents = React.lazy(() =>
import('@/features/oss/dialogs/DlgRelocateConstituents').then(module => ({
import('@/features/oss/dialogs/dlg-relocate-constituents').then(module => ({
default: module.DlgRelocateConstituents
}))
);
const DlgRenameCst = React.lazy(() =>
import('@/features/rsform/dialogs/DlgRenameCst').then(module => ({
import('@/features/rsform/dialogs/dlg-rename-cst').then(module => ({
default: module.DlgRenameCst
}))
);
const DlgShowAST = React.lazy(() =>
import('@/features/rsform/dialogs/DlgShowAST').then(module => ({
import('@/features/rsform/dialogs/dlg-show-ast').then(module => ({
default: module.DlgShowAST
}))
);
const DlgShowQR = React.lazy(() =>
import('@/features/rsform/dialogs/DlgShowQR').then(module => ({
import('@/features/rsform/dialogs/dlg-show-qr').then(module => ({
default: module.DlgShowQR
}))
);
const DlgShowTypeGraph = React.lazy(() =>
import('@/features/rsform/dialogs/DlgShowTypeGraph').then(module => ({
import('@/features/rsform/dialogs/dlg-show-type-graph').then(module => ({
default: module.DlgShowTypeGraph
}))
);
const DlgSubstituteCst = React.lazy(() =>
import('@/features/rsform/dialogs/DlgSubstituteCst').then(module => ({
import('@/features/rsform/dialogs/dlg-substitute-cst').then(module => ({
default: module.DlgSubstituteCst
}))
);
const DlgUploadRSForm = React.lazy(() =>
import('@/features/rsform/dialogs/DlgUploadRSForm').then(module => ({
import('@/features/rsform/dialogs/dlg-upload-rsform').then(module => ({
default: module.DlgUploadRSForm
}))
);
const DlgGraphParams = React.lazy(() =>
import('@/features/rsform/dialogs/DlgGraphParams').then(module => ({ default: module.DlgGraphParams }))
import('@/features/rsform/dialogs/dlg-graph-params').then(module => ({ default: module.DlgGraphParams }))
);
export const GlobalDialogs = () => {

View File

@ -4,7 +4,7 @@ import { IntlProvider } from 'react-intl';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from '@/backend/queryClient';
import { queryClient } from '@/backend/query-client';
// prettier-ignore
export function GlobalProviders({ children }: React.PropsWithChildren) {

View File

@ -1,6 +1,6 @@
'use client';
import { Tooltip } from '@/components/Container';
import { Tooltip } from '@/components/container';
import { globalIDs } from '@/utils/constants';
export const GlobalTooltips = () => {

View File

@ -1,9 +1,9 @@
export { useConceptNavigation } from './Navigation/NavigationContext';
export { useBlockNavigation } from './Navigation/NavigationContext';
export { useConceptNavigation } from './navigation/navigation-context';
export { useBlockNavigation } from './navigation/navigation-context';
export { urls } from './urls';
import { RouterProvider } from 'react-router';
import { Router } from './Router';
import { Router } from './router';
export function App() {
return <RouterProvider router={Router} />;

View File

@ -1,8 +1,8 @@
import { useMutationErrors } from '@/backend/useMutationErrors';
import { Button } from '@/components/Control';
import { DescribeError } from '@/components/InfoError';
import { ModalBackdrop } from '@/components/Modal/ModalBackdrop';
import { useEscapeKey } from '@/hooks/useEscapeKey';
import { useMutationErrors } from '@/backend/use-mutation-errors';
import { Button } from '@/components/control';
import { DescribeError } from '@/components/info-error';
import { ModalBackdrop } from '@/components/modal/modal-backdrop';
import { useEscapeKey } from '@/hooks/use-escape-key';
import { useDialogsStore } from '@/stores/dialogs';
export function MutationErrors() {
@ -20,7 +20,7 @@ export function MutationErrors() {
return (
<div className='cc-modal-wrapper'>
<ModalBackdrop onHide={resetErrors} />
<div className='px-10 py-3 flex flex-col items-center border rounded-xl bg-prim-100' role='alertdialog'>
<div className='z-pop px-10 py-3 flex flex-col items-center border rounded-xl bg-prim-100' role='alertdialog'>
<h1 className='py-2 select-none'>Ошибка при обработке</h1>
<div className='px-3 flex flex-col text-warn-600 text-sm font-semibold select-text'>
<DescribeError error={mutationErrors[0]} />

View File

@ -0,0 +1 @@
export { Navigation } from './navigation';

View File

@ -1,4 +1,4 @@
import { useWindowSize } from '@/hooks/useWindowSize';
import { useWindowSize } from '@/hooks/use-window-size';
import { usePreferencesStore } from '@/stores/preferences';
export function Logo() {

View File

@ -1,6 +1,6 @@
'use client';
import { createContext, use, useEffect, useState } from 'react';
import { createContext, use, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router';
export interface NavigationProps {
@ -19,8 +19,7 @@ interface INavigationContext {
canBack: () => boolean;
isBlocked: boolean;
setIsBlocked: (value: boolean) => void;
setRequireConfirmation: (value: boolean) => void;
}
export const NavigationContext = createContext<INavigationContext | null>(null);
@ -35,11 +34,11 @@ export const useConceptNavigation = () => {
export const NavigationState = ({ children }: React.PropsWithChildren) => {
const router = useNavigate();
const [isBlocked, setIsBlocked] = useState(false);
const isBlocked = useRef(false);
const [internalNavigation, setInternalNavigation] = useState(false);
function validate() {
return !isBlocked || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?');
return !isBlocked.current || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?');
}
function canBack() {
@ -50,7 +49,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
if (props.newTab) {
window.open(`${props.path}`, '_blank');
} else if (props.force || validate()) {
setIsBlocked(false);
isBlocked.current = false;
setInternalNavigation(true);
Promise.resolve(router(props.path, { viewTransition: true })).catch(console.error);
}
@ -60,7 +59,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
if (props.newTab) {
window.open(`${props.path}`, '_blank');
} else if (props.force || validate()) {
setIsBlocked(false);
isBlocked.current = false;
setInternalNavigation(true);
return router(props.path, { viewTransition: true });
}
@ -68,14 +67,14 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
function replace(props: Omit<NavigationProps, 'newTab'>) {
if (props.force || validate()) {
setIsBlocked(false);
isBlocked.current = false;
Promise.resolve(router(props.path, { replace: true, viewTransition: true })).catch(console.error);
}
}
function replaceAsync(props: Omit<NavigationProps, 'newTab'>): void | Promise<void> {
if (props.force || validate()) {
setIsBlocked(false);
isBlocked.current = false;
return router(props.path, { replace: true, viewTransition: true });
}
}
@ -83,14 +82,14 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
function back(force?: boolean) {
if (force || validate()) {
Promise.resolve(router(-1)).catch(console.error);
setIsBlocked(false);
isBlocked.current = false;
}
}
function forward(force?: boolean) {
if (force || validate()) {
Promise.resolve(router(1)).catch(console.error);
setIsBlocked(false);
isBlocked.current = false;
}
}
@ -104,8 +103,7 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
back,
forward,
canBack,
isBlocked,
setIsBlocked
setRequireConfirmation: (value: boolean) => (isBlocked.current = value)
}}
>
{children}
@ -114,9 +112,9 @@ export const NavigationState = ({ children }: React.PropsWithChildren) => {
};
export function useBlockNavigation(isBlocked: boolean) {
const router = useConceptNavigation();
const { setRequireConfirmation } = useConceptNavigation();
useEffect(() => {
router.setIsBlocked(isBlocked);
return () => router.setIsBlocked(false);
}, [router, isBlocked]);
setRequireConfirmation(isBlocked);
return () => setRequireConfirmation(false);
}, [setRequireConfirmation, isBlocked]);
}

View File

@ -1,14 +1,16 @@
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons';
import { useWindowSize } from '@/hooks/useWindowSize';
import { useAppLayoutStore } from '@/stores/appLayout';
import clsx from 'clsx';
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/icons';
import { useWindowSize } from '@/hooks/use-window-size';
import { useAppLayoutStore } from '@/stores/app-layout';
import { urls } from '../urls';
import { Logo } from './Logo';
import { NavigationButton } from './NavigationButton';
import { useConceptNavigation } from './NavigationContext';
import { ToggleNavigation } from './ToggleNavigation';
import { UserMenu } from './UserMenu';
import { Logo } from './logo';
import { NavigationButton } from './navigation-button';
import { useConceptNavigation } from './navigation-context';
import { ToggleNavigation } from './toggle-navigation';
import { UserMenu } from './user-menu';
export function Navigation() {
const router = useConceptNavigation();
@ -28,11 +30,11 @@ export function Navigation() {
<nav className='z-navigation sticky top-0 left-0 right-0 select-none bg-prim-100'>
<ToggleNavigation />
<div
className='pl-2 pr-6 sm:pr-4 h-12 flex cc-shadow-border'
style={{
maxHeight: noNavigationAnimation ? '0rem' : '3rem',
translate: noNavigationAnimation ? '0 -1.5rem' : '0'
}}
className={clsx(
'pl-2 pr-6 sm:pr-4 h-12 flex cc-shadow-border',
'transition-[max-height,translate] ease-bezier duration-(--duration-move)',
noNavigationAnimation ? '-translate-y-6 max-h-0' : 'max-h-12'
)}
>
<div className='flex items-center mr-auto cursor-pointer' onClick={!size.isSmall ? navigateHome : undefined}>
<Logo />

View File

@ -1,7 +1,7 @@
'use client';
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons';
import { useAppLayoutStore } from '@/stores/appLayout';
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/icons';
import { useAppLayoutStore } from '@/stores/app-layout';
import { usePreferencesStore } from '@/stores/preferences';
import { globalIDs } from '@/utils/constants';

View File

@ -1,10 +1,10 @@
import { useAuthSuspense } from '@/features/auth';
import { IconLogin, IconUser2 } from '@/components/Icons';
import { IconLogin, IconUser2 } from '@/components/icons';
import { usePreferencesStore } from '@/stores/preferences';
import { globalIDs } from '@/utils/constants';
import { NavigationButton } from './NavigationButton';
import { NavigationButton } from './navigation-button';
interface UserButtonProps {
onLogin: () => void;

View File

@ -1,7 +1,7 @@
import { useAuthSuspense } from '@/features/auth';
import { useLogout } from '@/features/auth/backend/useLogout';
import { useLogout } from '@/features/auth/backend/use-logout';
import { Dropdown, DropdownButton } from '@/components/Dropdown';
import { Dropdown, DropdownButton } from '@/components/dropdown';
import {
IconAdmin,
IconAdminOff,
@ -15,13 +15,13 @@ import {
IconLogout,
IconRESTapi,
IconUser
} from '@/components/Icons';
} from '@/components/icons';
import { usePreferencesStore } from '@/stores/preferences';
import { globalIDs } from '@/utils/constants';
import { urls } from '../urls';
import { useConceptNavigation } from './NavigationContext';
import { useConceptNavigation } from './navigation-context';
interface UserDropdownProps {
isOpen: boolean;

View File

@ -1,13 +1,13 @@
import { Suspense } from 'react';
import { useDropdown } from '@/components/Dropdown';
import { Loader } from '@/components/Loader';
import { useDropdown } from '@/components/dropdown';
import { Loader } from '@/components/loader';
import { urls } from '../urls';
import { useConceptNavigation } from './NavigationContext';
import { UserButton } from './UserButton';
import { UserDropdown } from './UserDropdown';
import { useConceptNavigation } from './navigation-context';
import { UserButton } from './user-button';
import { UserDropdown } from './user-dropdown';
export function UserMenu() {
const router = useConceptNavigation();

View File

@ -1,20 +1,20 @@
import { createBrowserRouter } from 'react-router';
import { prefetchAuth } from '@/features/auth/backend/useAuth';
import { LoginPage } from '@/features/auth/pages/LoginPage';
import { HomePage } from '@/features/home/HomePage';
import { NotFoundPage } from '@/features/home/NotFoundPage';
import { prefetchLibrary } from '@/features/library/backend/useLibrary';
import { CreateItemPage } from '@/features/library/pages/CreateItemPage';
import { prefetchOSS } from '@/features/oss/backend/useOSS';
import { prefetchRSForm } from '@/features/rsform/backend/useRSForm';
import { prefetchProfile } from '@/features/users/backend/useProfile';
import { prefetchUsers } from '@/features/users/backend/useUsers';
import { prefetchAuth } from '@/features/auth/backend/use-auth';
import { LoginPage } from '@/features/auth/pages/login-page';
import { HomePage } from '@/features/home/home-page';
import { NotFoundPage } from '@/features/home/not-found-page';
import { prefetchLibrary } from '@/features/library/backend/use-library';
import { CreateItemPage } from '@/features/library/pages/create-item-page';
import { prefetchOSS } from '@/features/oss/backend/use-oss';
import { prefetchRSForm } from '@/features/rsform/backend/use-rsform';
import { prefetchProfile } from '@/features/users/backend/use-profile';
import { prefetchUsers } from '@/features/users/backend/use-users';
import { Loader } from '@/components/Loader';
import { Loader } from '@/components/loader';
import { ApplicationLayout } from './ApplicationLayout';
import { ErrorFallback } from './ErrorFallback';
import { ApplicationLayout } from './application-layout';
import { ErrorFallback } from './error-fallback';
import { routes } from './urls';
export const Router = createBrowserRouter([
@ -39,25 +39,25 @@ export const Router = createBrowserRouter([
},
{
path: routes.signup,
lazy: () => import('@/features/users/pages/RegisterPage')
lazy: () => import('@/features/users/pages/register-page')
},
{
path: routes.profile,
loader: prefetchProfile,
lazy: () => import('@/features/users/pages/UserProfilePage')
lazy: () => import('@/features/users/pages/user-profile-page')
},
{
path: routes.restore_password,
lazy: () => import('@/features/auth/pages/RestorePasswordPage')
lazy: () => import('@/features/auth/pages/restore-password-page')
},
{
path: routes.password_change,
lazy: () => import('@/features/auth/pages/PasswordChangePage')
lazy: () => import('@/features/auth/pages/password-change-page')
},
{
path: routes.library,
loader: () => Promise.allSettled([prefetchLibrary(), prefetchUsers()]),
lazy: () => import('@/features/library/pages/LibraryPage')
lazy: () => import('@/features/library/pages/library-page')
},
{
path: routes.create_schema,
@ -66,24 +66,24 @@ export const Router = createBrowserRouter([
{
path: `${routes.rsforms}/:id`,
loader: data => prefetchRSForm(parseRSFormURL(data.params.id, data.request.url)),
lazy: () => import('@/features/rsform/pages/RSFormPage')
lazy: () => import('@/features/rsform/pages/rsform-page')
},
{
path: `${routes.oss}/:id`,
loader: data => prefetchOSS(parseOssURL(data.params.id)),
lazy: () => import('@/features/oss/pages/OssPage')
lazy: () => import('@/features/oss/pages/oss-page')
},
{
path: routes.manuals,
lazy: () => import('@/features/help/pages/ManualsPage')
lazy: () => import('@/features/help/pages/manuals-page')
},
{
path: `${routes.icons}`,
lazy: () => import('@/features/home/IconsPage')
lazy: () => import('@/features/home/icons-page')
},
{
path: `${routes.database_schema}`,
lazy: () => import('@/features/home/DatabaseSchemaPage')
lazy: () => import('@/features/home/database-schema-page')
}
]
}

View File

@ -2,7 +2,7 @@
* Module: Internal navigation constants.
*/
import { buildConstants } from '@/utils/buildConstants';
import { buildConstants } from '@/utils/build-constants';
/**
* Routes.

View File

@ -5,7 +5,7 @@ import { toast } from 'react-toastify';
import axios, { type AxiosError, type AxiosRequestConfig } from 'axios';
import { type z, ZodError } from 'zod';
import { buildConstants } from '@/utils/buildConstants';
import { buildConstants } from '@/utils/build-constants';
import { PARAMETER } from '@/utils/constants';
import { errorMsg } from '@/utils/labels';
import { extractErrorMessage } from '@/utils/utils';

View File

@ -1,7 +1,7 @@
import { QueryClient } from '@tanstack/react-query';
import { type ZodError } from 'zod';
import { type AxiosError } from './apiTransport';
import { type AxiosError } from './api-transport';
import { DELAYS } from './configuration';
declare module '@tanstack/react-query' {

View File

@ -1,2 +0,0 @@
export { Divider } from './Divider';
export { type PlacesType, Tooltip } from './Tooltip';

View File

@ -1,5 +0,0 @@
export { Button } from './Button';
export { MiniButton } from './MiniButton';
export { SelectorButton } from './SelectorButton';
export { SubmitButton } from './SubmitButton';
export { TextURL } from './TextURL';

View File

@ -1,20 +0,0 @@
'use no memo';
import { type Column } from '@tanstack/react-table';
import { IconSortAsc, IconSortDesc } from '../Icons';
interface SortingIconProps<TData> {
column: Column<TData>;
}
export function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
return (
<>
{{
desc: <IconSortDesc size='1rem' />,
asc: <IconSortAsc size='1rem' />
}[column.getIsSorted() as string] ?? <IconSortDesc size='1rem' className='opacity-0 group-hover:opacity-25' />}
</>
);
}

View File

@ -1,104 +0,0 @@
'use no memo';
import { type Cell, flexRender, type Row, type Table } from '@tanstack/react-table';
import clsx from 'clsx';
import { SelectRow } from './SelectRow';
import { type IConditionalStyle } from '.';
interface TableBodyProps<TData> {
table: Table<TData>;
dense?: boolean;
noHeader?: boolean;
enableRowSelection?: boolean;
conditionalRowStyles?: IConditionalStyle<TData>[];
lastSelected: string | null;
onChangeLastSelected: (newValue: string | null) => void;
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void;
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void;
}
export function TableBody<TData>({
table,
dense,
noHeader,
enableRowSelection,
conditionalRowStyles,
lastSelected,
onChangeLastSelected,
onRowClicked,
onRowDoubleClicked
}: TableBodyProps<TData>) {
function handleRowClicked(target: Row<TData>, event: React.MouseEvent<Element>) {
onRowClicked?.(target.original, event);
if (enableRowSelection && target.getCanSelect()) {
if (event.shiftKey && !!lastSelected && lastSelected !== target.id) {
const { rows, rowsById } = table.getRowModel();
const lastIndex = rowsById[lastSelected].index;
const currentIndex = target.index;
const toggleRows = rows.slice(
lastIndex > currentIndex ? currentIndex : lastIndex + 1,
lastIndex > currentIndex ? lastIndex : currentIndex + 1
);
const newSelection: Record<string, boolean> = {};
toggleRows.forEach(row => {
newSelection[row.id] = !target.getIsSelected();
});
table.setRowSelection(prev => ({ ...prev, ...newSelection }));
onChangeLastSelected(null);
} else {
onChangeLastSelected(target.id);
target.toggleSelected(!target.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={clsx(
'cc-scroll-row',
'clr-hover cc-animate-color',
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
row.getIsSelected() ? 'clr-selected' : 'odd:bg-prim-200 even:bg-prim-100'
)}
style={{ ...(conditionalRowStyles ? getRowStyles(row) : []) }}
>
{enableRowSelection ? (
<td key={`select-${row.id}`} className='pl-3 pr-1 border-y'>
<SelectRow row={row} onChangeLastSelected={onChangeLastSelected} />
</td>
) : null}
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
<td
key={cell.id}
className='px-2 align-middle border-y'
style={{
cursor: onRowClicked || onRowDoubleClicked ? 'pointer' : 'auto',
paddingBottom: dense ? '0.25rem' : '0.5rem',
paddingTop: dense ? '0.25rem' : '0.5rem',
width: noHeader && index === 0 ? `calc(var(--col-${cell.column.id}-size) * 1px)` : 'auto'
}}
onClick={event => handleRowClicked(row, event)}
onDoubleClick={event => onRowDoubleClicked?.(row.original, event)}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
);
}

View File

@ -1,62 +0,0 @@
'use no memo';
import { flexRender, type Header, type HeaderGroup, type 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;
resetLastSelected: () => void;
}
export function TableHeader<TData>({
table,
headPosition,
enableRowSelection,
enableSorting,
resetLastSelected
}: TableHeaderProps<TData>) {
return (
<thead
className='bg-prim-100 cc-shadow-border'
style={{
top: headPosition,
position: 'sticky'
}}
>
{table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
<tr key={headerGroup.id}>
{enableRowSelection ? (
<th className='pl-3 pr-1' scope='col'>
<SelectAll table={table} resetLastSelected={resetLastSelected} />
</th>
) : null}
{headerGroup.headers.map((header: Header<TData, unknown>) => (
<th
key={header.id}
colSpan={header.colSpan}
scope='col'
className='cc-table-header group'
style={{
width: `calc(var(--header-${header?.id}-size) * 1px)`,
cursor: enableSorting && header.column.getCanSort() ? 'pointer' : 'auto'
}}
onClick={enableSorting ? header.column.getToggleSortingHandler() : undefined}
>
{!header.isPlaceholder ? (
<span className='inline-flex gap-1'>
{flexRender(header.column.columnDef.header, header.getContext())}
{enableSorting && header.column.getCanSort() ? <SortingIcon column={header.column} /> : null}
</span>
) : null}
</th>
))}
</tr>
))}
</thead>
);
}

View File

@ -1,4 +0,0 @@
export { Dropdown } from './Dropdown';
export { DropdownButton } from './DropdownButton';
export { DropdownCheckbox } from './DropdownCheckbox';
export { useDropdown } from './useDropdown';

View File

@ -1,11 +0,0 @@
export { Checkbox, type CheckboxProps } from './Checkbox';
export { CheckboxTristate } from './CheckboxTristate';
export { ErrorField } from './ErrorField';
export { FileInput } from './FileInput';
export { Label } from './Label';
export { SearchBar } from './SearchBar';
export { SelectMulti, type SelectMultiProps } from './SelectMulti';
export { SelectSingle, type SelectSingleProps } from './SelectSingle';
export { SelectTree } from './SelectTree';
export { TextArea } from './TextArea';
export { TextInput } from './TextInput';

View File

@ -1,83 +0,0 @@
'use client';
import clsx from 'clsx';
import { BadgeHelp } from '@/features/help/components';
import { useEscapeKey } from '@/hooks/useEscapeKey';
import { useDialogsStore } from '@/stores/dialogs';
import { prepareTooltip } from '@/utils/utils';
import { Button, MiniButton } from '../Control';
import { IconClose } from '../Icons';
import { ModalBackdrop } from './ModalBackdrop';
import { type ModalProps } from './ModalForm';
interface ModalViewProps extends ModalProps {}
/**
* Displays a customizable modal window with submit form.
*/
export function ModalView({
children,
className,
header,
overflowVisible,
helpTopic,
hideHelpWhen,
...restProps
}: React.PropsWithChildren<ModalViewProps>) {
const hideDialog = useDialogsStore(state => state.hideDialog);
useEscapeKey(hideDialog);
return (
<div className='cc-modal-wrapper'>
<ModalBackdrop onHide={hideDialog} />
<div className='cc-animate-modal grid border rounded-xl bg-prim-100' role='dialog'>
{helpTopic && !hideHelpWhen?.() ? (
<BadgeHelp
topic={helpTopic}
className='absolute z-pop left-0 mt-2 ml-2'
padding='p-0'
contentClass='sm:max-w-160'
/>
) : null}
<MiniButton
noPadding
aria-label='Закрыть'
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
icon={<IconClose size='1.25rem' />}
className='absolute z-pop right-0 mt-2 mr-2'
onClick={hideDialog}
/>
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
<div
className={clsx(
'@container/modal',
'max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
'overscroll-contain outline-hidden',
{
'overflow-auto': !overflowVisible,
'overflow-visible': overflowVisible
},
className
)}
{...restProps}
>
{children}
</div>
<Button
text='Закрыть'
aria-label='Закрыть'
className='z-pop my-2 mx-auto text-sm min-w-28'
onClick={hideDialog}
/>
</div>
</div>
);
}

View File

@ -1,3 +0,0 @@
export { ModalForm } from './ModalForm';
export { ModalLoader } from './ModalLoader';
export { ModalView } from './ModalView';

View File

@ -1,9 +0,0 @@
export { EmbedYoutube } from './EmbedYoutube';
export { Indicator } from './Indicator';
export { NoData } from './NoData';
export { PDFViewer } from './PDFViewer';
export { PrettyJson } from './PrettyJSON';
export { TextContent } from './TextContent';
export { ValueIcon } from './ValueIcon';
export { ValueLabeled } from './ValueLabeled';
export { ValueStats } from './ValueStats';

View File

@ -17,12 +17,9 @@ export function Divider({ vertical, margins = 'mx-2', className, ...restProps }:
return (
<div
className={clsx(
margins, //
className,
{
'border-x': vertical,
'border-y': !vertical
}
vertical ? 'border-x' : 'border-y', //
margins,
className
)}
{...restProps}
/>

View File

@ -0,0 +1,2 @@
export { Divider } from './divider';
export { type PlacesType, Tooltip } from './tooltip';

View File

@ -26,7 +26,6 @@ export function Tooltip({
layer = 'z-tooltip',
place = 'bottom',
className,
style,
...restProps
}: TooltipProps) {
const darkMode = usePreferencesStore(state => state.darkMode);
@ -40,6 +39,7 @@ export function Tooltip({
opacity={1}
className={clsx(
'relative',
'py-0.5! px-2!',
'max-h-[calc(100svh-6rem)]',
'overflow-y-auto overflow-x-hidden sm:overflow-hidden overscroll-contain',
'border shadow-md',
@ -48,7 +48,6 @@ export function Tooltip({
className
)}
classNameArrow={layer}
style={{ ...{ paddingTop: '2px', paddingBottom: '2px', paddingLeft: '8px', paddingRight: '8px' }, ...style }}
variant={darkMode ? 'dark' : 'light'}
place={place}
{...restProps}

View File

@ -43,15 +43,10 @@ export function Button({
'inline-flex gap-2 items-center justify-center',
'font-medium select-none disabled:cursor-auto',
'clr-btn-default cc-animate-color',
{
'border rounded-sm': !noBorder,
'px-1': dense,
'px-3 py-1': !dense,
'cursor-progress': loading,
'cursor-pointer': !loading,
'outline-hidden': noOutline,
'clr-outline': !noOutline
},
dense ? 'px-1' : 'px-3 py-1',
loading ? 'cursor-progress' : 'cursor-pointer',
noOutline ? 'outline-hidden' : 'clr-outline',
!noBorder && 'border rounded-sm',
className
)}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}

View File

@ -0,0 +1,5 @@
export { Button } from './button';
export { MiniButton } from './mini-button';
export { SelectorButton } from './selector-button';
export { SubmitButton } from './submit-button';
export { TextURL } from './text-url';

View File

@ -41,11 +41,8 @@ export function MiniButton({
'rounded-lg',
'clr-text-controls cc-animate-color',
'cursor-pointer disabled:cursor-auto',
{
'px-1 py-1': !noPadding,
'outline-hidden': noHover,
'clr-hover': !noHover
},
noHover ? 'outline-hidden' : 'clr-hover',
!noPadding && 'px-1 py-1',
className
)}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}

View File

@ -38,10 +38,7 @@ export function SelectorButton({
'text-btn clr-text-controls',
'disabled:cursor-auto cursor-pointer',
'cc-animate-color',
{
'clr-hover': transparent,
'clr-btn-default border': !transparent
},
transparent ? 'clr-hover' : 'clr-btn-default border',
className
)}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}

View File

@ -5,25 +5,21 @@ import { useMemo, useState } from 'react';
import {
type ColumnSort,
createColumnHelper,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
type PaginationState,
type RowData,
type RowSelectionState,
type SortingState,
type TableOptions,
useReactTable,
type VisibilityState
} from '@tanstack/react-table';
import clsx from 'clsx';
import { type Styling } from '../props';
import { DefaultNoData } from './DefaultNoData';
import { PaginationTools } from './PaginationTools';
import { TableBody } from './TableBody';
import { TableFooter } from './TableFooter';
import { TableHeader } from './TableHeader';
import { DefaultNoData } from './default-no-data';
import { PaginationTools } from './pagination-tools';
import { TableBody } from './table-body';
import { TableFooter } from './table-footer';
import { TableHeader } from './table-header';
import { useDataTable } from './use-data-table';
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
@ -124,49 +120,16 @@ export function DataTable<TData extends RowData>({
onRowDoubleClicked,
noDataComponent,
enableRowSelection,
rowSelection,
enableHiding,
columnVisibility,
enableSorting,
initialSorting,
enablePagination,
paginationPerPage = 10,
paginationPerPage,
paginationOptions = [10, 20, 30, 40, 50],
onChangePaginationOption,
...restProps
}: DataTableProps<TData>) {
const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []);
const [lastSelected, setLastSelected] = useState<string | null>(null);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: paginationPerPage
});
const table = useDataTable({ paginationPerPage, ...restProps });
const tableImpl = useReactTable({
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,
getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
state: {
pagination: pagination,
sorting: sorting,
rowSelection: rowSelection ?? {},
columnVisibility: columnVisibility ?? {}
},
enableHiding: enableHiding,
onPaginationChange: enablePagination ? setPagination : undefined,
onSortingChange: enableSorting ? setSorting : undefined,
enableMultiRowSelection: enableRowSelection,
...restProps
});
const isEmpty = tableImpl.getRowModel().rows.length === 0;
const isEmpty = table.getRowModel().rows.length === 0;
const fixedSize = useMemo(() => {
if (!rows) {
@ -180,49 +143,46 @@ export function DataTable<TData extends RowData>({
}, [rows, dense, noHeader, contentHeight]);
const columnSizeVars = useMemo(() => {
const headers = tableImpl.getFlatHeaders();
const headers = table.getFlatHeaders();
const colSizes: Record<string, number> = {};
for (const header of headers) {
colSizes[`--header-${header.id}-size`] = header.getSize();
colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
}
return colSizes;
}, [tableImpl]);
}, [table]);
return (
<div tabIndex={-1} id={id} className={className} style={{ minHeight: fixedSize, maxHeight: fixedSize, ...style }}>
<div
tabIndex={-1}
id={id}
className={clsx('table-auto', className)}
style={{ minHeight: fixedSize, maxHeight: fixedSize, ...style }}
>
<table className='w-full' style={{ ...columnSizeVars }}>
{!noHeader ? (
<TableHeader
table={tableImpl}
enableRowSelection={enableRowSelection}
enableSorting={enableSorting}
headPosition={headPosition}
resetLastSelected={() => setLastSelected(null)}
/>
<TableHeader table={table} headPosition={headPosition} resetLastSelected={() => setLastSelected(null)} />
) : null}
<TableBody
table={tableImpl}
table={table}
dense={dense}
noHeader={noHeader}
conditionalRowStyles={conditionalRowStyles}
enableRowSelection={enableRowSelection}
lastSelected={lastSelected}
onChangeLastSelected={setLastSelected}
onRowClicked={onRowClicked}
onRowDoubleClicked={onRowDoubleClicked}
/>
{!noFooter ? <TableFooter table={tableImpl} /> : null}
{!noFooter ? <TableFooter table={table} /> : null}
</table>
{enablePagination && !isEmpty ? (
{!!paginationPerPage && !isEmpty ? (
<PaginationTools
id={id ? `${id}__pagination` : undefined}
table={tableImpl}
table={table}
paginationOptions={paginationOptions}
onChangePaginationOption={onChangePaginationOption}
/>
) : null}
{isEmpty ? noDataComponent ?? <DefaultNoData /> : null}

View File

@ -4,4 +4,4 @@ export {
type IConditionalStyle,
type RowSelectionState,
type VisibilityState
} from './DataTable';
} from './data-table';

View File

@ -1,35 +1,26 @@
'use client';
'use no memo';
'use client';
import { useCallback } from 'react';
import { type Table } from '@tanstack/react-table';
import { prefixes } from '@/utils/constants';
import { IconPageFirst, IconPageLast, IconPageLeft, IconPageRight } from '../Icons';
import { IconPageFirst, IconPageLast, IconPageLeft, IconPageRight } from '../icons';
interface PaginationToolsProps<TData> {
id?: string;
table: Table<TData>;
paginationOptions: number[];
onChangePaginationOption?: (newValue: number) => void;
}
export function PaginationTools<TData>({
id,
table,
paginationOptions,
onChangePaginationOption
}: PaginationToolsProps<TData>) {
export function PaginationTools<TData>({ id, table, paginationOptions }: PaginationToolsProps<TData>) {
const handlePaginationOptionsChange = useCallback(
(event: React.ChangeEvent<HTMLSelectElement>) => {
const perPage = Number(event.target.value);
table.setPageSize(perPage);
if (onChangePaginationOption) {
onChangePaginationOption(perPage);
}
},
[onChangePaginationOption, table]
[table]
);
return (

View File

@ -2,7 +2,7 @@
import { type Table } from '@tanstack/react-table';
import { CheckboxTristate } from '../Input';
import { CheckboxTristate } from '../input';
interface SelectAllProps<TData> {
table: Table<TData>;

View File

@ -2,7 +2,7 @@
import { type Row } from '@tanstack/react-table';
import { Checkbox } from '../Input';
import { Checkbox } from '../input';
interface SelectRowProps<TData> {
row: Row<TData>;

View File

@ -0,0 +1,15 @@
import { IconSortAsc, IconSortDesc } from '../icons';
interface SortingIconProps {
sortDirection?: 'asc' | 'desc' | false;
}
export function SortingIcon({ sortDirection }: SortingIconProps) {
if (sortDirection === 'asc') {
return <IconSortAsc size='1rem' />;
}
if (sortDirection === 'desc') {
return <IconSortDesc size='1rem' />;
}
return <IconSortDesc size='1rem' className='opacity-0 group-hover:opacity-25 transition-opacity' />;
}

View File

@ -0,0 +1,112 @@
'use no memo';
import { useCallback } from 'react';
import { type Cell, flexRender, type Row, type Table } from '@tanstack/react-table';
import clsx from 'clsx';
import { SelectRow } from './select-row';
import { type IConditionalStyle } from '.';
interface TableBodyProps<TData> {
table: Table<TData>;
dense?: boolean;
noHeader?: boolean;
conditionalRowStyles?: IConditionalStyle<TData>[];
lastSelected: string | null;
onChangeLastSelected: (newValue: string | null) => void;
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void;
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void;
}
export function TableBody<TData>({
table,
dense,
noHeader,
conditionalRowStyles,
lastSelected,
onChangeLastSelected,
onRowClicked,
onRowDoubleClicked
}: TableBodyProps<TData>) {
const handleRowClicked = useCallback(
(target: Row<TData>, event: React.MouseEvent<Element>) => {
onRowClicked?.(target.original, event);
if (target.getCanSelect()) {
if (event.shiftKey && !!lastSelected && lastSelected !== target.id) {
const { rows, rowsById } = table.getRowModel();
const lastIndex = rowsById[lastSelected].index;
const currentIndex = target.index;
const toggleRows = rows.slice(
lastIndex > currentIndex ? currentIndex : lastIndex + 1,
lastIndex > currentIndex ? lastIndex : currentIndex + 1
);
const newSelection: Record<string, boolean> = {};
toggleRows.forEach(row => {
newSelection[row.id] = !target.getIsSelected();
});
table.setRowSelection(prev => ({ ...prev, ...newSelection }));
onChangeLastSelected(null);
} else {
onChangeLastSelected(target.id);
target.toggleSelected(!target.getIsSelected());
}
}
},
[table, lastSelected, onChangeLastSelected, onRowClicked]
);
const getRowStyles = useCallback(
(row: Row<TData>) => {
return {
...conditionalRowStyles!
.filter(item => item.when(row.original))
.reduce((prev, item) => ({ ...prev, ...item.style }), {})
};
},
[conditionalRowStyles]
);
return (
<tbody>
{table.getRowModel().rows.map((row: Row<TData>, index) => (
<tr
key={row.id}
className={clsx(
'cc-scroll-row',
'clr-hover cc-animate-color',
!noHeader && 'scroll-mt-[calc(2px+2rem)]',
table.options.enableRowSelection && row.getIsSelected()
? 'clr-selected'
: 'odd:bg-prim-200 even:bg-prim-100'
)}
style={{ ...(conditionalRowStyles ? getRowStyles(row) : []) }}
onClick={event => handleRowClicked(row, event)}
onDoubleClick={event => onRowDoubleClicked?.(row.original, event)}
>
{table.options.enableRowSelection ? (
<td key={`select-${row.id}`} className='pl-2 border-y'>
<SelectRow row={row} onChangeLastSelected={onChangeLastSelected} />
</td>
) : null}
{row.getVisibleCells().map((cell: Cell<TData, unknown>) => (
<td
key={cell.id}
className={clsx(
'px-2 align-middle border-y',
dense ? 'py-1' : 'py-2',
onRowClicked || onRowDoubleClicked ? 'cursor-pointer' : 'cursor-auto'
)}
style={{
width: noHeader && index === 0 ? `calc(var(--col-${cell.column.id}-size) * 1px)` : undefined
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
);
}

View File

@ -0,0 +1,51 @@
'use no memo';
import { flexRender, type Header, type HeaderGroup, type Table } from '@tanstack/react-table';
import clsx from 'clsx';
import { SelectAll } from './select-all';
import { SortingIcon } from './sorting-icon';
interface TableHeaderProps<TData> {
table: Table<TData>;
headPosition?: string;
resetLastSelected: () => void;
}
export function TableHeader<TData>({ table, headPosition, resetLastSelected }: TableHeaderProps<TData>) {
return (
<thead className='sticky bg-prim-100 cc-shadow-border' style={{ top: headPosition }}>
{table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
<tr key={headerGroup.id}>
{table.options.enableRowSelection ? (
<th className='pl-2' scope='col'>
<SelectAll table={table} resetLastSelected={resetLastSelected} />
</th>
) : null}
{headerGroup.headers.map((header: Header<TData, unknown>) => (
<th
key={header.id}
colSpan={header.colSpan}
scope='col'
className={clsx(
'cc-table-header group',
table.options.enableSorting && header.column.getCanSort() ? 'cursor-pointer' : 'cursor-auto'
)}
style={{ width: `calc(var(--header-${header?.id}-size) * 1px)` }}
onClick={table.options.enableSorting ? header.column.getToggleSortingHandler() : undefined}
>
{!header.isPlaceholder ? (
<span className='inline-flex gap-1'>
{flexRender(header.column.columnDef.header, header.getContext())}
{table.options.enableSorting && header.column.getCanSort() ? (
<SortingIcon sortDirection={header.column.getIsSorted()} />
) : null}
</span>
) : null}
</th>
))}
</tr>
))}
</thead>
);
}

View File

@ -0,0 +1,125 @@
'use client';
import { useCallback, useState } from 'react';
import {
type ColumnSort,
createColumnHelper,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
type PaginationState,
type RowData,
type RowSelectionState,
type SortingState,
type TableOptions,
type Updater,
useReactTable,
type VisibilityState
} from '@tanstack/react-table';
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
/** Style to conditionally apply to rows. */
export interface IConditionalStyle<TData> {
/** Callback to determine if the style should be applied. */
when: (rowData: TData) => boolean;
/** Style to apply. */
style: React.CSSProperties;
}
export interface UseDataTableProps<TData extends RowData>
extends Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
/** Enable row selection. */
enableRowSelection?: boolean;
/** Current row selection. */
rowSelection?: RowSelectionState;
/** Enable hiding of columns. */
enableHiding?: boolean;
/** Current column visibility. */
columnVisibility?: VisibilityState;
/** Enable pagination. */
enablePagination?: boolean;
/** Number of rows per page. */
paginationPerPage?: number;
/** Callback to be called when the pagination option is changed. */
onChangePaginationOption?: (newValue: number) => void;
/** Enable sorting. */
enableSorting?: boolean;
/** Initial sorting. */
initialSorting?: ColumnSort;
}
/**
* Dta representation as a table.
*
* @param headPosition - Top position of sticky header (0 if no other sticky elements are present).
* No sticky header if omitted
*/
export function useDataTable<TData extends RowData>({
enableRowSelection,
rowSelection,
enableHiding,
columnVisibility,
enableSorting,
initialSorting,
enablePagination,
paginationPerPage = 10,
onChangePaginationOption,
...restProps
}: UseDataTableProps<TData>) {
const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []);
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: paginationPerPage
});
const handleChangePagination = useCallback(
(updater: Updater<PaginationState>) => {
setPagination(prev => {
const resolvedValue = typeof updater === 'function' ? updater(prev) : updater;
if (onChangePaginationOption && prev.pageSize !== resolvedValue.pageSize) {
onChangePaginationOption(resolvedValue.pageSize);
}
return resolvedValue;
});
},
[onChangePaginationOption]
);
const table = useReactTable({
state: {
pagination: pagination,
sorting: sorting,
rowSelection: rowSelection,
columnVisibility: columnVisibility
},
getCoreRowModel: getCoreRowModel(),
enableSorting: enableSorting,
getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,
onSortingChange: enableSorting ? setSorting : undefined,
getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
onPaginationChange: enablePagination ? handleChangePagination : undefined,
enableHiding: enableHiding,
enableMultiRowSelection: enableRowSelection,
enableRowSelection: enableRowSelection,
...restProps
});
return table;
}

View File

@ -39,11 +39,7 @@ export function DropdownButton({
'text-left text-sm text-ellipsis whitespace-nowrap',
'disabled:clr-text-controls',
'cc-animate-color',
{
'clr-hover': onClick,
'cursor-pointer disabled:cursor-auto': onClick,
'cursor-default': !onClick
},
!!onClick ? 'clr-hover cursor-pointer disabled:cursor-auto' : 'clr-btn-default',
className
)}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}

View File

@ -1,6 +1,6 @@
import clsx from 'clsx';
import { Checkbox, type CheckboxProps } from '../Input';
import { Checkbox, type CheckboxProps } from '../input';
/** Animated {@link Checkbox} inside a {@link Dropdown} item. */
export function DropdownCheckbox({ onChange: setValue, disabled, ...restProps }: CheckboxProps) {

View File

@ -1,8 +1,6 @@
import React from 'react';
import clsx from 'clsx';
import { PARAMETER } from '@/utils/constants';
import { type Styling } from '../props';
interface DropdownProps extends Styling {
@ -36,36 +34,19 @@ export function Dropdown({
margin,
className,
children,
style,
...restProps
}: React.PropsWithChildren<DropdownProps>) {
return (
<div
tabIndex={-1}
className={clsx(
'z-topmost absolute',
{
'right-0': stretchLeft,
'left-0': !stretchLeft,
'bottom-0': stretchTop,
'top-full': !stretchTop
},
'grid',
'border rounded-md shadow-lg',
'clr-input',
'text-sm',
'cc-dropdown isolate z-topmost absolute grid bg-prim-0 border rounded-md shadow-lg text-sm',
stretchLeft ? 'right-0' : 'left-0',
stretchTop ? 'bottom-0' : 'top-full',
isOpen && 'open',
margin,
className
)}
style={{
willChange: 'clip-path, transform',
transitionProperty: 'clip-path, transform',
transitionDuration: `${PARAMETER.dropdownDuration}ms`,
transitionTimingFunction: 'ease-in-out',
transform: isOpen ? 'translateY(0)' : 'translateY(-10%)',
clipPath: isOpen ? 'inset(0% 0% 0% 0%)' : 'inset(10% 0% 90% 0%)',
...style
}}
aria-hidden={!isOpen}
{...restProps}
>

View File

@ -0,0 +1,4 @@
export { Dropdown } from './dropdown';
export { DropdownButton } from './dropdown-button';
export { DropdownCheckbox } from './dropdown-checkbox';
export { useDropdown } from './use-dropdown';

View File

@ -2,7 +2,7 @@
import { useRef, useState } from 'react';
import { useClickedOutside } from '@/hooks/useClickedOutside';
import { useClickedOutside } from '@/hooks/use-clicked-outside';
export function useDropdown() {
const [isOpen, setIsOpen] = useState(false);

View File

@ -196,9 +196,10 @@ export function IconLogin(props: IconProps) {
);
}
export function CheckboxChecked() {
return (
<svg className='w-4 h-4 p-0.75' viewBox='0 0 512 512' fill='#ffffff'>
<svg className='w-4 h-4 p-0.75 -ml-0.25' viewBox='0 0 512 512' fill='#ffffff'>
<path d='M470.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 338.7l233.4-233.3c12.5-12.5 32.8-12.5 45.3 0z' />
</svg>
);
@ -206,7 +207,7 @@ export function CheckboxChecked() {
export function CheckboxNull() {
return (
<svg className='w-4 h-4 p-0.25' viewBox='0 0 16 16' fill='#ffffff'>
<svg className='w-4 h-4 px-0.25' viewBox='0 0 16 16' fill='#ffffff'>
<path d='M2 7.75A.75.75 0 012.75 7h10a.75.75 0 010 1.5h-10A.75.75 0 012 7.75z' />
</svg>
);

View File

@ -1,10 +1,10 @@
import clsx from 'clsx';
import { ZodError } from 'zod';
import { type AxiosError, isAxiosError } from '@/backend/apiTransport';
import { type AxiosError, isAxiosError } from '@/backend/api-transport';
import { isResponseHtml } from '@/utils/utils';
import { PrettyJson } from './View';
import { PrettyJson } from './view';
export type ErrorData = string | Error | AxiosError | ZodError;
@ -33,18 +33,7 @@ export function DescribeError({ error }: { error: ErrorData }) {
<p>
<b>Message:</b> {error.message}
</p>
{error.stack && (
<pre
style={{
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
padding: '6px',
overflowX: 'auto'
}}
>
{error.stack}
</pre>
)}
{error.stack && <pre className='whitespace-pre-wrap p-2 overflow-x-auto break-words'>{error.stack}</pre>}
</div>
);
}

View File

@ -2,9 +2,9 @@ import clsx from 'clsx';
import { globalIDs } from '@/utils/constants';
import { CheckboxChecked, CheckboxNull } from '../Icons';
import { CheckboxChecked, CheckboxNull } from '../icons';
import { type CheckboxProps } from './Checkbox';
import { type CheckboxProps } from './checkbox';
export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'onChange'> {
/** Current value - `null`, `true` or `false`. */
@ -66,11 +66,8 @@ export function CheckboxTristate({
<div
className={clsx(
'w-4 h-4', //
'border rounded-xs',
{
'bg-sec-600 text-sec-0': value !== false,
'bg-prim-100': value === false
}
'border rounded-sm',
value === false ? 'bg-prim-100' : 'bg-sec-600 text-sec-0'
)}
>
{value ? <CheckboxChecked /> : null}

View File

@ -2,7 +2,7 @@ import clsx from 'clsx';
import { globalIDs } from '@/utils/constants';
import { CheckboxChecked } from '../Icons';
import { CheckboxChecked } from '../icons';
import { type Button } from '../props';
export interface CheckboxProps extends Omit<Button, 'value' | 'onClick' | 'onChange'> {
@ -65,11 +65,8 @@ export function Checkbox({
<div
className={clsx(
'w-4 h-4', //
'border rounded-xs',
{
'bg-sec-600 text-sec-0': value !== false,
'bg-prim-100': value === false
}
'border rounded-sm',
value === false ? 'bg-prim-100' : 'bg-sec-600 text-sec-0'
)}
>
{value ? <CheckboxChecked /> : null}

View File

@ -3,11 +3,11 @@
import { useRef, useState } from 'react';
import clsx from 'clsx';
import { Button } from '../Control';
import { IconUpload } from '../Icons';
import { Button } from '../control';
import { IconUpload } from '../icons';
import { type Titled } from '../props';
import { Label } from './Label';
import { Label } from './label';
interface FileInputProps extends Titled, Omit<React.ComponentProps<'input'>, 'accept' | 'type'> {
/** Label to display in file upload button. */
@ -46,7 +46,7 @@ export function FileInput({ id, label, acceptType, title, className, style, onCh
id={id}
type='file'
ref={inputRef}
style={{ display: 'none' }}
className='hidden'
accept={acceptType}
onChange={handleFileChange}
{...restProps}

View File

@ -0,0 +1,11 @@
export { Checkbox, type CheckboxProps } from './checkbox';
export { CheckboxTristate } from './checkbox-tristate';
export { ErrorField } from './error-field';
export { FileInput } from './file-input';
export { Label } from './label';
export { SearchBar } from './search-bar';
export { SelectMulti, type SelectMultiProps } from './select-multi';
export { SelectSingle, type SelectSingleProps } from './select-single';
export { SelectTree } from './select-tree';
export { TextArea } from './text-area';
export { TextInput } from './text-input';

View File

@ -1,9 +1,9 @@
import clsx from 'clsx';
import { IconSearch } from '@/components/Icons';
import { IconSearch } from '@/components/icons';
import { type Styling } from '@/components/props';
import { TextInput } from './TextInput';
import { TextInput } from './text-input';
interface SearchBarProps extends Styling {
/** Id of the search bar. */
@ -39,10 +39,10 @@ export function SearchBar({
...restProps
}: SearchBarProps) {
return (
<div className={clsx('relative', className)} {...restProps}>
<div className={clsx('relative flex items-center', className)} {...restProps}>
{!noIcon ? (
<IconSearch
className='absolute -top-0.5 left-3 translate-y-1/2 pointer-events-none clr-text-controls'
className='absolute -top-0.5 left-2 translate-y-1/2 pointer-events-none clr-text-controls'
size='1.25rem'
/>
) : null}
@ -52,7 +52,7 @@ export function SearchBar({
transparent
placeholder={placeholder}
type='search'
className={clsx('bg-transparent', !noIcon && 'pl-10')}
className={clsx('bg-transparent', !noIcon && 'pl-8')}
noBorder={noBorder}
value={query}
onChange={event => onChangeQuery?.(event.target.value)}

View File

@ -9,10 +9,10 @@ import Select, {
type StylesConfig
} from 'react-select';
import { useWindowSize } from '@/hooks/useWindowSize';
import { useWindowSize } from '@/hooks/use-window-size';
import { APP_COLORS, SELECT_THEME } from '@/styling/colors';
import { IconClose, IconDropArrow, IconDropArrowUp } from '../Icons';
import { IconClose, IconDropArrow, IconDropArrowUp } from '../icons';
function DropdownIndicator<Option, Group extends GroupBase<Option> = GroupBase<Option>>(
props: DropdownIndicatorProps<Option, true, Group>

View File

@ -9,10 +9,10 @@ import Select, {
type StylesConfig
} from 'react-select';
import { useWindowSize } from '@/hooks/useWindowSize';
import { useWindowSize } from '@/hooks/use-window-size';
import { APP_COLORS, SELECT_THEME } from '@/styling/colors';
import { IconClose, IconDropArrow, IconDropArrowUp } from '../Icons';
import { IconClose, IconDropArrow, IconDropArrowUp } from '../icons';
function DropdownIndicator<Option, Group extends GroupBase<Option> = GroupBase<Option>>(
props: DropdownIndicatorProps<Option, false, Group>

View File

@ -1,10 +1,10 @@
import { useEffect, useState } from 'react';
import clsx from 'clsx';
import { globalIDs, PARAMETER } from '@/utils/constants';
import { globalIDs } from '@/utils/constants';
import { MiniButton } from '../Control';
import { IconDropArrow, IconPageRight } from '../Icons';
import { MiniButton } from '../control';
import { IconDropArrow, IconPageRight } from '../icons';
import { type Styling } from '../props';
interface SelectTreeProps<ItemType> extends Styling {
@ -89,27 +89,13 @@ export function SelectTree<ItemType>({
<div
key={`${prefix}${index}`}
className={clsx(
'relative',
'pr-3 pl-6 border-b',
'cc-scroll-row',
'bg-prim-200 clr-hover cc-animate-color',
'cursor-pointer',
value === item && 'clr-selected',
!isActive && 'pointer-events-none'
'cc-tree-item relative cc-scroll-row clr-hover',
isActive ? 'max-h-7 py-1 border-b' : 'max-h-0 opacity-0 pointer-events-none',
value === item && 'clr-selected'
)}
data-tooltip-id={globalIDs.tooltip}
data-tooltip-html={getDescription(item)}
onClick={event => handleClickItem(event, item)}
style={{
borderBottomWidth: isActive ? '1px' : '0px',
willChange: 'max-height, opacity, padding',
transitionProperty: 'max-height, opacity, padding',
transitionDuration: `${PARAMETER.moveDuration}ms`,
paddingTop: isActive ? '0.25rem' : '0',
paddingBottom: isActive ? '0.25rem' : '0',
maxHeight: isActive ? '1.75rem' : '0',
opacity: isActive ? '1' : '0'
}}
>
{foldable.has(item) ? (
<MiniButton

View File

@ -1,9 +1,9 @@
import clsx from 'clsx';
import { Label } from '../Input/Label';
import { type Editor, type ErrorProcessing, type Titled } from '../props';
import { ErrorField } from './ErrorField';
import { ErrorField } from './error-field';
import { Label } from './label';
export interface TextAreaProps extends Editor, ErrorProcessing, Titled, React.ComponentProps<'textarea'> {
/** Indicates that the input should be transparent. */
@ -40,11 +40,8 @@ export function TextArea({
return (
<div
className={clsx(
'w-full',
{
'flex flex-col': !dense,
'flex grow items-center gap-3': dense
},
'w-full', //
dense ? 'flex grow items-center gap-3' : 'flex flex-col',
dense && className
)}
>
@ -55,16 +52,13 @@ export function TextArea({
'px-3 py-2',
'leading-tight',
'overflow-x-hidden overflow-y-auto',
{
'field-sizing-content': fitContent,
'resize-none': noResize,
'border': !noBorder,
'grow max-w-full': dense,
'mt-2': !dense && !!label,
'clr-outline': !noOutline,
'bg-transparent': transparent,
'clr-input': !transparent
},
!noBorder && 'border',
fitContent && 'field-sizing-content',
noResize && 'resize-none',
transparent ? 'bg-transparent' : 'clr-input',
!noOutline && 'clr-outline',
dense && 'grow max-w-full',
!dense && !!label && 'mt-2',
!dense && className
)}
rows={rows}

View File

@ -1,9 +1,9 @@
import clsx from 'clsx';
import { Label } from '../Input/Label';
import { type Editor, type ErrorProcessing, type Titled } from '../props';
import { ErrorField } from './ErrorField';
import { ErrorField } from './error-field';
import { Label } from './label';
interface TextInputProps extends Editor, ErrorProcessing, Titled, React.ComponentProps<'input'> {
/** Indicates that the input should be transparent. */
@ -42,10 +42,7 @@ export function TextInput({
return (
<div
className={clsx(
{
'flex flex-col': !dense,
'flex items-center gap-3': dense
},
dense ? 'flex items-center gap-3' : 'flex flex-col', //
dense && className
)}
>
@ -55,15 +52,12 @@ export function TextInput({
className={clsx(
'min-w-0 py-2',
'leading-tight truncate hover:text-clip',
{
'px-3': !noBorder || !disabled,
'grow max-w-full': dense,
'mt-2': !dense && !!label,
'border': !noBorder,
'clr-outline': !noOutline,
'bg-transparent': transparent,
'clr-input': !transparent
},
transparent ? 'bg-transparent' : 'clr-input',
!noBorder && 'border',
!noOutline && 'clr-outline',
(!noBorder || !disabled) && 'px-3',
dense && 'grow max-w-full',
!dense && !!label && 'mt-2',
!dense && className
)}
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown}

View File

@ -0,0 +1,3 @@
export { ModalForm } from './modal-form';
export { ModalLoader } from './modal-loader';
export { ModalView } from './modal-view';

View File

@ -5,15 +5,15 @@ import clsx from 'clsx';
import { type HelpTopic } from '@/features/help';
import { BadgeHelp } from '@/features/help/components';
import { useEscapeKey } from '@/hooks/useEscapeKey';
import { useEscapeKey } from '@/hooks/use-escape-key';
import { useDialogsStore } from '@/stores/dialogs';
import { prepareTooltip } from '@/utils/utils';
import { Button, MiniButton, SubmitButton } from '../Control';
import { IconClose } from '../Icons';
import { Button, MiniButton, SubmitButton } from '../control';
import { IconClose } from '../icons';
import { type Styling } from '../props';
import { ModalBackdrop } from './ModalBackdrop';
import { ModalBackdrop } from './modal-backdrop';
export interface ModalProps extends Styling {
/** Title of the modal window. */
@ -90,7 +90,7 @@ export function ModalForm({
<div className='cc-modal-wrapper'>
<ModalBackdrop onHide={handleCancel} />
<form
className='cc-animate-modal grid border rounded-xl bg-prim-100'
className='cc-animate-modal relative grid border rounded-xl bg-prim-100'
role='dialog'
onSubmit={handleSubmit}
aria-labelledby='modal-title'
@ -98,7 +98,7 @@ export function ModalForm({
{helpTopic && !hideHelpWhen?.() ? (
<BadgeHelp
topic={helpTopic}
className='absolute z-pop left-0 mt-2 ml-2'
className='absolute z-top top-2 left-2'
padding='p-0'
contentClass='sm:max-w-160'
/>
@ -109,7 +109,7 @@ export function ModalForm({
aria-label='Закрыть'
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
icon={<IconClose size='1.25rem' />}
className='absolute z-pop right-0 mt-2 mr-2'
className='absolute z-pop top-2 right-2'
onClick={hideDialog}
/>
@ -124,10 +124,7 @@ export function ModalForm({
'@container/modal',
'max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
'overscroll-contain outline-hidden',
{
'overflow-auto': !overflowVisible,
'overflow-visible': overflowVisible
},
overflowVisible ? 'overflow-visible' : 'overflow-auto',
className
)}
{...restProps}

View File

@ -1,6 +1,6 @@
import { Loader } from '@/components/Loader';
import { Loader } from '@/components/loader';
import { ModalBackdrop } from './ModalBackdrop';
import { ModalBackdrop } from './modal-backdrop';
export function ModalLoader() {
return (

View File

@ -0,0 +1,103 @@
'use client';
import clsx from 'clsx';
import { BadgeHelp } from '@/features/help/components';
import { useEscapeKey } from '@/hooks/use-escape-key';
import { useDialogsStore } from '@/stores/dialogs';
import { prepareTooltip } from '@/utils/utils';
import { Button, MiniButton } from '../control';
import { IconClose } from '../icons';
import { ModalBackdrop } from './modal-backdrop';
import { type ModalProps } from './modal-form';
interface ModalViewProps extends ModalProps {
/** Float all UI elements on top of contents. */
fullScreen?: boolean;
}
/**
* Displays a customizable modal window with submit form.
*/
export function ModalView({
children,
className,
header,
overflowVisible,
helpTopic,
hideHelpWhen,
fullScreen,
...restProps
}: React.PropsWithChildren<ModalViewProps>) {
const hideDialog = useDialogsStore(state => state.hideDialog);
useEscapeKey(hideDialog);
return (
<div className='cc-modal-wrapper'>
<ModalBackdrop onHide={hideDialog} />
<div className='cc-animate-modal relative grid border rounded-xl bg-prim-100' role='dialog'>
{helpTopic && !hideHelpWhen?.() ? (
<BadgeHelp
topic={helpTopic}
className='absolute z-pop top-2 left-2'
padding='p-0'
contentClass='sm:max-w-160'
/>
) : null}
<MiniButton
noPadding
aria-label='Закрыть'
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
icon={<IconClose size='1.25rem' />}
className='absolute z-pop top-2 right-2'
onClick={hideDialog}
/>
{header ? (
<h1
className={clsx(
'px-12 py-2 select-none',
fullScreen && 'z-pop absolute top-0 right-1/2 translate-x-1/2 backdrop-blur-xs bg-prim-100/90 rounded-2xl'
)}
>
{header}
</h1>
) : null}
<div
className={clsx(
'@container/modal',
'max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)]',
'overscroll-contain outline-hidden',
overflowVisible ? 'overflow-visible' : 'overflow-auto',
className
)}
{...restProps}
>
{children}
</div>
{!fullScreen ? (
<Button
text='Закрыть'
aria-label='Закрыть'
className={clsx(
'my-2 mx-auto text-sm min-w-28',
fullScreen && 'z-pop absolute bottom-0 right-1/2 translate-x-1/2'
)}
onClick={hideDialog}
/>
) : (
<div className='z-pop absolute bottom-0 right-1/2 translate-x-1/2 p-3 rounded-xl bg-prim-100/90 backdrop-blur-xs'>
{' '}
<Button text='Закрыть' aria-label='Закрыть' className='text-sm min-w-28' onClick={hideDialog} />
</div>
)}
</div>
</div>
);
}

View File

@ -1,2 +1,2 @@
export { TabLabel } from './TabLabel';
export { TabLabel } from './tab-label';
export { TabList, TabPanel, Tabs } from 'react-tabs';

View File

@ -18,9 +18,8 @@ export function EmbedYoutube({ videoID, pxHeight, pxWidth }: EmbedYoutubeProps)
}
return (
<div
className='relative'
className='relative h-0'
style={{
height: 0,
paddingBottom: `${pxHeight}px`,
paddingLeft: `${pxWidth}px`
}}

View File

@ -0,0 +1,9 @@
export { EmbedYoutube } from './embed-youtube';
export { Indicator } from './indicator';
export { NoData } from './no-data';
export { PDFViewer } from './pdf-viewer';
export { PrettyJson } from './pretty-json';
export { TextContent } from './text-content';
export { ValueIcon } from './value-icon';
export { ValueLabeled } from './value-labeled';
export { ValueStats } from './value-stats';

View File

@ -18,11 +18,9 @@ export function Indicator({ icon, title, titleHtml, hideTitle, noPadding, classN
return (
<div
className={clsx(
'clr-text-controls',
'clr-text-controls', //
'outline-hidden',
{
'px-1 py-1': !noPadding
},
!noPadding && 'px-1 py-1',
className
)}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined}

View File

@ -1,7 +1,7 @@
'use client';
import { useWindowSize } from '@/hooks/useWindowSize';
import { useFitHeight } from '@/stores/appLayout';
import { useWindowSize } from '@/hooks/use-window-size';
import { useFitHeight } from '@/stores/app-layout';
/** Maximum width of the viewer. */
const MAXIMUM_WIDTH = 1600;

View File

@ -2,7 +2,7 @@ import clsx from 'clsx';
import { globalIDs } from '@/utils/constants';
import { MiniButton } from '../Control';
import { MiniButton } from '../control';
import { type Styling, type Titled } from '../props';
interface ValueIconProps extends Styling, Titled {
@ -57,7 +57,7 @@ export function ValueIcon({
'flex items-center',
'text-right',
'hover:cursor-default',
{ 'justify-between gap-6': !dense, 'gap-1': dense },
dense ? 'gap-1' : 'justify-between gap-6',
className
)}
{...restProps}

View File

@ -1,6 +1,6 @@
import { type Styling, type Titled } from '@/components/props';
import { ValueIcon } from './ValueIcon';
import { ValueIcon } from './value-icon';
// characters - threshold for small labels - small font
const SMALL_THRESHOLD = 3;

View File

@ -1,6 +1,6 @@
import { queryOptions } from '@tanstack/react-query';
import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
import { axiosGet, axiosPatch, axiosPost } from '@/backend/api-transport';
import { DELAYS, KEYS } from '@/backend/configuration';
import { infoMsg } from '@/utils/labels';

View File

@ -1,6 +1,6 @@
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { queryClient } from '@/backend/queryClient';
import { queryClient } from '@/backend/query-client';
import { authApi } from './api';

Some files were not shown because too many files have changed in this diff Show More