Compare commits

..

No commits in common. "6e1d99122e9c5f91228c9b9cb9d1432fb11640ba" and "4113370c62f2565b0a90442172dc24820fe15b2a" have entirely different histories.

390 changed files with 3609 additions and 3735 deletions

View File

@ -40,7 +40,6 @@ This readme file is used mostly to document project dependencies and conventions
- react-tooltip - react-tooltip
- react-zoom-pan-pinch - react-zoom-pan-pinch
- react-hook-form - react-hook-form
- react-scan
- reactflow - reactflow
- js-file-download - js-file-download
- use-debounce - use-debounce
@ -62,6 +61,8 @@ This readme file is used mostly to document project dependencies and conventions
<summary>npm install -D</summary> <summary>npm install -D</summary>
<pre> <pre>
- tailwindcss - tailwindcss
- postcss
- autoprefixer
- eslint-plugin-import - eslint-plugin-import
- eslint-plugin-react-compiler - eslint-plugin-react-compiler
- eslint-plugin-simple-import-sort - eslint-plugin-simple-import-sort
@ -71,7 +72,6 @@ This readme file is used mostly to document project dependencies and conventions
- vite - vite
- jest - jest
- ts-jest - ts-jest
- @vitejs/plugin-react
- @types/jest - @types/jest
- @lezer/generator - @lezer/generator
- @playwright/test - @playwright/test

View File

@ -2,16 +2,16 @@
For more specific TODOs see comments in code For more specific TODOs see comments in code
[Bugs - PENDING] [Bugs - PENDING]
- - Tab index still selecting background elements when modal is active
[Functionality - PENDING] [Functionality - PENDING]
- Landing page - Landing page
- Design first user experience - Design first user experience
- Demo sandbox for anonymous users - Demo sandbox for anonymous users
User profile: - User profile: Settings + settings server persistency
- Settings + settings server persistency
- Profile pictures - Profile pictures
- Integrate socials and feedback
- Custom LibraryItem lists - Custom LibraryItem lists
- Custom user filters and sharing filters - Custom user filters and sharing filters
@ -31,7 +31,6 @@ User profile:
[Functionality - CANCELED] [Functionality - CANCELED]
- User notifications on edit - consider spam prevention and change aggregation - User notifications on edit - consider spam prevention and change aggregation
- Integrate socials and feedback
- Content based search in Library - Content based search in Library
- Private projects. Consider cooperative editing - Private projects. Consider cooperative editing
- OSS: synthesis table: auto substitution for diamond synthesis - OSS: synthesis table: auto substitution for diamond synthesis
@ -39,7 +38,10 @@ User profile:
[Tech] [Tech]
- duplicate syntax parsing and type info calculations to client. Consider moving backend to Nodejs or embedding c++ lib - duplicate syntax parsing and type info calculations to client. Consider moving backend to Nodejs or embedding c++ lib
- Testing E2E playwright - add debounce to some search fields. Consider pagination and dynamic loading
- move autopep8 and isort settings from vscode settings to pyproject.toml
- Testing: frontend react components, testplane / playwright?
[Deployment] [Deployment]
@ -68,3 +70,11 @@ https://drf-standardized-errors.readthedocs.io/en/latest/error_response.html
https://stackoverflow.com/questions/28838170/multilevel-json-diff-in-python https://stackoverflow.com/questions/28838170/multilevel-json-diff-in-python
- Documentation platform. Consider diplodoc - Documentation platform. Consider diplodoc
- radix-ui
- shadcn-ui
- Zod
- react-query
- react-hook-form

View File

@ -11,7 +11,15 @@ export default [
...typescriptPlugin.configs.recommendedTypeChecked, ...typescriptPlugin.configs.recommendedTypeChecked,
...typescriptPlugin.configs.stylisticTypeChecked, ...typescriptPlugin.configs.stylisticTypeChecked,
{ {
ignores: ['**/parser.ts', '**/node_modules/**', '**/public/**', '**/dist/**', 'eslint.config.js'] ignores: [
'**/parser.ts',
'**/node_modules/**',
'**/public/**',
'**/dist/**',
'eslint.config.js',
'tailwind.config.js',
'postcss.config.js'
]
}, },
{ {
languageOptions: { languageOptions: {
@ -35,12 +43,6 @@ export default [
settings: { react: { version: 'detect' } }, settings: { react: { version: 'detect' } },
rules: { rules: {
'react-compiler/react-compiler': 'error', 'react-compiler/react-compiler': 'error',
'@typescript-eslint/consistent-type-imports': [
'warn',
{
fixStyle: 'inline-type-imports'
}
],
'@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'with-single-extends' }], '@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'with-single-extends' }],
'@typescript-eslint/prefer-nullish-coalescing': 'off', '@typescript-eslint/prefer-nullish-coalescing': 'off',
'@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/no-inferrable-types': 'off',

File diff suppressed because it is too large Load Diff

View File

@ -16,14 +16,13 @@
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@hookform/resolvers": "^4.1.0", "@hookform/resolvers": "^4.1.0",
"@lezer/lr": "^1.4.2", "@lezer/lr": "^1.4.2",
"@tanstack/react-query": "^5.66.8", "@tanstack/react-query": "^5.66.7",
"@tanstack/react-query-devtools": "^5.66.8", "@tanstack/react-query-devtools": "^5.66.7",
"@tanstack/react-table": "^8.21.2", "@tanstack/react-table": "^8.21.2",
"@uiw/codemirror-themes": "^4.23.8", "@uiw/codemirror-themes": "^4.23.8",
"@uiw/react-codemirror": "^4.23.8", "@uiw/react-codemirror": "^4.23.8",
"axios": "^1.7.9", "axios": "^1.7.9",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"global": "^4.4.0",
"html-to-image": "^1.11.13", "html-to-image": "^1.11.13",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
@ -31,10 +30,9 @@
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-error-boundary": "^5.0.0", "react-error-boundary": "^5.0.0",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"react-icons": "^5.5.0", "react-icons": "^5.4.0",
"react-intl": "^7.1.6", "react-intl": "^7.1.6",
"react-router": "^7.2.0", "react-router": "^7.2.0",
"react-scan": "^0.1.3",
"react-select": "^5.10.0", "react-select": "^5.10.0",
"react-tabs": "^6.1.0", "react-tabs": "^6.1.0",
"react-toastify": "^11.0.3", "react-toastify": "^11.0.3",
@ -48,7 +46,6 @@
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.7.2", "@lezer/generator": "^1.7.2",
"@playwright/test": "^1.50.1", "@playwright/test": "^1.50.1",
"@tailwindcss/vite": "^4.0.7",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.13.4", "@types/node": "^22.13.4",
"@types/react": "^19.0.10", "@types/react": "^19.0.10",
@ -56,20 +53,22 @@
"@typescript-eslint/eslint-plugin": "^8.0.1", "@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1", "@typescript-eslint/parser": "^8.0.1",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"babel-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216", "autoprefixer": "^10.4.20",
"babel-plugin-react-compiler": "^19.0.0-beta-30d8a17-20250209",
"eslint": "^9.20.1", "eslint": "^9.20.1",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-react": "^7.37.4", "eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-compiler": "^19.0.0-beta-21e868a-20250216", "eslint-plugin-react-compiler": "^19.0.0-beta-30d8a17-20250209",
"eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^16.0.0", "globals": "^15.15.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"tailwindcss": "^4.0.7", "postcss": "^8.5.2",
"tailwindcss": "^3.4.17",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.24.1", "typescript-eslint": "^8.24.1",
"vite": "^6.1.1" "vite": "^6.1.0"
}, },
"overrides": { "overrides": {
"react": "^19.0.0" "react": "^19.0.0"

View File

@ -0,0 +1,8 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
'tailwindcss': {},
'autoprefixer': {}
}
};

View File

@ -3,7 +3,7 @@ import { Outlet } from 'react-router';
import { ModalLoader } from '@/components/Modal'; import { ModalLoader } from '@/components/Modal';
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout'; import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout';
import { useDialogsStore } from '@/stores/dialogs'; import { globals } from '@/utils/constants';
import { NavigationState } from './Navigation/NavigationContext'; import { NavigationState } from './Navigation/NavigationContext';
import { Footer } from './Footer'; import { Footer } from './Footer';
@ -14,14 +14,13 @@ import { GlobalTooltips } from './GlobalTooltips';
import { MutationErrors } from './MutationErrors'; import { MutationErrors } from './MutationErrors';
import { Navigation } from './Navigation'; import { Navigation } from './Navigation';
export function ApplicationLayout() { function ApplicationLayout() {
const mainHeight = useMainHeight(); const mainHeight = useMainHeight();
const viewportHeight = useViewportHeight(); const viewportHeight = useViewportHeight();
const showScroll = useAppLayoutStore(state => !state.noScroll); const showScroll = useAppLayoutStore(state => !state.noScroll);
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation); const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
const noNavigation = useAppLayoutStore(state => state.noNavigation); const noNavigation = useAppLayoutStore(state => state.noNavigation);
const noFooter = useAppLayoutStore(state => state.noFooter); const noFooter = useAppLayoutStore(state => state.noFooter);
const activeDialog = useDialogsStore(state => state.active);
return ( return (
<NavigationState> <NavigationState>
@ -42,9 +41,11 @@ export function ApplicationLayout() {
<Navigation /> <Navigation />
<div <div
id={globals.main_scroll}
className='overflow-x-auto max-w-[100vw]' className='overflow-x-auto max-w-[100vw]'
style={{ maxHeight: viewportHeight }} style={{
inert={activeDialog !== null} maxHeight: viewportHeight
}}
> >
<main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}> <main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}>
<GlobalLoader /> <GlobalLoader />
@ -57,3 +58,5 @@ export function ApplicationLayout() {
</NavigationState> </NavigationState>
); );
} }
export default ApplicationLayout;

View File

@ -8,7 +8,7 @@ export function ErrorFallback() {
const router = useNavigate(); const router = useNavigate();
function resetErrorBoundary() { function resetErrorBoundary() {
Promise.resolve(router('/')).catch(console.error); Promise.resolve(router('/')).catch(console.log);
} }
return ( return (
<div className='flex flex-col gap-3 my-3 items-center antialiased' role='alert'> <div className='flex flex-col gap-3 my-3 items-center antialiased' role='alert'>

View File

@ -4,120 +4,34 @@ import React from 'react';
import { DialogType, useDialogsStore } from '@/stores/dialogs'; import { DialogType, useDialogsStore } from '@/stores/dialogs';
const DlgChangeInputSchema = React.lazy(() => const DlgChangeInputSchema = React.lazy(() => import('@/features/oss/dialogs/DlgChangeInputSchema'));
import('@/features/oss/dialogs/DlgChangeInputSchema').then(module => ({ default: module.DlgChangeInputSchema })) const DlgChangeLocation = React.lazy(() => import('@/features/library/dialogs/DlgChangeLocation'));
); const DlgCloneLibraryItem = React.lazy(() => import('@/features/library/dialogs/DlgCloneLibraryItem'));
const DlgChangeLocation = React.lazy(() => const DlgCreateCst = React.lazy(() => import('@/features/rsform/dialogs/DlgCreateCst'));
import('@/features/library/dialogs/DlgChangeLocation').then(module => ({ const DlgCreateOperation = React.lazy(() => import('@/features/oss/dialogs/DlgCreateOperation'));
default: module.DlgChangeLocation const DlgCreateVersion = React.lazy(() => import('@/features/library/dialogs/DlgCreateVersion'));
})) const DlgCstTemplate = React.lazy(() => import('@/features/rsform/dialogs/DlgCstTemplate'));
); const DlgDeleteCst = React.lazy(() => import('@/features/rsform/dialogs/DlgDeleteCst'));
const DlgCloneLibraryItem = React.lazy(() => const DlgDeleteOperation = React.lazy(() => import('@/features/oss/dialogs/DlgDeleteOperation'));
import('@/features/library/dialogs/DlgCloneLibraryItem').then(module => ({ const DlgEditEditors = React.lazy(() => import('@/features/library/dialogs/DlgEditEditors'));
default: module.DlgCloneLibraryItem const DlgEditOperation = React.lazy(() => import('@/features/oss/dialogs/DlgEditOperation'));
})) const DlgEditReference = React.lazy(() => import('@/features/rsform/dialogs/DlgEditReference'));
); const DlgEditVersions = React.lazy(() => import('@/features/library/dialogs/DlgEditVersions'));
const DlgCreateCst = React.lazy(() => const DlgEditWordForms = React.lazy(() => import('@/features/rsform/dialogs/DlgEditWordForms'));
import('@/features/rsform/dialogs/DlgCreateCst').then(module => ({ default: module.DlgCreateCst })) const DlgGraphParams = React.lazy(() => import('@/features/rsform/dialogs/DlgGraphParams'));
); const DlgInlineSynthesis = React.lazy(() => import('@/features/rsform/dialogs/DlgInlineSynthesis'));
const DlgCreateOperation = React.lazy(() => const DlgRelocateConstituents = React.lazy(() => import('@/features/oss/dialogs/DlgRelocateConstituents'));
import('@/features/oss/dialogs/DlgCreateOperation').then(module => ({ const DlgRenameCst = React.lazy(() => import('@/features/rsform/dialogs/DlgRenameCst'));
default: module.DlgCreateOperation const DlgShowAST = React.lazy(() => import('@/features/rsform/dialogs/DlgShowAST'));
})) const DlgShowQR = React.lazy(() => import('@/features/rsform/dialogs/DlgShowQR'));
); const DlgShowTypeGraph = React.lazy(() => import('@/features/rsform/dialogs/DlgShowTypeGraph'));
const DlgCreateVersion = React.lazy(() => const DlgSubstituteCst = React.lazy(() => import('@/features/rsform/dialogs/DlgSubstituteCst'));
import('@/features/library/dialogs/DlgCreateVersion').then(module => ({ const DlgUploadRSForm = React.lazy(() => import('@/features/rsform/dialogs/DlgUploadRSForm'));
default: module.DlgCreateVersion
}))
);
const DlgCstTemplate = React.lazy(() =>
import('@/features/rsform/dialogs/DlgCstTemplate').then(module => ({
default: module.DlgCstTemplate
}))
);
const DlgDeleteCst = React.lazy(() =>
import('@/features/rsform/dialogs/DlgDeleteCst').then(module => ({
default: module.DlgDeleteCst
}))
);
const DlgDeleteOperation = React.lazy(() =>
import('@/features/oss/dialogs/DlgDeleteOperation').then(module => ({
default: module.DlgDeleteOperation
}))
);
const DlgEditEditors = React.lazy(() =>
import('@/features/library/dialogs/DlgEditEditors').then(module => ({
default: module.DlgEditEditors
}))
);
const DlgEditOperation = React.lazy(() =>
import('@/features/oss/dialogs/DlgEditOperation').then(module => ({
default: module.DlgEditOperation
}))
);
const DlgEditReference = React.lazy(() =>
import('@/features/rsform/dialogs/DlgEditReference').then(module => ({
default: module.DlgEditReference
}))
);
const DlgEditVersions = React.lazy(() =>
import('@/features/library/dialogs/DlgEditVersions').then(module => ({
default: module.DlgEditVersions
}))
);
const DlgEditWordForms = React.lazy(() =>
import('@/features/rsform/dialogs/DlgEditWordForms').then(module => ({
default: module.DlgEditWordForms
}))
);
const DlgInlineSynthesis = React.lazy(() =>
import('@/features/rsform/dialogs/DlgInlineSynthesis').then(module => ({
default: module.DlgInlineSynthesis
}))
);
const DlgRelocateConstituents = React.lazy(() =>
import('@/features/oss/dialogs/DlgRelocateConstituents').then(module => ({
default: module.DlgRelocateConstituents
}))
);
const DlgRenameCst = React.lazy(() =>
import('@/features/rsform/dialogs/DlgRenameCst').then(module => ({
default: module.DlgRenameCst
}))
);
const DlgShowAST = React.lazy(() =>
import('@/features/rsform/dialogs/DlgShowAST').then(module => ({
default: module.DlgShowAST
}))
);
const DlgShowQR = React.lazy(() =>
import('@/features/rsform/dialogs/DlgShowQR').then(module => ({
default: module.DlgShowQR
}))
);
const DlgShowTypeGraph = React.lazy(() =>
import('@/features/rsform/dialogs/DlgShowTypeGraph').then(module => ({
default: module.DlgShowTypeGraph
}))
);
const DlgSubstituteCst = React.lazy(() =>
import('@/features/rsform/dialogs/DlgSubstituteCst').then(module => ({
default: module.DlgSubstituteCst
}))
);
const DlgUploadRSForm = React.lazy(() =>
import('@/features/rsform/dialogs/DlgUploadRSForm').then(module => ({
default: module.DlgUploadRSForm
}))
);
const DlgGraphParams = React.lazy(() =>
import('@/features/rsform/dialogs/DlgGraphParams').then(module => ({ default: module.DlgGraphParams }))
);
export const GlobalDialogs = () => { export const GlobalDialogs = () => {
const active = useDialogsStore(state => state.active); const active = useDialogsStore(state => state.active);
if (active === null) { if (active === undefined) {
return null; return null;
} }
switch (active) { switch (active) {

View File

@ -7,7 +7,7 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from '@/backend/queryClient'; import { queryClient } from '@/backend/queryClient';
// prettier-ignore // prettier-ignore
export function GlobalProviders({ children }: React.PropsWithChildren) { function GlobalProviders({ children }: React.PropsWithChildren) {
return ( return (
<IntlProvider locale='ru' defaultLocale='ru'> <IntlProvider locale='ru' defaultLocale='ru'>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
@ -18,3 +18,5 @@ export function GlobalProviders({ children }: React.PropsWithChildren) {
</QueryClientProvider> </QueryClientProvider>
</IntlProvider>); </IntlProvider>);
} }
export default GlobalProviders;

View File

@ -1,59 +1,32 @@
'use client'; 'use client';
import React, { Suspense } from 'react'; import InfoConstituenta from '@/features/rsform/components/InfoConstituenta';
import { Tooltip } from '@/components/Container'; import { Tooltip } from '@/components/Container';
import { Loader } from '@/components/Loader'; import { Loader } from '@/components/Loader';
import { useTooltipsStore } from '@/stores/tooltips'; import { useTooltipsStore } from '@/stores/tooltips';
import { globalIDs } from '@/utils/constants'; import { globals } from '@/utils/constants';
const InfoConstituenta = React.lazy(() =>
import('@/features/rsform/components/InfoConstituenta').then(module => ({ default: module.InfoConstituenta }))
);
const InfoOperation = React.lazy(() =>
import('@/features/oss/components/InfoOperation').then(module => ({ default: module.InfoOperation }))
);
export const GlobalTooltips = () => { export const GlobalTooltips = () => {
const hoverCst = useTooltipsStore(state => state.activeCst); const hoverCst = useTooltipsStore(state => state.activeCst);
const hoverOperation = useTooltipsStore(state => state.activeOperation);
return ( return (
<> <>
<Tooltip <Tooltip
float float
id={globalIDs.tooltip} id={globals.tooltip}
layer='z-topmost' layer='z-topmost'
place='right-start' place='right-start'
className='mt-8 max-w-[20rem] break-words' className='mt-8 max-w-[20rem] break-words'
/> />
<Tooltip <Tooltip
float float
id={globalIDs.value_tooltip} id={globals.value_tooltip}
layer='z-topmost' layer='z-topmost'
className='max-w-[calc(min(40rem,100dvw-2rem))] text-justify' className='max-w-[calc(min(40rem,100dvw-2rem))] text-justify'
/> />
<Tooltip <Tooltip clickable id={globals.constituenta_tooltip} layer='z-modalTooltip' className='max-w-[30rem]'>
clickable {hoverCst ? <InfoConstituenta data={hoverCst} onClick={event => event.stopPropagation()} /> : <Loader />}
id={globalIDs.constituenta_tooltip}
layer='z-modalTooltip'
className='max-w-[30rem]'
hidden={!hoverCst}
>
<Suspense fallback={<Loader />}>
{hoverCst ? <InfoConstituenta data={hoverCst} onClick={event => event.stopPropagation()} /> : null}
</Suspense>
</Tooltip>
<Tooltip
id={globalIDs.operation_tooltip}
layer='z-modalTooltip'
className='max-w-[35rem] max-h-[40rem] dense'
hidden={!hoverOperation}
>
<Suspense fallback={<Loader />}>
{hoverOperation ? <InfoOperation operation={hoverOperation} /> : null}
</Suspense>
</Tooltip> </Tooltip>
</> </>
); );

View File

@ -3,7 +3,7 @@ import clsx from 'clsx';
import { useMutationErrors } from '@/backend/useMutationErrors'; import { useMutationErrors } from '@/backend/useMutationErrors';
import { Button } from '@/components/Control'; import { Button } from '@/components/Control';
import { DescribeError } from '@/components/InfoError'; import { DescribeError } from '@/components/InfoError';
import { useEscapeKey } from '@/hooks/useEscapeKey'; import useEscapeKey from '@/hooks/useEscapeKey';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
export function MutationErrors() { export function MutationErrors() {

View File

@ -1,7 +1,7 @@
import { useWindowSize } from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
export function Logo() { function Logo() {
const darkMode = usePreferencesStore(state => state.darkMode); const darkMode = usePreferencesStore(state => state.darkMode);
const size = useWindowSize(); const size = useWindowSize();
@ -13,3 +13,4 @@ export function Logo() {
/> />
); );
} }
export default Logo;

View File

@ -1,28 +1,28 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons'; import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons';
import { useWindowSize } from '@/hooks/useWindowSize'; import { CProps } from '@/components/props';
import useWindowSize from '@/hooks/useWindowSize';
import { useAppLayoutStore } from '@/stores/appLayout'; import { useAppLayoutStore } from '@/stores/appLayout';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { urls } from '../urls'; import { urls } from '../urls';
import { Logo } from './Logo'; import Logo from './Logo';
import { NavigationButton } from './NavigationButton'; import NavigationButton from './NavigationButton';
import { useConceptNavigation } from './NavigationContext'; import { useConceptNavigation } from './NavigationContext';
import { ToggleNavigation } from './ToggleNavigation'; import ToggleNavigation from './ToggleNavigation';
import { UserMenu } from './UserMenu'; import UserMenu from './UserMenu';
export function Navigation() { export function Navigation() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const size = useWindowSize(); const size = useWindowSize();
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation); const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
const navigateHome = (event: React.MouseEvent<Element>) => router.push(urls.home, event.ctrlKey || event.metaKey); const navigateHome = (event: CProps.EventMouse) => router.push(urls.home, event.ctrlKey || event.metaKey);
const navigateLibrary = (event: React.MouseEvent<Element>) => const navigateLibrary = (event: CProps.EventMouse) => router.push(urls.library, event.ctrlKey || event.metaKey);
router.push(urls.library, event.ctrlKey || event.metaKey); const navigateHelp = (event: CProps.EventMouse) => router.push(urls.manuals, event.ctrlKey || event.metaKey);
const navigateHelp = (event: React.MouseEvent<Element>) => router.push(urls.manuals, event.ctrlKey || event.metaKey); const navigateCreateNew = (event: CProps.EventMouse) =>
const navigateCreateNew = (event: React.MouseEvent<Element>) =>
router.push(urls.create_schema, event.ctrlKey || event.metaKey); router.push(urls.create_schema, event.ctrlKey || event.metaKey);
return ( return (

View File

@ -1,15 +1,15 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { type Styling, type Titled } from '@/components/props'; import { CProps } from '@/components/props';
import { globalIDs } from '@/utils/constants'; import { globals } from '@/utils/constants';
interface NavigationButtonProps extends Titled, Styling { interface NavigationButtonProps extends CProps.Titled, CProps.Styling {
text?: string; text?: string;
icon: React.ReactNode; icon: React.ReactNode;
onClick?: (event: React.MouseEvent<Element>) => void; onClick?: (event: CProps.EventMouse) => void;
} }
export function NavigationButton({ function NavigationButton({
icon, icon,
title, title,
className, className,
@ -23,15 +23,14 @@ export function NavigationButton({
<button <button
type='button' type='button'
tabIndex={-1} tabIndex={-1}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined} data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
data-tooltip-html={titleHtml} data-tooltip-html={titleHtml}
data-tooltip-content={title} data-tooltip-content={title}
data-tooltip-hidden={hideTitle} data-tooltip-hidden={hideTitle}
onClick={onClick} onClick={onClick}
className={clsx( className={clsx(
'mr-1 h-full', 'mr-1 h-full', //
'flex items-center gap-1', 'flex items-center gap-1',
'cursor-pointer',
'clr-btn-nav cc-animate-color duration-500', 'clr-btn-nav cc-animate-color duration-500',
'rounded-xl', 'rounded-xl',
'font-controls whitespace-nowrap', 'font-controls whitespace-nowrap',
@ -48,3 +47,5 @@ export function NavigationButton({
</button> </button>
); );
} }
export default NavigationButton;

View File

@ -1,8 +1,9 @@
'use client'; 'use client';
import { createContext, useContext, useEffect, useState } from 'react'; import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { useNavigate } from 'react-router'; import { useLocation, useNavigate } from 'react-router';
import { globals } from '@/utils/constants';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
interface INavigationContext { interface INavigationContext {
@ -28,48 +29,68 @@ export const useConceptNavigation = () => {
export const NavigationState = ({ children }: React.PropsWithChildren) => { export const NavigationState = ({ children }: React.PropsWithChildren) => {
const router = useNavigate(); const router = useNavigate();
const { pathname } = useLocation();
const [isBlocked, setIsBlocked] = useState(false); const [isBlocked, setIsBlocked] = useState(false);
const validate = useCallback(() => {
function validate() {
return !isBlocked || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?'); return !isBlocked || confirm('Изменения не сохранены. Вы уверены что хотите совершить переход?');
} }, [isBlocked]);
function canBack() { const canBack = useCallback(() => !!window.history && window.history?.length !== 0, []);
return !!window.history && window.history?.length !== 0;
}
function push(path: string, newTab?: boolean) { const scrollTop = useCallback(() => {
window.scrollTo(0, 0);
const mainScroll = document.getElementById(globals.main_scroll);
if (mainScroll) {
mainScroll.scroll(0, 0);
}
}, []);
const push = useCallback(
(path: string, newTab?: boolean) => {
if (newTab) { if (newTab) {
window.open(`${path}`, '_blank'); window.open(`${path}`, '_blank');
return; return;
} }
if (validate()) { if (validate()) {
Promise.resolve(router(path, { viewTransition: true })).catch(console.error); scrollTop();
Promise.resolve(router(path, { viewTransition: true })).catch(console.log);
setIsBlocked(false); setIsBlocked(false);
} }
} },
[router, validate, scrollTop]
);
function replace(path: string) { const replace = useCallback(
(path: string) => {
if (validate()) { if (validate()) {
Promise.resolve(router(path, { replace: true, viewTransition: true })).catch(console.error); scrollTop();
Promise.resolve(router(path, { replace: true, viewTransition: true })).catch(console.log);
setIsBlocked(false); setIsBlocked(false);
} }
} },
[router, validate, scrollTop]
);
function back() { const back = useCallback(() => {
if (validate()) { if (validate()) {
Promise.resolve(router(-1)).catch(console.error); scrollTop();
Promise.resolve(router(-1)).catch(console.log);
setIsBlocked(false); setIsBlocked(false);
} }
} }, [router, validate, scrollTop]);
function forward() { const forward = useCallback(() => {
if (validate()) { if (validate()) {
Promise.resolve(router(1)).catch(console.error); scrollTop();
Promise.resolve(router(1)).catch(console.log);
setIsBlocked(false); setIsBlocked(false);
} }
} }, [router, validate, scrollTop]);
useEffect(() => {
scrollTop();
}, [pathname, scrollTop]);
return ( return (
<NavigationContext <NavigationContext

View File

@ -3,9 +3,9 @@ import clsx from 'clsx';
import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons'; import { IconDarkTheme, IconLightTheme, IconPin, IconUnpin } from '@/components/Icons';
import { useAppLayoutStore } from '@/stores/appLayout'; import { useAppLayoutStore } from '@/stores/appLayout';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { globalIDs, PARAMETER } from '@/utils/constants'; import { globals, PARAMETER } from '@/utils/constants';
export function ToggleNavigation() { function ToggleNavigation() {
const darkMode = usePreferencesStore(state => state.darkMode); const darkMode = usePreferencesStore(state => state.darkMode);
const toggleDarkMode = usePreferencesStore(state => state.toggleDarkMode); const toggleDarkMode = usePreferencesStore(state => state.toggleDarkMode);
const noNavigation = useAppLayoutStore(state => state.noNavigation); const noNavigation = useAppLayoutStore(state => state.noNavigation);
@ -34,7 +34,7 @@ export function ToggleNavigation() {
type='button' type='button'
className='p-1' className='p-1'
onClick={toggleDarkMode} onClick={toggleDarkMode}
data-tooltip-id={globalIDs.tooltip} data-tooltip-id={globals.tooltip}
data-tooltip-content={darkMode ? 'Тема: Темная' : 'Тема: Светлая'} data-tooltip-content={darkMode ? 'Тема: Темная' : 'Тема: Светлая'}
> >
{darkMode ? <IconDarkTheme size='0.75rem' /> : null} {darkMode ? <IconDarkTheme size='0.75rem' /> : null}
@ -46,7 +46,7 @@ export function ToggleNavigation() {
type='button' type='button'
className='p-1' className='p-1'
onClick={toggleNoNavigation} onClick={toggleNoNavigation}
data-tooltip-id={globalIDs.tooltip} data-tooltip-id={globals.tooltip}
data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'} data-tooltip-content={noNavigationAnimation ? 'Показать навигацию' : 'Скрыть навигацию'}
> >
{!noNavigationAnimation ? <IconPin size={iconSize} /> : null} {!noNavigationAnimation ? <IconPin size={iconSize} /> : null}
@ -55,3 +55,5 @@ export function ToggleNavigation() {
</div> </div>
); );
} }
export default ToggleNavigation;

View File

@ -3,14 +3,14 @@ import { useAuthSuspense } from '@/features/auth';
import { IconLogin, IconUser2 } from '@/components/Icons'; import { IconLogin, IconUser2 } from '@/components/Icons';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { NavigationButton } from './NavigationButton'; import NavigationButton from './NavigationButton';
interface UserButtonProps { interface UserButtonProps {
onLogin: () => void; onLogin: () => void;
onClickUser: () => void; onClickUser: () => void;
} }
export function UserButton({ onLogin, onClickUser }: UserButtonProps) { function UserButton({ onLogin, onClickUser }: UserButtonProps) {
const { user, isAnonymous } = useAuthSuspense(); const { user, isAnonymous } = useAuthSuspense();
const adminMode = usePreferencesStore(state => state.adminMode); const adminMode = usePreferencesStore(state => state.adminMode);
if (isAnonymous) { if (isAnonymous) {
@ -32,3 +32,5 @@ export function UserButton({ onLogin, onClickUser }: UserButtonProps) {
); );
} }
} }
export default UserButton;

View File

@ -15,6 +15,7 @@ import {
IconRESTapi, IconRESTapi,
IconUser IconUser
} from '@/components/Icons'; } from '@/components/Icons';
import { CProps } from '@/components/props';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { urls } from '../urls'; import { urls } from '../urls';
@ -26,7 +27,7 @@ interface UserDropdownProps {
hideDropdown: () => void; hideDropdown: () => void;
} }
export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) { function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user } = useAuthSuspense(); const { user } = useAuthSuspense();
const { logout } = useLogout(); const { logout } = useLogout();
@ -38,7 +39,7 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
const adminMode = usePreferencesStore(state => state.adminMode); const adminMode = usePreferencesStore(state => state.adminMode);
const toggleAdminMode = usePreferencesStore(state => state.toggleAdminMode); const toggleAdminMode = usePreferencesStore(state => state.toggleAdminMode);
function navigateProfile(event: React.MouseEvent<Element>) { function navigateProfile(event: CProps.EventMouse) {
hideDropdown(); hideDropdown();
router.push(urls.profile, event.ctrlKey || event.metaKey); router.push(urls.profile, event.ctrlKey || event.metaKey);
} }
@ -53,7 +54,7 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
void logout().then(() => router.push(urls.admin, true)); void logout().then(() => router.push(urls.admin, true));
} }
function gotoIcons(event: React.MouseEvent<Element>) { function gotoIcons(event: CProps.EventMouse) {
hideDropdown(); hideDropdown();
router.push(urls.icons, event.ctrlKey || event.metaKey); router.push(urls.icons, event.ctrlKey || event.metaKey);
} }
@ -63,7 +64,7 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
router.push(urls.rest_api, true); router.push(urls.rest_api, true);
} }
function gotoDatabaseSchema(event: React.MouseEvent<Element>) { function gotoDatabaseSchema(event: CProps.EventMouse) {
hideDropdown(); hideDropdown();
router.push(urls.database_schema, event.ctrlKey || event.metaKey); router.push(urls.database_schema, event.ctrlKey || event.metaKey);
} }
@ -140,3 +141,5 @@ export function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
</Dropdown> </Dropdown>
); );
} }
export default UserDropdown;

View File

@ -6,10 +6,10 @@ import { Loader } from '@/components/Loader';
import { urls } from '../urls'; import { urls } from '../urls';
import { useConceptNavigation } from './NavigationContext'; import { useConceptNavigation } from './NavigationContext';
import { UserButton } from './UserButton'; import UserButton from './UserButton';
import { UserDropdown } from './UserDropdown'; import UserDropdown from './UserDropdown';
export function UserMenu() { function UserMenu() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const menu = useDropdown(); const menu = useDropdown();
return ( return (
@ -21,3 +21,5 @@ export function UserMenu() {
</div> </div>
); );
} }
export default UserMenu;

View File

@ -1,11 +1,11 @@
import { createBrowserRouter } from 'react-router'; import { createBrowserRouter } from 'react-router';
import { prefetchAuth } from '@/features/auth/backend/useAuth'; import { prefetchAuth } from '@/features/auth/backend/useAuth';
import { LoginPage } from '@/features/auth/pages/LoginPage'; import LoginPage from '@/features/auth/pages/LoginPage';
import { HomePage } from '@/features/home/HomePage'; import HomePage from '@/features/home/HomePage';
import { NotFoundPage } from '@/features/home/NotFoundPage'; import NotFoundPage from '@/features/home/NotFoundPage';
import { prefetchLibrary } from '@/features/library/backend/useLibrary'; import { prefetchLibrary } from '@/features/library/backend/useLibrary';
import { CreateItemPage } from '@/features/library/pages/CreateItemPage'; import CreateItemPage from '@/features/library/pages/CreateItemPage';
import { prefetchOSS } from '@/features/oss/backend/useOSS'; import { prefetchOSS } from '@/features/oss/backend/useOSS';
import { prefetchRSForm } from '@/features/rsform/backend/useRSForm'; import { prefetchRSForm } from '@/features/rsform/backend/useRSForm';
import { prefetchProfile } from '@/features/users/backend/useProfile'; import { prefetchProfile } from '@/features/users/backend/useProfile';
@ -13,7 +13,7 @@ import { prefetchUsers } from '@/features/users/backend/useUsers';
import { Loader } from '@/components/Loader'; import { Loader } from '@/components/Loader';
import { ApplicationLayout } from './ApplicationLayout'; import ApplicationLayout from './ApplicationLayout';
import { ErrorFallback } from './ErrorFallback'; import { ErrorFallback } from './ErrorFallback';
import { routes } from './urls'; import { routes } from './urls';

View File

@ -5,6 +5,8 @@ import { RouterProvider } from 'react-router';
import { Router } from './Router'; import { Router } from './Router';
export function App() { function App() {
return <RouterProvider router={Router} />; return <RouterProvider router={Router} />;
} }
export default App;

View File

@ -2,8 +2,8 @@
* Module: generic API for backend REST communications using axios library. * Module: generic API for backend REST communications using axios library.
*/ */
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import axios, { type AxiosError, type AxiosRequestConfig } from 'axios'; import axios, { AxiosError, AxiosRequestConfig } from 'axios';
import { type z, ZodError } from 'zod'; import { z, ZodError } from 'zod';
import { buildConstants } from '@/utils/buildConstants'; import { buildConstants } from '@/utils/buildConstants';
import { errorMsg } from '@/utils/labels'; import { errorMsg } from '@/utils/labels';
@ -32,6 +32,8 @@ axiosInstance.interceptors.request.use(config => {
}); });
// ================ Data transfer types ================ // ================ Data transfer types ================
export type DataCallback<ResponseData = undefined> = (data: ResponseData) => void;
export interface IFrontRequest<RequestData, ResponseData> { export interface IFrontRequest<RequestData, ResponseData> {
data?: RequestData; data?: RequestData;
successMessage?: string | ((data: ResponseData) => string); successMessage?: string | ((data: ResponseData) => string);

View File

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

View File

@ -1,15 +1,18 @@
import { useState } from 'react'; import { useState } from 'react';
import { useMutationState } from '@tanstack/react-query'; import { useMutationState, useQueryClient } from '@tanstack/react-query';
import { KEYS } from './configuration'; import { KEYS } from './configuration';
export const useMutationErrors = () => { export const useMutationErrors = () => {
const [ignored, setIgnored] = useState<Error[]>([]); const queryClient = useQueryClient();
const [ignored, setIgnored] = useState<(Error | null)[]>([]);
const mutationErrors = useMutationState({ const mutationErrors = useMutationState({
filters: { mutationKey: [KEYS.global_mutation], status: 'error' }, filters: { mutationKey: [KEYS.global_mutation], status: 'error' },
select: mutation => mutation.state.error! select: mutation => mutation.state.error
}); });
console.log(queryClient.getMutationCache().getAll());
function resetErrors() { function resetErrors() {
setIgnored(mutationErrors); setIgnored(mutationErrors);
} }

View File

@ -1,8 +1,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { type Styling } from '../props'; import { CProps } from '../props';
interface DividerProps extends Styling { interface DividerProps extends CProps.Styling {
/** Indicates whether the divider is vertical. */ /** Indicates whether the divider is vertical. */
vertical?: boolean; vertical?: boolean;

View File

@ -1,10 +1,12 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { CProps } from '../props';
/** /**
* `flex` column container. * `flex` column container.
* This component is useful for creating vertical layouts with flexbox. * This component is useful for creating vertical layouts with flexbox.
*/ */
export function FlexColumn({ className, children, ...restProps }: React.ComponentProps<'div'>) { export function FlexColumn({ className, children, ...restProps }: CProps.Div) {
return ( return (
<div className={clsx('cc-column', className)} {...restProps}> <div className={clsx('cc-column', className)} {...restProps}>
{children} {children}

View File

@ -1,8 +1,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { type Styling } from '../props'; import { CProps } from '../props';
interface OverlayProps extends Styling { interface OverlayProps extends CProps.Styling {
/** Id of the overlay. */ /** Id of the overlay. */
id?: string; id?: string;

View File

@ -1,8 +1,8 @@
'use client'; 'use client';
import { type ReactNode } from 'react'; import { ReactNode } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { type ITooltip, Tooltip as TooltipImpl } from 'react-tooltip'; import { ITooltip, Tooltip as TooltipImpl } from 'react-tooltip';
import clsx from 'clsx'; import clsx from 'clsx';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';

View File

@ -1,10 +1,10 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { globalIDs } from '@/utils/constants'; import { globals } from '@/utils/constants';
import { type Button as ButtonStyle, type Control } from '../props'; import { CProps } from '../props';
interface ButtonProps extends Control, ButtonStyle { interface ButtonProps extends CProps.Control, CProps.Colors, CProps.Button {
/** Icon to display first. */ /** Icon to display first. */
icon?: React.ReactNode; icon?: React.ReactNode;
@ -32,6 +32,7 @@ export function Button({
disabled, disabled,
noBorder, noBorder,
noOutline, noOutline,
colors = 'clr-btn-default',
className, className,
...restProps ...restProps
}: ButtonProps) { }: ButtonProps) {
@ -42,19 +43,20 @@ export function Button({
className={clsx( className={clsx(
'inline-flex gap-2 items-center justify-center', 'inline-flex gap-2 items-center justify-center',
'select-none disabled:cursor-auto', 'select-none disabled:cursor-auto',
'clr-btn-default cc-animate-color', 'cc-animate-color',
{ {
'border rounded-sm': !noBorder, 'border rounded': !noBorder,
'px-1': dense, 'px-1': dense,
'px-3 py-1': !dense, 'px-3 py-1': !dense,
'cursor-progress': loading, 'cursor-progress': loading,
'cursor-pointer': !loading, 'cursor-pointer': !loading,
'outline-hidden': noOutline, 'outline-none': noOutline,
'clr-outline': !noOutline 'clr-outline': !noOutline
}, },
className className,
colors
)} )}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined} data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
data-tooltip-html={titleHtml} data-tooltip-html={titleHtml}
data-tooltip-content={title} data-tooltip-content={title}
data-tooltip-hidden={hideTitle} data-tooltip-hidden={hideTitle}

View File

@ -1,10 +1,10 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { globalIDs } from '@/utils/constants'; import { globals } from '@/utils/constants';
import { type Button } from '../props'; import { CProps } from '../props';
interface MiniButtonProps extends Button { interface MiniButtonProps extends CProps.Button {
/** Button type. */ /** Button type. */
type?: 'button' | 'submit'; type?: 'button' | 'submit';
@ -43,12 +43,12 @@ export function MiniButton({
'cursor-pointer disabled:cursor-auto', 'cursor-pointer disabled:cursor-auto',
{ {
'px-1 py-1': !noPadding, 'px-1 py-1': !noPadding,
'outline-hidden': noHover, 'outline-none': noHover,
'clr-hover': !noHover 'clr-hover': !noHover
}, },
className className
)} )}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined} data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
data-tooltip-html={titleHtml} data-tooltip-html={titleHtml}
data-tooltip-content={title} data-tooltip-content={title}
data-tooltip-hidden={hideTitle} data-tooltip-hidden={hideTitle}

View File

@ -1,16 +1,19 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { globalIDs } from '@/utils/constants'; import { globals } from '@/utils/constants';
import { type Button } from '../props'; import { CProps } from '../props';
interface SelectorButtonProps extends Button { interface SelectorButtonProps extends CProps.Button {
/** Text to display in the button. */ /** Text to display in the button. */
text?: string; text?: string;
/** Icon to display in the button. */ /** Icon to display in the button. */
icon?: React.ReactNode; icon?: React.ReactNode;
/** Classnames for the colors of the button. */
colors?: string;
/** Indicates if button background should be transparent. */ /** Indicates if button background should be transparent. */
transparent?: boolean; transparent?: boolean;
} }
@ -23,6 +26,7 @@ export function SelectorButton({
icon, icon,
title, title,
titleHtml, titleHtml,
colors = 'clr-btn-default',
className, className,
transparent, transparent,
hideTitle, hideTitle,
@ -40,11 +44,12 @@ export function SelectorButton({
'cc-animate-color', 'cc-animate-color',
{ {
'clr-hover': transparent, 'clr-hover': transparent,
'clr-btn-default border': !transparent 'border': !transparent
}, },
className className,
!transparent && colors
)} )}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined} data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
data-tooltip-html={titleHtml} data-tooltip-html={titleHtml}
data-tooltip-content={title} data-tooltip-content={title}
data-tooltip-hidden={hideTitle} data-tooltip-hidden={hideTitle}

View File

@ -1,8 +1,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { type Button } from '../props'; import { CProps } from '../props';
interface SubmitButtonProps extends Button { interface SubmitButtonProps extends CProps.Button {
/** Text to display in the button. */ /** Text to display in the button. */
text?: string; text?: string;

View File

@ -3,27 +3,27 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { import {
type ColumnSort, ColumnSort,
createColumnHelper, createColumnHelper,
getCoreRowModel, getCoreRowModel,
getPaginationRowModel, getPaginationRowModel,
getSortedRowModel, getSortedRowModel,
type PaginationState, PaginationState,
type RowData, RowData,
type RowSelectionState, type RowSelectionState,
type SortingState, SortingState,
type TableOptions, TableOptions,
useReactTable, useReactTable,
type VisibilityState type VisibilityState
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { type Styling } from '../props'; import { CProps } from '../props';
import { DefaultNoData } from './DefaultNoData'; import DefaultNoData from './DefaultNoData';
import { PaginationTools } from './PaginationTools'; import PaginationTools from './PaginationTools';
import { TableBody } from './TableBody'; import TableBody from './TableBody';
import { TableFooter } from './TableFooter'; import TableFooter from './TableFooter';
import { TableHeader } from './TableHeader'; import TableHeader from './TableHeader';
export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState }; export { type ColumnSort, createColumnHelper, type RowSelectionState, type VisibilityState };
@ -37,7 +37,7 @@ export interface IConditionalStyle<TData> {
} }
export interface DataTableProps<TData extends RowData> export interface DataTableProps<TData extends RowData>
extends Styling, extends CProps.Styling,
Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> { Pick<TableOptions<TData>, 'data' | 'columns' | 'onRowSelectionChange' | 'onColumnVisibilityChange'> {
/** Id of the component. */ /** Id of the component. */
id?: string; id?: string;
@ -67,10 +67,10 @@ export interface DataTableProps<TData extends RowData>
noDataComponent?: React.ReactNode; noDataComponent?: React.ReactNode;
/** Callback to be called when a row is clicked. */ /** Callback to be called when a row is clicked. */
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void; onRowClicked?: (rowData: TData, event: CProps.EventMouse) => void;
/** Callback to be called when a row is double clicked. */ /** Callback to be called when a row is double clicked. */
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void; onRowDoubleClicked?: (rowData: TData, event: CProps.EventMouse) => void;
/** Enable row selection. */ /** Enable row selection. */
enableRowSelection?: boolean; enableRowSelection?: boolean;
@ -109,7 +109,7 @@ export interface DataTableProps<TData extends RowData>
* @param headPosition - Top position of sticky header (0 if no other sticky elements are present). * @param headPosition - Top position of sticky header (0 if no other sticky elements are present).
* No sticky header if omitted * No sticky header if omitted
*/ */
export function DataTable<TData extends RowData>({ function DataTable<TData extends RowData>({
id, id,
style, style,
className, className,
@ -141,7 +141,7 @@ export function DataTable<TData extends RowData>({
...restProps ...restProps
}: DataTableProps<TData>) { }: DataTableProps<TData>) {
const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []); const [sorting, setSorting] = useState<SortingState>(initialSorting ? [initialSorting] : []);
const [lastSelected, setLastSelected] = useState<string | null>(null); const [lastSelected, setLastSelected] = useState<string | undefined>(undefined);
const [pagination, setPagination] = useState<PaginationState>({ const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0, pageIndex: 0,
@ -198,7 +198,7 @@ export function DataTable<TData extends RowData>({
enableRowSelection={enableRowSelection} enableRowSelection={enableRowSelection}
enableSorting={enableSorting} enableSorting={enableSorting}
headPosition={headPosition} headPosition={headPosition}
resetLastSelected={() => setLastSelected(null)} resetLastSelected={() => setLastSelected(undefined)}
/> />
) : null} ) : null}
@ -229,3 +229,5 @@ export function DataTable<TData extends RowData>({
</div> </div>
); );
} }
export default DataTable;

View File

@ -1,3 +1,5 @@
export function DefaultNoData() { function defaultNoDataComponent() {
return <div className='p-2 text-center'>Данные отсутствуют</div>; return <div className='p-2 text-center'>Данные отсутствуют</div>;
} }
export default defaultNoDataComponent;

View File

@ -2,7 +2,7 @@
'use no memo'; 'use no memo';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { type Table } from '@tanstack/react-table'; import { Table } from '@tanstack/react-table';
import clsx from 'clsx'; import clsx from 'clsx';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
@ -16,7 +16,7 @@ interface PaginationToolsProps<TData> {
onChangePaginationOption?: (newValue: number) => void; onChangePaginationOption?: (newValue: number) => void;
} }
export function PaginationTools<TData>({ function PaginationTools<TData>({
id, id,
table, table,
paginationOptions, paginationOptions,
@ -106,3 +106,5 @@ export function PaginationTools<TData>({
</div> </div>
); );
} }
export default PaginationTools;

View File

@ -1,6 +1,6 @@
'use no memo'; 'use no memo';
import { type Table } from '@tanstack/react-table'; import { Table } from '@tanstack/react-table';
import { CheckboxTristate } from '../Input'; import { CheckboxTristate } from '../Input';
@ -9,7 +9,7 @@ interface SelectAllProps<TData> {
resetLastSelected: () => void; resetLastSelected: () => void;
} }
export function SelectAll<TData>({ table, resetLastSelected }: SelectAllProps<TData>) { function SelectAll<TData>({ table, resetLastSelected }: SelectAllProps<TData>) {
function handleChange(value: boolean | null) { function handleChange(value: boolean | null) {
resetLastSelected(); resetLastSelected();
table.toggleAllPageRowsSelected(value !== false); table.toggleAllPageRowsSelected(value !== false);
@ -26,3 +26,5 @@ export function SelectAll<TData>({ table, resetLastSelected }: SelectAllProps<TD
/> />
); );
} }
export default SelectAll;

View File

@ -1,15 +1,15 @@
'use no memo'; 'use no memo';
import { type Row } from '@tanstack/react-table'; import { Row } from '@tanstack/react-table';
import { Checkbox } from '../Input'; import { Checkbox } from '../Input';
interface SelectRowProps<TData> { interface SelectRowProps<TData> {
row: Row<TData>; row: Row<TData>;
onChangeLastSelected: (newValue: string) => void; onChangeLastSelected: (newValue: string | undefined) => void;
} }
export function SelectRow<TData>({ row, onChangeLastSelected }: SelectRowProps<TData>) { function SelectRow<TData>({ row, onChangeLastSelected }: SelectRowProps<TData>) {
function handleChange(value: boolean) { function handleChange(value: boolean) {
onChangeLastSelected(row.id); onChangeLastSelected(row.id);
row.toggleSelected(value); row.toggleSelected(value);
@ -17,3 +17,5 @@ export function SelectRow<TData>({ row, onChangeLastSelected }: SelectRowProps<T
return <Checkbox tabIndex={-1} value={row.getIsSelected()} onChange={handleChange} />; return <Checkbox tabIndex={-1} value={row.getIsSelected()} onChange={handleChange} />;
} }
export default SelectRow;

View File

@ -1,6 +1,6 @@
'use no memo'; 'use no memo';
import { type Column } from '@tanstack/react-table'; import { Column } from '@tanstack/react-table';
import { IconSortAsc, IconSortDesc } from '../Icons'; import { IconSortAsc, IconSortDesc } from '../Icons';
@ -8,7 +8,7 @@ interface SortingIconProps<TData> {
column: Column<TData>; column: Column<TData>;
} }
export function SortingIcon<TData>({ column }: SortingIconProps<TData>) { function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
return ( return (
<> <>
{{ {{
@ -18,3 +18,5 @@ export function SortingIcon<TData>({ column }: SortingIconProps<TData>) {
</> </>
); );
} }
export default SortingIcon;

View File

@ -1,10 +1,12 @@
'use no memo'; 'use no memo';
import { type Cell, flexRender, type Row, type Table } from '@tanstack/react-table'; import { Cell, flexRender, Row, Table } from '@tanstack/react-table';
import clsx from 'clsx'; import clsx from 'clsx';
import { SelectRow } from './SelectRow'; import { CProps } from '../props';
import { type IConditionalStyle } from '.';
import SelectRow from './SelectRow';
import { IConditionalStyle } from '.';
interface TableBodyProps<TData> { interface TableBodyProps<TData> {
table: Table<TData>; table: Table<TData>;
@ -13,14 +15,14 @@ interface TableBodyProps<TData> {
enableRowSelection?: boolean; enableRowSelection?: boolean;
conditionalRowStyles?: IConditionalStyle<TData>[]; conditionalRowStyles?: IConditionalStyle<TData>[];
lastSelected: string | null; lastSelected: string | undefined;
onChangeLastSelected: (newValue: string | null) => void; onChangeLastSelected: (newValue: string | undefined) => void;
onRowClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void; onRowClicked?: (rowData: TData, event: CProps.EventMouse) => void;
onRowDoubleClicked?: (rowData: TData, event: React.MouseEvent<Element>) => void; onRowDoubleClicked?: (rowData: TData, event: CProps.EventMouse) => void;
} }
export function TableBody<TData>({ function TableBody<TData>({
table, table,
dense, dense,
noHeader, noHeader,
@ -31,7 +33,7 @@ export function TableBody<TData>({
onRowClicked, onRowClicked,
onRowDoubleClicked onRowDoubleClicked
}: TableBodyProps<TData>) { }: TableBodyProps<TData>) {
function handleRowClicked(target: Row<TData>, event: React.MouseEvent<Element>) { function handleRowClicked(target: Row<TData>, event: CProps.EventMouse) {
onRowClicked?.(target.original, event); onRowClicked?.(target.original, event);
if (enableRowSelection && target.getCanSelect()) { if (enableRowSelection && target.getCanSelect()) {
if (event.shiftKey && !!lastSelected && lastSelected !== target.id) { if (event.shiftKey && !!lastSelected && lastSelected !== target.id) {
@ -47,7 +49,7 @@ export function TableBody<TData>({
newSelection[row.id] = !target.getIsSelected(); newSelection[row.id] = !target.getIsSelected();
}); });
table.setRowSelection(prev => ({ ...prev, ...newSelection })); table.setRowSelection(prev => ({ ...prev, ...newSelection }));
onChangeLastSelected(null); onChangeLastSelected(undefined);
} else { } else {
onChangeLastSelected(target.id); onChangeLastSelected(target.id);
target.toggleSelected(!target.getIsSelected()); target.toggleSelected(!target.getIsSelected());
@ -92,7 +94,7 @@ export function TableBody<TData>({
width: noHeader && index === 0 ? `calc(var(--col-${cell.column.id}-size) * 1px)` : 'auto' width: noHeader && index === 0 ? `calc(var(--col-${cell.column.id}-size) * 1px)` : 'auto'
}} }}
onClick={event => handleRowClicked(row, event)} onClick={event => handleRowClicked(row, event)}
onDoubleClick={event => onRowDoubleClicked?.(row.original, event)} onDoubleClick={event => (onRowDoubleClicked ? onRowDoubleClicked(row.original, event) : undefined)}
> >
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</td> </td>
@ -102,3 +104,5 @@ export function TableBody<TData>({
</tbody> </tbody>
); );
} }
export default TableBody;

View File

@ -1,12 +1,12 @@
'use no memo'; 'use no memo';
import { flexRender, type Header, type HeaderGroup, type Table } from '@tanstack/react-table'; import { flexRender, Header, HeaderGroup, Table } from '@tanstack/react-table';
interface TableFooterProps<TData> { interface TableFooterProps<TData> {
table: Table<TData>; table: Table<TData>;
} }
export function TableFooter<TData>({ table }: TableFooterProps<TData>) { function TableFooter<TData>({ table }: TableFooterProps<TData>) {
return ( return (
<tfoot> <tfoot>
{table.getFooterGroups().map((footerGroup: HeaderGroup<TData>) => ( {table.getFooterGroups().map((footerGroup: HeaderGroup<TData>) => (
@ -21,3 +21,5 @@ export function TableFooter<TData>({ table }: TableFooterProps<TData>) {
</tfoot> </tfoot>
); );
} }
export default TableFooter;

View File

@ -1,9 +1,9 @@
'use no memo'; 'use no memo';
import { flexRender, type Header, type HeaderGroup, type Table } from '@tanstack/react-table'; import { flexRender, Header, HeaderGroup, Table } from '@tanstack/react-table';
import { SelectAll } from './SelectAll'; import SelectAll from './SelectAll';
import { SortingIcon } from './SortingIcon'; import SortingIcon from './SortingIcon';
interface TableHeaderProps<TData> { interface TableHeaderProps<TData> {
table: Table<TData>; table: Table<TData>;
@ -13,7 +13,7 @@ interface TableHeaderProps<TData> {
resetLastSelected: () => void; resetLastSelected: () => void;
} }
export function TableHeader<TData>({ function TableHeader<TData>({
table, table,
headPosition, headPosition,
enableRowSelection, enableRowSelection,
@ -61,3 +61,5 @@ export function TableHeader<TData>({
</thead> </thead>
); );
} }
export default TableHeader;

View File

@ -1,6 +1,6 @@
export { export {
createColumnHelper, createColumnHelper,
DataTable, default,
type IConditionalStyle, type IConditionalStyle,
type RowSelectionState, type RowSelectionState,
type VisibilityState type VisibilityState

View File

@ -14,7 +14,7 @@ import {
IconHide, IconHide,
IconMoveDown, IconMoveDown,
IconMoveUp, IconMoveUp,
type IconProps, IconProps,
IconPublic, IconPublic,
IconSettings, IconSettings,
IconShow, IconShow,

View File

@ -2,9 +2,9 @@ import clsx from 'clsx';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { type Styling } from '../props'; import { CProps } from '../props';
interface DropdownProps extends Styling { interface DropdownProps extends CProps.Styling {
/** Indicates whether the dropdown should stretch to the left. */ /** Indicates whether the dropdown should stretch to the left. */
stretchLeft?: boolean; stretchLeft?: boolean;

View File

@ -1,9 +1,9 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { type Button } from '@/components/props'; import { CProps } from '@/components/props';
import { globalIDs } from '@/utils/constants'; import { globals } from '@/utils/constants';
interface DropdownButtonProps extends Button { interface DropdownButtonProps extends CProps.Button {
/** Icon to display first (not used if children are provided). */ /** Icon to display first (not used if children are provided). */
icon?: React.ReactNode; icon?: React.ReactNode;
@ -36,7 +36,7 @@ export function DropdownButton({
onClick={onClick} onClick={onClick}
className={clsx( className={clsx(
'px-3 py-1 inline-flex items-center gap-2', 'px-3 py-1 inline-flex items-center gap-2',
'text-left text-sm text-ellipsis whitespace-nowrap', 'text-left text-sm overflow-ellipsis whitespace-nowrap',
'disabled:clr-text-controls', 'disabled:clr-text-controls',
'cc-animate-color', 'cc-animate-color',
{ {
@ -46,7 +46,7 @@ export function DropdownButton({
}, },
className className
)} )}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined} data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
data-tooltip-html={titleHtml} data-tooltip-html={titleHtml}
data-tooltip-content={title} data-tooltip-content={title}
data-tooltip-hidden={hideTitle} data-tooltip-hidden={hideTitle}

View File

@ -1,6 +1,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { Checkbox, type CheckboxProps } from '../Input'; import { Checkbox, CheckboxProps } from '../Input';
/** Animated {@link Checkbox} inside a {@link Dropdown} item. */ /** Animated {@link Checkbox} inside a {@link Dropdown} item. */
export function DropdownCheckbox({ onChange: setValue, disabled, ...restProps }: CheckboxProps) { export function DropdownCheckbox({ onChange: setValue, disabled, ...restProps }: CheckboxProps) {
@ -8,7 +8,7 @@ export function DropdownCheckbox({ onChange: setValue, disabled, ...restProps }:
<div <div
className={clsx( className={clsx(
'px-3 py-1', 'px-3 py-1',
'text-left text-ellipsis whitespace-nowrap', 'text-left overflow-ellipsis whitespace-nowrap',
'disabled:clr-text-controls cc-animate-color', 'disabled:clr-text-controls cc-animate-color',
!!setValue && !disabled && 'clr-hover' !!setValue && !disabled && 'clr-hover'
)} )}

View File

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

View File

@ -1,10 +1,10 @@
import { type EdgeProps, getStraightPath } from 'reactflow'; import { EdgeProps, getStraightPath } from 'reactflow';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
const RADIUS = PARAMETER.graphNodeRadius + PARAMETER.graphNodePadding; const RADIUS = PARAMETER.graphNodeRadius + PARAMETER.graphNodePadding;
export function DynamicEdge({ id, markerEnd, style, ...props }: EdgeProps) { function DynamicEdge({ id, markerEnd, style, ...props }: EdgeProps) {
const sourceY = props.sourceY - PARAMETER.graphNodeRadius - PARAMETER.graphHandleSize; const sourceY = props.sourceY - PARAMETER.graphNodeRadius - PARAMETER.graphHandleSize;
const targetY = props.targetY + PARAMETER.graphNodeRadius + PARAMETER.graphHandleSize; const targetY = props.targetY + PARAMETER.graphNodeRadius + PARAMETER.graphHandleSize;
@ -32,3 +32,5 @@ export function DynamicEdge({ id, markerEnd, style, ...props }: EdgeProps) {
</> </>
); );
} }
export default DynamicEdge;

View File

@ -1,12 +1,12 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
import { type AxiosError, isAxiosError } from '@/backend/apiTransport'; import { AxiosError, isAxiosError } from '@/backend/apiTransport';
import { isResponseHtml } from '@/utils/utils'; import { isResponseHtml } from '@/utils/utils';
import { PrettyJson } from './View'; import { PrettyJson } from './View';
export type ErrorData = string | Error | AxiosError | ZodError; export type ErrorData = string | Error | AxiosError | ZodError | undefined | null;
interface InfoErrorProps { interface InfoErrorProps {
error: ErrorData; error: ErrorData;

View File

@ -1,11 +1,11 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { globalIDs } from '@/utils/constants'; import { globals } from '@/utils/constants';
import { CheckboxChecked } from '../Icons'; import { CheckboxChecked } from '../Icons';
import { type Button } from '../props'; import { CProps } from '../props';
export interface CheckboxProps extends Omit<Button, 'value' | 'onClick' | 'onChange'> { export interface CheckboxProps extends Omit<CProps.Button, 'value' | 'onClick' | 'onChange'> {
/** Label to display next to the checkbox. */ /** Label to display next to the checkbox. */
label?: string; label?: string;
@ -35,7 +35,7 @@ export function Checkbox({
}: CheckboxProps) { }: CheckboxProps) {
const cursor = disabled ? 'cursor-arrow' : onChange ? 'cursor-pointer' : ''; const cursor = disabled ? 'cursor-arrow' : onChange ? 'cursor-pointer' : '';
function handleClick(event: React.MouseEvent<Element>): void { function handleClick(event: CProps.EventMouse): void {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (disabled || !onChange) { if (disabled || !onChange) {
@ -49,14 +49,14 @@ export function Checkbox({
type='button' type='button'
className={clsx( className={clsx(
'flex items-center gap-2', // 'flex items-center gap-2', //
'outline-hidden', 'outline-none',
'focus-frame', 'focus-frame',
cursor, cursor,
className className
)} )}
disabled={disabled} disabled={disabled}
onClick={handleClick} onClick={handleClick}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined} data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
data-tooltip-html={titleHtml} data-tooltip-html={titleHtml}
data-tooltip-content={title} data-tooltip-content={title}
data-tooltip-hidden={hideTitle} data-tooltip-hidden={hideTitle}
@ -66,7 +66,7 @@ export function Checkbox({
className={clsx( className={clsx(
'max-w-[1rem] min-w-[1rem] h-4', // 'max-w-[1rem] min-w-[1rem] h-4', //
'pt-[0.05rem] pl-[0.05rem]', 'pt-[0.05rem] pl-[0.05rem]',
'border rounded-xs', 'border rounded-sm',
'cc-animate-color', 'cc-animate-color',
{ {
'bg-sec-600 text-sec-0': value !== false, 'bg-sec-600 text-sec-0': value !== false,

View File

@ -1,10 +1,11 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { globalIDs } from '@/utils/constants'; import { globals } from '@/utils/constants';
import { CheckboxChecked, CheckboxNull } from '../Icons'; import { CheckboxChecked, CheckboxNull } from '../Icons';
import { CProps } from '../props';
import { type CheckboxProps } from './Checkbox'; import { CheckboxProps } from './Checkbox';
export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'onChange'> { export interface CheckboxTristateProps extends Omit<CheckboxProps, 'value' | 'onChange'> {
/** Current value - `null`, `true` or `false`. */ /** Current value - `null`, `true` or `false`. */
@ -30,7 +31,7 @@ export function CheckboxTristate({
}: CheckboxTristateProps) { }: CheckboxTristateProps) {
const cursor = disabled ? 'cursor-arrow' : onChange ? 'cursor-pointer' : ''; const cursor = disabled ? 'cursor-arrow' : onChange ? 'cursor-pointer' : '';
function handleClick(event: React.MouseEvent<Element>): void { function handleClick(event: CProps.EventMouse): void {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (disabled || !onChange) { if (disabled || !onChange) {
@ -50,14 +51,14 @@ export function CheckboxTristate({
type='button' type='button'
className={clsx( className={clsx(
'flex items-center gap-2', // 'flex items-center gap-2', //
'outline-hidden', 'outline-none',
'focus-frame', 'focus-frame',
cursor, cursor,
className className
)} )}
disabled={disabled} disabled={disabled}
onClick={handleClick} onClick={handleClick}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined} data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
data-tooltip-html={titleHtml} data-tooltip-html={titleHtml}
data-tooltip-content={title} data-tooltip-content={title}
data-tooltip-hidden={hideTitle} data-tooltip-hidden={hideTitle}
@ -67,7 +68,7 @@ export function CheckboxTristate({
className={clsx( className={clsx(
'w-4 h-4', // 'w-4 h-4', //
'pt-[0.05rem] pl-[0.05rem]', 'pt-[0.05rem] pl-[0.05rem]',
'border rounded-xs', 'border rounded-sm',
'cc-animate-color', 'cc-animate-color',
{ {
'bg-sec-600 text-sec-0': value !== false, 'bg-sec-600 text-sec-0': value !== false,

View File

@ -1,9 +1,9 @@
import { type FieldError, type GlobalError } from 'react-hook-form'; import { FieldError, GlobalError } from 'react-hook-form';
import clsx from 'clsx'; import clsx from 'clsx';
import { type Styling } from '../props'; import { CProps } from '../props';
interface ErrorFieldProps extends Styling { interface ErrorFieldProps extends CProps.Styling {
error?: FieldError | GlobalError; error?: FieldError | GlobalError;
} }

View File

@ -5,11 +5,11 @@ import clsx from 'clsx';
import { Button } from '../Control'; import { Button } from '../Control';
import { IconUpload } from '../Icons'; import { IconUpload } from '../Icons';
import { type Titled } from '../props'; import { CProps } from '../props';
import { Label } from './Label'; import { Label } from './Label';
interface FileInputProps extends Titled, Omit<React.ComponentProps<'input'>, 'accept' | 'type'> { interface FileInputProps extends Omit<CProps.Input, 'accept' | 'type'> {
/** Label to display in file upload button. */ /** Label to display in file upload button. */
label: string; label: string;

View File

@ -1,6 +1,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
interface LabelProps extends Omit<React.ComponentProps<'label'>, 'children'> { import { CProps } from '../props';
interface LabelProps extends CProps.Label {
/** Text to display. */ /** Text to display. */
text?: string; text?: string;
} }

View File

@ -2,11 +2,11 @@ import clsx from 'clsx';
import { Overlay } from '@/components/Container'; import { Overlay } from '@/components/Container';
import { IconSearch } from '@/components/Icons'; import { IconSearch } from '@/components/Icons';
import { type Styling } from '@/components/props'; import { CProps } from '@/components/props';
import { TextInput } from './TextInput'; import { TextInput } from './TextInput';
interface SearchBarProps extends Styling { interface SearchBarProps extends CProps.Styling {
/** Id of the search bar. */ /** Id of the search bar. */
id?: string; id?: string;
@ -48,13 +48,12 @@ export function SearchBar({
<TextInput <TextInput
id={id} id={id}
noOutline noOutline
transparent
placeholder={placeholder} placeholder={placeholder}
type='search' type='search'
className={clsx('bg-transparent', !noIcon && 'pl-10')} className={clsx('outline-none bg-transparent', !noIcon && 'pl-10')}
noBorder={noBorder} noBorder={noBorder}
value={query} value={query}
onChange={event => onChangeQuery?.(event.target.value)} onChange={event => (onChangeQuery ? onChangeQuery(event.target.value) : undefined)}
/> />
</div> </div>
); );

View File

@ -1,15 +1,15 @@
'use client'; 'use client';
import Select, { import Select, {
type ClearIndicatorProps, ClearIndicatorProps,
components, components,
type DropdownIndicatorProps, DropdownIndicatorProps,
type GroupBase, GroupBase,
type Props, Props,
type StylesConfig StylesConfig
} from 'react-select'; } from 'react-select';
import { useWindowSize } from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { APP_COLORS, SELECT_THEME } from '@/styling/colors'; import { APP_COLORS, SELECT_THEME } from '@/styling/colors';
import { IconClose, IconDropArrow, IconDropArrowUp } from '../Icons'; import { IconClose, IconDropArrow, IconDropArrowUp } from '../Icons';

View File

@ -1,15 +1,15 @@
'use client'; 'use client';
import Select, { import Select, {
type ClearIndicatorProps, ClearIndicatorProps,
components, components,
type DropdownIndicatorProps, DropdownIndicatorProps,
type GroupBase, GroupBase,
type Props, Props,
type StylesConfig StylesConfig
} from 'react-select'; } from 'react-select';
import { useWindowSize } from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { APP_COLORS, SELECT_THEME } from '@/styling/colors'; import { APP_COLORS, SELECT_THEME } from '@/styling/colors';
import { IconClose, IconDropArrow, IconDropArrowUp } from '../Icons'; import { IconClose, IconDropArrow, IconDropArrowUp } from '../Icons';

View File

@ -1,14 +1,14 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { globalIDs, PARAMETER } from '@/utils/constants'; import { globals, PARAMETER } from '@/utils/constants';
import { Overlay } from '../Container'; import { Overlay } from '../Container';
import { MiniButton } from '../Control'; import { MiniButton } from '../Control';
import { IconDropArrow, IconPageRight } from '../Icons'; import { IconDropArrow, IconPageRight } from '../Icons';
import { type Styling } from '../props'; import { CProps } from '../props';
interface SelectTreeProps<ItemType> extends Styling { interface SelectTreeProps<ItemType> extends CProps.Styling {
/** Current value. */ /** Current value. */
value: ItemType; value: ItemType;
@ -66,13 +66,13 @@ export function SelectTree<ItemType>({
); );
} }
function handleClickFold(event: React.MouseEvent<Element>, target: ItemType, showChildren: boolean) { function handleClickFold(event: CProps.EventMouse, target: ItemType, showChildren: boolean) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
onFoldItem(target, showChildren); onFoldItem(target, showChildren);
} }
function handleSetValue(event: React.MouseEvent<Element>, target: ItemType) { function handleSetValue(event: CProps.EventMouse, target: ItemType) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
onChange(target); onChange(target);
@ -93,7 +93,7 @@ export function SelectTree<ItemType>({
value === item && 'clr-selected', value === item && 'clr-selected',
!isActive && 'pointer-events-none' !isActive && 'pointer-events-none'
)} )}
data-tooltip-id={globalIDs.tooltip} data-tooltip-id={globals.tooltip}
data-tooltip-html={getDescription(item)} data-tooltip-html={getDescription(item)}
onClick={event => handleSetValue(event, item)} onClick={event => handleSetValue(event, item)}
style={{ style={{

View File

@ -1,14 +1,11 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { Label } from '../Input/Label'; import { Label } from '../Input/Label';
import { type Editor, type ErrorProcessing, type Titled } from '../props'; import { CProps } from '../props';
import { ErrorField } from './ErrorField'; import { ErrorField } from './ErrorField';
export interface TextAreaProps extends Editor, ErrorProcessing, Titled, React.ComponentProps<'textarea'> { export interface TextAreaProps extends CProps.Editor, CProps.ErrorProcessing, CProps.Colors, CProps.TextArea {
/** Indicates that the input should be transparent. */
transparent?: boolean;
/** Indicates that padding should be minimal. */ /** Indicates that padding should be minimal. */
dense?: boolean; dense?: boolean;
@ -26,7 +23,6 @@ export function TextArea({
id, id,
label, label,
required, required,
transparent,
rows, rows,
dense, dense,
noBorder, noBorder,
@ -35,6 +31,7 @@ export function TextArea({
className, className,
fitContent, fitContent,
error, error,
colors = 'clr-input',
...restProps ...restProps
}: TextAreaProps) { }: TextAreaProps) {
return ( return (
@ -43,7 +40,7 @@ export function TextArea({
'w-full', 'w-full',
{ {
'flex flex-col': !dense, 'flex flex-col': !dense,
'flex grow items-center gap-3': dense 'flex flex-grow items-center gap-3': dense
}, },
dense && className dense && className
)} )}
@ -56,15 +53,14 @@ export function TextArea({
'leading-tight', 'leading-tight',
'overflow-x-hidden overflow-y-auto', 'overflow-x-hidden overflow-y-auto',
{ {
'field-sizing-content': fitContent, 'cc-fit-content': fitContent,
'resize-none': noResize, 'resize-none': noResize,
'border': !noBorder, 'border': !noBorder,
'grow max-w-full': dense, 'flex-grow max-w-full': dense,
'mt-2': !dense && !!label, 'mt-2': !dense && !!label,
'clr-outline': !noOutline, 'clr-outline': !noOutline
'bg-transparent': transparent,
'clr-input': !transparent
}, },
colors,
!dense && className !dense && className
)} )}
rows={rows} rows={rows}

View File

@ -1,14 +1,11 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { Label } from '../Input/Label'; import { Label } from '../Input/Label';
import { type Editor, type ErrorProcessing, type Titled } from '../props'; import { CProps } from '../props';
import { ErrorField } from './ErrorField'; import { ErrorField } from './ErrorField';
interface TextInputProps extends Editor, ErrorProcessing, Titled, React.ComponentProps<'input'> { interface TextInputProps extends CProps.Editor, CProps.ErrorProcessing, CProps.Colors, CProps.Input {
/** Indicates that the input should be transparent. */
transparent?: boolean;
/** Indicates that padding should be minimal. */ /** Indicates that padding should be minimal. */
dense?: boolean; dense?: boolean;
@ -33,8 +30,8 @@ export function TextInput({
noOutline, noOutline,
allowEnter, allowEnter,
disabled, disabled,
transparent,
className, className,
colors = 'clr-input',
onKeyDown, onKeyDown,
error, error,
...restProps ...restProps
@ -57,13 +54,12 @@ export function TextInput({
'leading-tight truncate hover:text-clip', 'leading-tight truncate hover:text-clip',
{ {
'px-3': !noBorder || !disabled, 'px-3': !noBorder || !disabled,
'grow max-w-full': dense, 'flex-grow max-w-full': dense,
'mt-2': !dense && !!label, 'mt-2': !dense && !!label,
'border': !noBorder, 'border': !noBorder,
'clr-outline': !noOutline, 'clr-outline': !noOutline
'bg-transparent': transparent,
'clr-input': !transparent
}, },
colors,
!dense && className !dense && className
)} )}
onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown} onKeyDown={!allowEnter && !onKeyDown ? preventEnterCapture : onKeyDown}

View File

@ -2,20 +2,20 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { BadgeHelp, type HelpTopic } from '@/features/help'; import { BadgeHelp, HelpTopic } from '@/features/help';
import { useEscapeKey } from '@/hooks/useEscapeKey'; import useEscapeKey from '@/hooks/useEscapeKey';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { prepareTooltip } from '@/utils/utils'; import { prepareTooltip } from '@/utils/utils';
import { Button, MiniButton, SubmitButton } from '../Control'; import { Button, MiniButton, SubmitButton } from '../Control';
import { IconClose } from '../Icons'; import { IconClose } from '../Icons';
import { type Styling } from '../props'; import { CProps } from '../props';
import { ModalBackdrop } from './ModalBackdrop'; import { ModalBackdrop } from './ModalBackdrop';
export interface ModalProps extends Styling { export interface ModalProps extends CProps.Styling {
/** Title of the modal window. */ /** Title of the modal window. */
header?: string; header?: string;
@ -115,7 +115,7 @@ export function ModalForm({
<div <div
className={clsx( className={clsx(
'overscroll-contain max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)] outline-hidden', 'overscroll-contain max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)] outline-none',
{ {
'overflow-auto': !overflowVisible, 'overflow-auto': !overflowVisible,
'overflow-visible': overflowVisible 'overflow-visible': overflowVisible

View File

@ -4,7 +4,7 @@ import clsx from 'clsx';
import { BadgeHelp } from '@/features/help'; import { BadgeHelp } from '@/features/help';
import { useEscapeKey } from '@/hooks/useEscapeKey'; import useEscapeKey from '@/hooks/useEscapeKey';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { prepareTooltip } from '@/utils/utils'; import { prepareTooltip } from '@/utils/utils';
@ -13,7 +13,7 @@ import { Button, MiniButton } from '../Control';
import { IconClose } from '../Icons'; import { IconClose } from '../Icons';
import { ModalBackdrop } from './ModalBackdrop'; import { ModalBackdrop } from './ModalBackdrop';
import { type ModalProps } from './ModalForm'; import { ModalProps } from './ModalForm';
interface ModalViewProps extends ModalProps {} interface ModalViewProps extends ModalProps {}
@ -60,7 +60,7 @@ export function ModalView({
<div <div
className={clsx( className={clsx(
'overscroll-contain max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)] outline-hidden', 'overscroll-contain max-h-[calc(100svh-8rem)] max-w-[100svw] xs:max-w-[calc(100svw-2rem)] outline-none',
{ {
'overflow-auto': !overflowVisible, 'overflow-auto': !overflowVisible,
'overflow-visible': overflowVisible 'overflow-visible': overflowVisible

View File

@ -2,11 +2,11 @@ import type { TabProps as TabPropsImpl } from 'react-tabs';
import { Tab as TabImpl } from 'react-tabs'; import { Tab as TabImpl } from 'react-tabs';
import clsx from 'clsx'; import clsx from 'clsx';
import { globalIDs } from '@/utils/constants'; import { globals } from '@/utils/constants';
import { type Titled } from '../props'; import { CProps } from '../props';
interface TabLabelProps extends Omit<TabPropsImpl, 'children'>, Titled { interface TabLabelProps extends Omit<TabPropsImpl, 'children'>, CProps.Titled {
/** Label to display in the tab. */ /** Label to display in the tab. */
label?: string; label?: string;
} }
@ -23,11 +23,11 @@ export function TabLabel({ label, title, titleHtml, hideTitle, className, ...oth
'clr-hover cc-animate-color duration-150', 'clr-hover cc-animate-color duration-150',
'text-sm whitespace-nowrap font-controls', 'text-sm whitespace-nowrap font-controls',
'select-none hover:cursor-pointer', 'select-none hover:cursor-pointer',
'outline-hidden', 'outline-none',
className className
)} )}
tabIndex='-1' tabIndex='-1'
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined} data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
data-tooltip-html={titleHtml} data-tooltip-html={titleHtml}
data-tooltip-content={title} data-tooltip-content={title}
data-tooltip-hidden={hideTitle} data-tooltip-hidden={hideTitle}

View File

@ -1,9 +1,9 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { type Styling, type Titled } from '@/components/props'; import { CProps } from '@/components/props';
import { globalIDs } from '@/utils/constants'; import { globals } from '@/utils/constants';
interface IndicatorProps extends Titled, Styling { interface IndicatorProps extends CProps.Titled, CProps.Styling {
/** Icon to display. */ /** Icon to display. */
icon: React.ReactNode; icon: React.ReactNode;
@ -19,13 +19,13 @@ export function Indicator({ icon, title, titleHtml, hideTitle, noPadding, classN
<div <div
className={clsx( className={clsx(
'clr-text-controls', 'clr-text-controls',
'outline-hidden', 'outline-none',
{ {
'px-1 py-1': !noPadding 'px-1 py-1': !noPadding
}, },
className className
)} )}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined} data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
data-tooltip-html={titleHtml} data-tooltip-html={titleHtml}
data-tooltip-content={title} data-tooltip-content={title}
data-tooltip-hidden={hideTitle} data-tooltip-hidden={hideTitle}

View File

@ -1,9 +1,11 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { CProps } from '@/components/props';
/** /**
* Wraps content in a div with a centered text. * Wraps content in a div with a centered text.
*/ */
export function NoData({ className, children, ...restProps }: React.ComponentProps<'div'>) { export function NoData({ className, children, ...restProps }: CProps.Div) {
return ( return (
<div className={clsx('p-3 flex flex-col items-center text-center select-none w-full', className)} {...restProps}> <div className={clsx('p-3 flex flex-col items-center text-center select-none w-full', className)} {...restProps}>
{children} {children}

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useWindowSize } from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { useFitHeight } from '@/stores/appLayout'; import { useFitHeight } from '@/stores/appLayout';
/** Maximum width of the viewer. */ /** Maximum width of the viewer. */

View File

@ -1,11 +1,10 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { globalIDs } from '@/utils/constants'; import { CProps } from '@/components/props';
import { globals } from '@/utils/constants';
import { truncateToLastWord } from '@/utils/utils'; import { truncateToLastWord } from '@/utils/utils';
import { type Styling } from '../props'; export interface TextContentProps extends CProps.Styling {
export interface TextContentProps extends Styling {
/** Text to display. */ /** Text to display. */
text: string; text: string;
@ -25,7 +24,7 @@ export function TextContent({ className, text, maxLength, noTooltip, ...restProp
return ( return (
<div <div
className={clsx('text-xs text-pretty', className)} className={clsx('text-xs text-pretty', className)}
data-tooltip-id={isTruncated && !noTooltip ? globalIDs.value_tooltip : undefined} data-tooltip-id={isTruncated && !noTooltip ? globals.value_tooltip : undefined}
data-tooltip-html={isTruncated && !noTooltip ? text : undefined} data-tooltip-html={isTruncated && !noTooltip ? text : undefined}
{...restProps} {...restProps}
> >

View File

@ -1,11 +1,11 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { globalIDs } from '@/utils/constants'; import { CProps } from '@/components/props';
import { globals } from '@/utils/constants';
import { MiniButton } from '../Control'; import { MiniButton } from '../Control';
import { type Styling, type Titled } from '../props';
interface ValueIconProps extends Styling, Titled { interface ValueIconProps extends CProps.Styling, CProps.Titled {
/** Id of the component. */ /** Id of the component. */
id?: string; id?: string;
@ -19,7 +19,7 @@ interface ValueIconProps extends Styling, Titled {
textClassName?: string; textClassName?: string;
/** Callback to be called when the component is clicked. */ /** Callback to be called when the component is clicked. */
onClick?: (event: React.MouseEvent<Element>) => void; onClick?: (event: CProps.EventMouse) => void;
/** Number of symbols to display in a small size. */ /** Number of symbols to display in a small size. */
smallThreshold?: number; smallThreshold?: number;
@ -61,7 +61,7 @@ export function ValueIcon({
className className
)} )}
{...restProps} {...restProps}
data-tooltip-id={!!title || !!titleHtml ? globalIDs.tooltip : undefined} data-tooltip-id={!!title || !!titleHtml ? globals.tooltip : undefined}
data-tooltip-html={titleHtml} data-tooltip-html={titleHtml}
data-tooltip-content={title} data-tooltip-content={title}
data-tooltip-hidden={hideTitle} data-tooltip-hidden={hideTitle}

View File

@ -1,8 +1,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { type Styling } from '@/components/props'; import { CProps } from '@/components/props';
interface ValueLabeledProps extends Styling { interface ValueLabeledProps extends CProps.Styling {
/** Id of the component. */ /** Id of the component. */
id?: string; id?: string;

View File

@ -1,9 +1,9 @@
import { type Styling, type Titled } from '@/components/props'; import { CProps } from '@/components/props';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { ValueIcon } from './ValueIcon'; import { ValueIcon } from './ValueIcon';
interface ValueStatsProps extends Styling, Titled { interface ValueStatsProps extends CProps.Styling, CProps.Titled {
/** Id of the component. */ /** Id of the component. */
id: string; id: string;

View File

@ -1,22 +1,31 @@
// =========== Module contains interfaces for common UI elements. ========== // =========== Module contains interfaces for common UI elements. ==========
import type React from 'react'; import React from 'react';
import { type FieldError } from 'react-hook-form'; import { FieldError } from 'react-hook-form';
/** export namespace CProps {
/**
* Represents an object that can have inline styles and CSS class names for styling. * Represents an object that can have inline styles and CSS class names for styling.
*/ */
export interface Styling { export interface Styling {
/** Optional inline styles for the component. */ /** Optional inline styles for the component. */
style?: React.CSSProperties; style?: React.CSSProperties;
/** Optional CSS class name(s) for the component. */ /** Optional CSS class name(s) for the component. */
className?: string; className?: string;
} }
/** /**
* Represents an object that can have a color or set of colors.
*/
export interface Colors {
/** Optional color or set of colors applied via classNames. */
colors?: string;
}
/**
* Represents an object that can have a title with optional HTML rendering and a flag to hide the title. * Represents an object that can have a title with optional HTML rendering and a flag to hide the title.
*/ */
export interface Titled { export interface Titled {
/** Tooltip: `plain text`. */ /** Tooltip: `plain text`. */
title?: string; title?: string;
@ -25,22 +34,22 @@ export interface Titled {
/** Indicates whether the `title` should be hidden. */ /** Indicates whether the `title` should be hidden. */
hideTitle?: boolean; hideTitle?: boolean;
} }
/** /**
* Represents an object that can have an error message. * Represents an object that can have an error message.
*/ */
export interface ErrorProcessing { export interface ErrorProcessing {
error?: FieldError; error?: FieldError;
} }
/** /**
* Represents `control` component with optional title and configuration options. * Represents `control` component with optional title and configuration options.
* *
* @remarks * @remarks
* This type extends the {@link Titled} interface, adding properties to control the visual and interactive behavior of a component. * This type extends the {@link Titled} interface, adding properties to control the visual and interactive behavior of a component.
*/ */
export type Control = Titled & { export type Control = Titled & {
/** Indicates whether the control is disabled. */ /** Indicates whether the control is disabled. */
disabled?: boolean; disabled?: boolean;
@ -49,20 +58,46 @@ export type Control = Titled & {
/** Indicates whether the control should render without an outline. */ /** Indicates whether the control should render without an outline. */
noOutline?: boolean; noOutline?: boolean;
}; };
/** /**
* Represents `editor` component that includes a label, control features, and optional title properties. * Represents `editor` component that includes a label, control features, and optional title properties.
* *
* @remarks * @remarks
* This type extends the {@link Control} type, inheriting title-related properties and additional configuration options, while also adding an optional label. * This type extends the {@link Control} type, inheriting title-related properties and additional configuration options, while also adding an optional label.
*/ */
export type Editor = Control & { export type Editor = Control & {
/** Text label. */ /** Text label. */
label?: string; label?: string;
}; };
/** /**
* Represents `div` component with all standard HTML attributes and React-specific properties.
*/
export type Div = React.ComponentProps<'div'>;
/**
* Represents `button` component with optional title and HTML attributes. * Represents `button` component with optional title and HTML attributes.
*/ */
export type Button = Titled & Omit<React.ComponentProps<'button'>, 'children' | 'type'>; export type Button = Titled & Omit<React.ComponentProps<'button'>, 'children' | 'type'>;
/**
* Represents `label` component with HTML attributes.
*/
export type Label = Omit<React.ComponentProps<'label'>, 'children'>;
/**
* Represents `textarea` component with optional title and HTML attributes.
*/
export type TextArea = Titled & React.ComponentProps<'textarea'>;
/**
* Represents `input` component with optional title and HTML attributes.
*/
export type Input = Titled & React.ComponentProps<'input'>;
/**
* Represents `mouse event` in React.
*/
export type EventMouse = React.MouseEvent<Element, MouseEvent>;
}

View File

@ -5,12 +5,12 @@ import { DELAYS, KEYS } from '@/backend/configuration';
import { infoMsg } from '@/utils/labels'; import { infoMsg } from '@/utils/labels';
import { import {
type IChangePasswordDTO, IChangePasswordDTO,
type ICurrentUser, ICurrentUser,
type IPasswordTokenDTO, IPasswordTokenDTO,
type IRequestPasswordDTO, IRequestPasswordDTO,
type IResetPasswordDTO, IResetPasswordDTO,
type IUserLoginDTO IUserLoginDTO
} from './types'; } from './types';
/** /**

View File

@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration'; import { KEYS } from '@/backend/configuration';
import { authApi } from './api'; import { authApi } from './api';
import { type IChangePasswordDTO } from './types'; import { IChangePasswordDTO } from './types';
export const useChangePassword = () => { export const useChangePassword = () => {
const client = useQueryClient(); const client = useQueryClient();

View File

@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration'; import { KEYS } from '@/backend/configuration';
import { authApi } from './api'; import { authApi } from './api';
import { type IUserLoginDTO } from './types'; import { IUserLoginDTO } from './types';
export const useLogin = () => { export const useLogin = () => {
const client = useQueryClient(); const client = useQueryClient();

View File

@ -3,7 +3,7 @@ import { useMutation } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration'; import { KEYS } from '@/backend/configuration';
import { authApi } from './api'; import { authApi } from './api';
import { type IRequestPasswordDTO } from './types'; import { IRequestPasswordDTO } from './types';
export const useRequestPasswordReset = () => { export const useRequestPasswordReset = () => {
const mutation = useMutation({ const mutation = useMutation({

View File

@ -3,7 +3,7 @@ import { useMutation } from '@tanstack/react-query';
import { KEYS } from '@/backend/configuration'; import { KEYS } from '@/backend/configuration';
import { authApi } from './api'; import { authApi } from './api';
import { type IPasswordTokenDTO, type IResetPasswordDTO } from './types'; import { IPasswordTokenDTO, IResetPasswordDTO } from './types';
export const useResetPassword = () => { export const useResetPassword = () => {
const validateMutation = useMutation({ const validateMutation = useMutation({

View File

@ -8,17 +8,17 @@ import { urls, useConceptNavigation } from '@/app';
import { isAxiosError } from '@/backend/apiTransport'; import { isAxiosError } from '@/backend/apiTransport';
import { SubmitButton, TextURL } from '@/components/Control'; import { SubmitButton, TextURL } from '@/components/Control';
import { type ErrorData } from '@/components/InfoError'; import { ErrorData } from '@/components/InfoError';
import { TextInput } from '@/components/Input'; import { TextInput } from '@/components/Input';
import { useQueryStrings } from '@/hooks/useQueryStrings'; import useQueryStrings from '@/hooks/useQueryStrings';
import { resources } from '@/utils/constants'; import { resources } from '@/utils/constants';
import { type IUserLoginDTO, schemaUserLogin } from '../backend/types'; import { IUserLoginDTO, schemaUserLogin } from '../backend/types';
import { useAuthSuspense } from '../backend/useAuth'; import { useAuthSuspense } from '../backend/useAuth';
import { useLogin } from '../backend/useLogin'; import { useLogin } from '../backend/useLogin';
import { ExpectedAnonymous } from '../components/ExpectedAnonymous'; import { ExpectedAnonymous } from '../components/ExpectedAnonymous';
export function LoginPage() { function LoginPage() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const query = useQueryStrings(); const query = useQueryStrings();
const initialName = query.get('username') ?? ''; const initialName = query.get('username') ?? '';
@ -92,6 +92,8 @@ export function LoginPage() {
); );
} }
export default LoginPage;
// ====== Internals ========= // ====== Internals =========
function ServerError({ error }: { error: ErrorData }): React.ReactElement | null { function ServerError({ error }: { error: ErrorData }): React.ReactElement | null {
if (isAxiosError(error) && error.response && error.response.status === 400) { if (isAxiosError(error) && error.response && error.response.status === 400) {

View File

@ -7,10 +7,10 @@ import { urls, useConceptNavigation } from '@/app';
import { isAxiosError } from '@/backend/apiTransport'; import { isAxiosError } from '@/backend/apiTransport';
import { SubmitButton } from '@/components/Control'; import { SubmitButton } from '@/components/Control';
import { type ErrorData, InfoError } from '@/components/InfoError'; import { ErrorData, InfoError } from '@/components/InfoError';
import { TextInput } from '@/components/Input'; import { TextInput } from '@/components/Input';
import { Loader } from '@/components/Loader'; import { Loader } from '@/components/Loader';
import { useQueryStrings } from '@/hooks/useQueryStrings'; import useQueryStrings from '@/hooks/useQueryStrings';
import { useResetPassword } from '../backend/useResetPassword'; import { useResetPassword } from '../backend/useResetPassword';
@ -24,6 +24,9 @@ export function Component() {
const [newPassword, setNewPassword] = useState(''); const [newPassword, setNewPassword] = useState('');
const [newPasswordRepeat, setNewPasswordRepeat] = useState(''); const [newPasswordRepeat, setNewPasswordRepeat] = useState('');
const passwordColor =
!!newPassword && !!newPasswordRepeat && newPassword !== newPasswordRepeat ? 'bg-warn-100' : 'clr-input';
const canSubmit = !!newPassword && !!newPasswordRepeat && newPassword === newPasswordRepeat; const canSubmit = !!newPassword && !!newPasswordRepeat && newPassword === newPasswordRepeat;
function handleSubmit(event: React.FormEvent<HTMLFormElement>) { function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
@ -58,6 +61,7 @@ export function Component() {
label='Новый пароль' label='Новый пароль'
autoComplete='new-password' autoComplete='new-password'
allowEnter allowEnter
colors={passwordColor}
value={newPassword} value={newPassword}
onChange={event => { onChange={event => {
setNewPassword(event.target.value); setNewPassword(event.target.value);
@ -69,6 +73,7 @@ export function Component() {
label='Повторите новый' label='Повторите новый'
autoComplete='new-password' autoComplete='new-password'
allowEnter allowEnter
colors={passwordColor}
value={newPasswordRepeat} value={newPasswordRepeat}
onChange={event => { onChange={event => {
setNewPasswordRepeat(event.target.value); setNewPasswordRepeat(event.target.value);

View File

@ -5,7 +5,7 @@ import clsx from 'clsx';
import { isAxiosError } from '@/backend/apiTransport'; import { isAxiosError } from '@/backend/apiTransport';
import { SubmitButton, TextURL } from '@/components/Control'; import { SubmitButton, TextURL } from '@/components/Control';
import { type ErrorData } from '@/components/InfoError'; import { ErrorData } from '@/components/InfoError';
import { TextInput } from '@/components/Input'; import { TextInput } from '@/components/Input';
import { useRequestPasswordReset } from '../backend/useRequestPasswordReset'; import { useRequestPasswordReset } from '../backend/useRequestPasswordReset';

View File

@ -1,19 +1,17 @@
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
import { type PlacesType, Tooltip } from '@/components/Container'; import { PlacesType, Tooltip } from '@/components/Container';
import { TextURL } from '@/components/Control'; import { TextURL } from '@/components/Control';
import { IconHelp } from '@/components/Icons'; import { IconHelp } from '@/components/Icons';
import { Loader } from '@/components/Loader'; import { Loader } from '@/components/Loader';
import { type Styling } from '@/components/props'; import { CProps } from '@/components/props';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { type HelpTopic } from '../models/helpTopic'; import { HelpTopic } from '../models/helpTopic';
const TopicPage = React.lazy(() => const TopicPage = React.lazy(() => import('@/features/help/pages/ManualsPage/TopicPage'));
import('@/features/help/pages/ManualsPage/TopicPage').then(module => ({ default: module.TopicPage }))
);
interface BadgeHelpProps extends Styling { interface BadgeHelpProps extends CProps.Styling {
/** Topic to display in a tooltip. */ /** Topic to display in a tooltip. */
topic: HelpTopic; topic: HelpTopic;

View File

@ -10,7 +10,7 @@ interface InfoCstClassProps {
header?: string; header?: string;
} }
export function InfoCstClass({ header }: InfoCstClassProps) { function InfoCstClass({ header }: InfoCstClassProps) {
return ( return (
<div className='flex flex-col gap-1 mb-2 dense'> <div className='flex flex-col gap-1 mb-2 dense'>
{header ? <h1>{header}</h1> : null} {header ? <h1>{header}</h1> : null}
@ -31,3 +31,5 @@ export function InfoCstClass({ header }: InfoCstClassProps) {
</div> </div>
); );
} }
export default InfoCstClass;

View File

@ -10,7 +10,7 @@ interface InfoCstStatusProps {
title?: string; title?: string;
} }
export function InfoCstStatus({ title }: InfoCstStatusProps) { function InfoCstStatus({ title }: InfoCstStatusProps) {
return ( return (
<div className='flex flex-col gap-1 mb-2 dense'> <div className='flex flex-col gap-1 mb-2 dense'>
{title ? <h1>{title}</h1> : null} {title ? <h1>{title}</h1> : null}
@ -37,3 +37,5 @@ export function InfoCstStatus({ title }: InfoCstStatusProps) {
</div> </div>
); );
} }
export default InfoCstStatus;

View File

@ -2,7 +2,7 @@ import { urls } from '@/app';
import { TextURL } from '@/components/Control'; import { TextURL } from '@/components/Control';
import { type HelpTopic } from '../models/helpTopic'; import { HelpTopic } from '../models/helpTopic';
interface TextURLProps { interface TextURLProps {
/** Text to display. */ /** Text to display. */

View File

@ -2,7 +2,7 @@ import { removeTags } from '@/utils/utils';
import { LinkTopic } from '../components/LinkTopic'; import { LinkTopic } from '../components/LinkTopic';
import { describeHelpTopic, labelHelpTopic } from '../labels'; import { describeHelpTopic, labelHelpTopic } from '../labels';
import { type HelpTopic } from '../models/helpTopic'; import { HelpTopic } from '../models/helpTopic';
interface TopicItemProps { interface TopicItemProps {
topic: HelpTopic; topic: HelpTopic;

View File

@ -1,6 +1,6 @@
import { IconHide, IconImmutable, IconPrivate, IconProtected, IconPublic } from '@/components/Icons'; import { IconHide, IconImmutable, IconPrivate, IconProtected, IconPublic } from '@/components/Icons';
export function HelpAccess() { function HelpAccess() {
return ( return (
<div> <div>
<h1>Организация доступов</h1> <h1>Организация доступов</h1>
@ -29,3 +29,5 @@ export function HelpAccess() {
</div> </div>
); );
} }
export default HelpAccess;

View File

@ -4,7 +4,7 @@ import { external_urls } from '@/utils/constants';
import { Subtopics } from '../components/Subtopics'; import { Subtopics } from '../components/Subtopics';
import { HelpTopic } from '../models/helpTopic'; import { HelpTopic } from '../models/helpTopic';
export function HelpConcept() { function HelpConceptSystem() {
return ( return (
<div className='text-justify'> <div className='text-justify'>
<h1>Концептуализация</h1> <h1>Концептуализация</h1>
@ -43,3 +43,5 @@ export function HelpConcept() {
</div> </div>
); );
} }
export default HelpConceptSystem;

View File

@ -1,7 +1,7 @@
import { TextURL } from '@/components/Control'; import { TextURL } from '@/components/Control';
import { external_urls, PARAMETER } from '@/utils/constants'; import { external_urls, PARAMETER } from '@/utils/constants';
export function HelpExteor() { function HelpExteor() {
return ( return (
<div> <div>
<h1>Экстеор</h1> <h1>Экстеор</h1>
@ -38,3 +38,5 @@ export function HelpExteor() {
</div> </div>
); );
} }
export default HelpExteor;

View File

@ -1,7 +1,7 @@
import { Subtopics } from '../components/Subtopics'; import { Subtopics } from '../components/Subtopics';
import { HelpTopic } from '../models/helpTopic'; import { HelpTopic } from '../models/helpTopic';
export function HelpInfo() { function HelpInfo() {
return ( return (
<div> <div>
<h1>Справочная информация и документы</h1> <h1>Справочная информация и документы</h1>
@ -14,3 +14,5 @@ export function HelpInfo() {
</div> </div>
); );
} }
export default HelpInfo;

View File

@ -12,7 +12,7 @@ import {
import { Subtopics } from '../components/Subtopics'; import { Subtopics } from '../components/Subtopics';
import { HelpTopic } from '../models/helpTopic'; import { HelpTopic } from '../models/helpTopic';
export function HelpInterface() { function HelpInterface() {
return ( return (
<div> <div>
<h1>Пользовательский интерфейс</h1> <h1>Пользовательский интерфейс</h1>
@ -60,3 +60,5 @@ export function HelpInterface() {
</div> </div>
); );
} }
export default HelpInterface;

View File

@ -5,7 +5,7 @@ import { LinkTopic } from '../components/LinkTopic';
import { TopicItem } from '../components/TopicItem'; import { TopicItem } from '../components/TopicItem';
import { HelpTopic } from '../models/helpTopic'; import { HelpTopic } from '../models/helpTopic';
export function HelpMain() { function HelpMain() {
return ( return (
<div className='text-justify'> <div className='text-justify'>
<h1>Портал</h1> <h1>Портал</h1>
@ -66,3 +66,5 @@ export function HelpMain() {
</div> </div>
); );
} }
export default HelpMain;

View File

@ -1,11 +1,11 @@
import { EmbedYoutube } from '@/components/View'; import { EmbedYoutube } from '@/components/View';
import { useWindowSize } from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { external_urls, youtube } from '@/utils/constants'; import { external_urls, youtube } from '@/utils/constants';
import { Subtopics } from '../components/Subtopics'; import { Subtopics } from '../components/Subtopics';
import { HelpTopic } from '../models/helpTopic'; import { HelpTopic } from '../models/helpTopic';
export function HelpRSLang() { function HelpRSLang() {
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const videoHeight = (() => { const videoHeight = (() => {
@ -35,3 +35,5 @@ export function HelpRSLang() {
</div> </div>
</div>); </div>);
} }
export default HelpRSLang;

View File

@ -1,4 +1,4 @@
export function HelpTerminologyControl() { function HelpTerminologyControl() {
return ( return (
<div> <div>
<h1>Терминологизация</h1> <h1>Терминологизация</h1>
@ -24,3 +24,5 @@ export function HelpTerminologyControl() {
</div> </div>
); );
} }
export default HelpTerminologyControl;

View File

@ -30,7 +30,7 @@ import {
import { LinkTopic } from '../components/LinkTopic'; import { LinkTopic } from '../components/LinkTopic';
import { HelpTopic } from '../models/helpTopic'; import { HelpTopic } from '../models/helpTopic';
export function HelpThesaurus() { function HelpThesaurus() {
return ( return (
<div className='text-justify'> <div className='text-justify'>
<h1>Тезаурус</h1> <h1>Тезаурус</h1>
@ -281,3 +281,5 @@ export function HelpThesaurus() {
</div> </div>
); );
} }
export default HelpThesaurus;

View File

@ -1,6 +1,6 @@
import { IconEditor, IconNewVersion, IconShare, IconUpload, IconVersions } from '@/components/Icons'; import { IconEditor, IconNewVersion, IconShare, IconUpload, IconVersions } from '@/components/Icons';
export function HelpVersions() { function HelpVersions() {
return ( return (
<div className=''> <div className=''>
<h1>Версионирование схем</h1> <h1>Версионирование схем</h1>
@ -28,3 +28,5 @@ export function HelpVersions() {
</div> </div>
); );
} }
export default HelpVersions;

View File

@ -11,7 +11,7 @@ import {
import { LinkTopic } from '../../components/LinkTopic'; import { LinkTopic } from '../../components/LinkTopic';
import { HelpTopic } from '../../models/helpTopic'; import { HelpTopic } from '../../models/helpTopic';
export function HelpConceptOSS() { function HelpConceptOSS() {
return ( return (
<div className='text-justify'> <div className='text-justify'>
<h1>Операционная схема синтеза</h1> <h1>Операционная схема синтеза</h1>
@ -70,3 +70,5 @@ export function HelpConceptOSS() {
</div> </div>
); );
} }
export default HelpConceptOSS;

View File

@ -3,7 +3,7 @@ import { IconPredecessor } from '@/components/Icons';
import { LinkTopic } from '../../components/LinkTopic'; import { LinkTopic } from '../../components/LinkTopic';
import { HelpTopic } from '../../models/helpTopic'; import { HelpTopic } from '../../models/helpTopic';
export function HelpConceptPropagation() { function HelpConceptPropagation() {
return ( return (
<div className='text-justify'> <div className='text-justify'>
<h1>Сквозные изменения</h1> <h1>Сквозные изменения</h1>
@ -44,3 +44,5 @@ export function HelpConceptPropagation() {
</div> </div>
); );
} }
export default HelpConceptPropagation;

View File

@ -1,7 +1,7 @@
import { LinkTopic } from '../../components/LinkTopic'; import { LinkTopic } from '../../components/LinkTopic';
import { HelpTopic } from '../../models/helpTopic'; import { HelpTopic } from '../../models/helpTopic';
export function HelpConceptRelations() { function HelpConceptRelations() {
return ( return (
<div className='text-justify'> <div className='text-justify'>
<h1>Связи между конституентами</h1> <h1>Связи между конституентами</h1>
@ -38,3 +38,5 @@ export function HelpConceptRelations() {
</div> </div>
); );
} }
export default HelpConceptRelations;

View File

@ -1,7 +1,7 @@
import { LinkTopic } from '../../components/LinkTopic'; import { LinkTopic } from '../../components/LinkTopic';
import { HelpTopic } from '../../models/helpTopic'; import { HelpTopic } from '../../models/helpTopic';
export function HelpConceptSynthesis() { function HelpConceptSynthesis() {
return ( return (
<div className='text-justify'> <div className='text-justify'>
<h1>Синтез концептуальных схем</h1> <h1>Синтез концептуальных схем</h1>
@ -46,3 +46,5 @@ export function HelpConceptSynthesis() {
</div> </div>
); );
} }
export default HelpConceptSynthesis;

View File

@ -1,7 +1,7 @@
import { LinkTopic } from '../../components/LinkTopic'; import { LinkTopic } from '../../components/LinkTopic';
import { HelpTopic } from '../../models/helpTopic'; import { HelpTopic } from '../../models/helpTopic';
export function HelpConceptSystem() { function HelpConceptSystem() {
return ( return (
<div className='text-justify'> <div className='text-justify'>
<h1>Концептуальная схема Система определений</h1> <h1>Концептуальная схема Система определений</h1>
@ -61,3 +61,5 @@ export function HelpConceptSystem() {
</div> </div>
); );
} }
export default HelpConceptSystem;

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