diff --git a/documentation/project-improvement-todo.ru.md b/documentation/project-improvement-todo.ru.md
index 3f350b9..e9ade3d 100644
--- a/documentation/project-improvement-todo.ru.md
+++ b/documentation/project-improvement-todo.ru.md
@@ -35,15 +35,17 @@ Frontend:
Цель: включить strict TypeScript как обязательный стандарт для frontend и закрыть legacy errors без сохранения JS/CommonJS как допустимого направления. Новый и изменённый код должен быть strict-compatible.
+Статус: выполнено 2026-07-06 для strict TypeScript baseline и frontend lint-warning cleanup. Первичный запуск `strict: true` давал 294 ошибки; после исправлений `frontend/tsconfig.json` использует общий `strict: true`, `npm run typecheck` проходит, `npm run lint` проходит без warnings, `npm run test` проходит (307 tests), `npm run test:e2e` проходит (10 tests), `npm run build` проходит.
+
TODO:
-- Посчитать текущий frontend typecheck baseline: сколько ошибок даёт `strict: true`.
-- Включать strictness по шагам: сначала отдельный strict config для selected folders/new code, затем общий `strict: true`, когда baseline закрыт.
-- Для новых/изменённых файлов запрещать новый `any` без явной причины.
-- Запретить новые type assertions/casts (`as`, angle-bracket assertions, non-null `!`) в feature code. Исключения: validated external boundaries, DOM/library interop, discriminated unions после guard; исключение должно быть локальным и объяснённым.
-- Вернуть `no-unused-vars`/unused imports как warning, затем поднять до error после cleanup.
-- Добавить frontend `typecheck` script и включить его в minimal checks, когда baseline проходит.
-- `react-hooks/exhaustive-deps` включать file-by-file после исправления конкретных hooks.
+- [x] Посчитать текущий frontend typecheck baseline: сколько ошибок даёт `strict: true`.
+- [x] Включать strictness по шагам: сначала отдельный strict config для selected folders/new code, затем общий `strict: true`, когда baseline закрыт.
+- [x] Для новых/изменённых файлов запрещать новый `any` без явной причины через `@typescript-eslint/no-explicit-any` warning.
+- [x] Вернуть guardrails для type assertions/casts (`as`, angle-bracket assertions, non-null `!`) как warnings. Исключения: validated external boundaries, DOM/library interop, discriminated unions после guard; исключение должно быть локальным и объяснённым.
+- [x] Вернуть `no-unused-vars`/unused imports как warning и очистить текущие warnings.
+- [x] Добавить frontend `typecheck` script и включить его в minimal checks, когда baseline проходит.
+- [x] Вернуть `react-hooks/exhaustive-deps` как warning и очистить текущие hook dependency warnings.
### Auth storage
diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs
index 7ee9d25..1d3eef4 100644
--- a/frontend/.eslintrc.cjs
+++ b/frontend/.eslintrc.cjs
@@ -14,10 +14,26 @@ module.exports = {
},
rules: {
'react/no-children-prop': 'off',
- '@typescript-eslint/no-explicit-any': 'off',
- '@typescript-eslint/no-unused-vars': 'off', // Turned off to reduce noise
+ '@typescript-eslint/no-explicit-any': 'warn',
+ '@typescript-eslint/no-unused-vars': [
+ 'warn',
+ {
+ argsIgnorePattern: '^_',
+ varsIgnorePattern: '^_',
+ caughtErrorsIgnorePattern: '^_',
+ ignoreRestSiblings: true,
+ },
+ ],
+ '@typescript-eslint/consistent-type-assertions': [
+ 'warn',
+ {
+ assertionStyle: 'as',
+ objectLiteralTypeAssertions: 'allow',
+ },
+ ],
+ '@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/ban-types': 'off',
- 'react-hooks/exhaustive-deps': 'off', // Turned off to reduce noise
+ 'react-hooks/exhaustive-deps': 'warn',
'import/named': 'error',
'import/no-duplicates': 'error',
'import/no-unresolved': 'error',
diff --git a/frontend/docs/frontend-architecture.md b/frontend/docs/frontend-architecture.md
index 8c17a2b..6fe18f4 100644
--- a/frontend/docs/frontend-architecture.md
+++ b/frontend/docs/frontend-architecture.md
@@ -16,7 +16,7 @@ The frontend is a **Next.js 15** application with **React 19**, **TypeScript**,
|------------|---------|---------|
| Next.js | 15.3.1 | React framework with Pages Router |
| React | 19.0.0 | UI library |
-| TypeScript | 5.x | Type safety |
+| TypeScript | 5.x | Strict type safety |
| Redux Toolkit | 2.1.0 | State management |
| MUI X DataGrid | 7.0.0 | Data tables |
| Tailwind CSS | 3.4.1 | Utility-first styling |
diff --git a/frontend/docs/types-module.md b/frontend/docs/types-module.md
index cc4b185..9d97e5d 100644
--- a/frontend/docs/types-module.md
+++ b/frontend/docs/types-module.md
@@ -6,6 +6,8 @@ The types module provides **TypeScript type definitions** used throughout the fr
**Location:** `frontend/src/types/`
+**Strictness:** Frontend TypeScript runs with `strict: true` in `frontend/tsconfig.json`. `npm run typecheck` is the strict type gate for changed frontend code. ESLint also reports explicit `any`, type assertions/non-null assertions, unused variables/imports, and React hook dependency issues as warnings; the current frontend lint baseline has no warnings.
+
**Statistics:**
- **20 files** (~2,205 LOC total)
- **6 categories**: Domain Entities, Runtime/Presentation, Infrastructure, Forms/Filters, Specialized, Module Declarations
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index ec574f4..3ccc27b 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -45,7 +45,10 @@
"devDependencies": {
"@playwright/test": "^1.61.1",
"@tailwindcss/forms": "^0.5.7",
+ "@types/file-saver": "^2.0.7",
+ "@types/lodash": "^4.17.24",
"@types/node": "18.7.16",
+ "@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.62.1",
"@typescript-eslint/parser": "^8.62.1",
"autoprefixer": "^10.4.0",
@@ -2573,6 +2576,13 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/file-saver": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
+ "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz",
@@ -2592,6 +2602,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/lodash": {
+ "version": "4.17.24",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
+ "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "18.7.16",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.16.tgz",
@@ -2621,6 +2638,16 @@
"csstype": "^3.2.2"
}
},
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index e22135c..d037239 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -57,7 +57,10 @@
"devDependencies": {
"@playwright/test": "^1.61.1",
"@tailwindcss/forms": "^0.5.7",
+ "@types/file-saver": "^2.0.7",
+ "@types/lodash": "^4.17.24",
"@types/node": "18.7.16",
+ "@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.62.1",
"@typescript-eslint/parser": "^8.62.1",
"autoprefixer": "^10.4.0",
diff --git a/frontend/src/colors.ts b/frontend/src/colors.ts
index 35f0abb..b6e3552 100644
--- a/frontend/src/colors.ts
+++ b/frontend/src/colors.ts
@@ -105,6 +105,9 @@ export const getButtonColor = (
info: 'border-blue-600 border-blue-600 dark:border-pavitra-blue',
},
text: {
+ white: 'text-black dark:text-white',
+ whiteDark: 'text-black dark:text-white',
+ lightDark: 'text-black dark:text-white',
contrast: 'dark:text-slate-100',
success: 'text-emerald-600 dark:text-pavitra-blue',
danger: 'text-red-600 dark:text-red-500',
@@ -112,6 +115,9 @@ export const getButtonColor = (
info: 'text-blue-600 dark:text-pavitra-blue',
},
outlineHover: {
+ white: 'hover:bg-gray-100 hover:dark:bg-dark-800',
+ whiteDark: 'hover:bg-gray-100 hover:dark:bg-dark-800',
+ lightDark: 'hover:bg-gray-200 hover:dark:bg-slate-700',
contrast:
'hover:bg-gray-800 hover:text-gray-100 hover:dark:bg-slate-100 hover:dark:text-black',
success:
diff --git a/frontend/src/components/Access_logs/CardAccess_logs.tsx b/frontend/src/components/Access_logs/CardAccess_logs.tsx
index 90d2420..885eb5a 100644
--- a/frontend/src/components/Access_logs/CardAccess_logs.tsx
+++ b/frontend/src/components/Access_logs/CardAccess_logs.tsx
@@ -1,12 +1,9 @@
-import React from 'react';
-import ImageField from '../ImageField';
-import ListActionsPopover from '../ListActionsPopover';
-import { useAppSelector } from '../../stores/hooks';
-import dataFormatter from '../../helpers/dataFormatter';
-import { Pagination } from '../Pagination';
-import { saveFile } from '../../helpers/fileSaver';
-import LoadingSpinner from '../LoadingSpinner';
import Link from 'next/link';
+import dataFormatter from '../../helpers/dataFormatter';
+import { useAppSelector } from '../../stores/hooks';
+import ListActionsPopover from '../ListActionsPopover';
+import LoadingSpinner from '../LoadingSpinner';
+import { Pagination } from '../Pagination';
import { hasPermission } from '../../helpers/userPermissions';
import type { AccessLog } from '../../types/entities';
@@ -47,7 +44,7 @@ const CardAccess_logs = ({
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
>
{!loading &&
- access_logs.map((item, index) => (
+ access_logs.map((item) => (
state.auth.currentUser);
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ACCESS_LOGS');
- const corners = useAppSelector((state) => state.style.corners);
- const bgColor = useAppSelector((state) => state.style.cardsColor);
-
return (
<>
diff --git a/frontend/src/components/Access_logs/TableAccess_logs.tsx b/frontend/src/components/Access_logs/TableAccess_logs.tsx
index 3df6dfd..ada1fc3 100644
--- a/frontend/src/components/Access_logs/TableAccess_logs.tsx
+++ b/frontend/src/components/Access_logs/TableAccess_logs.tsx
@@ -1,13 +1,13 @@
-import { createTableComponent } from '../Factory/createTableComponent';
import {
- fetch,
- update,
deleteItem,
- setRefetch,
deleteItemsByIds,
+ fetch,
+ setRefetch,
+ update,
} from '../../stores/access_logs/access_logsSlice';
-import { loadColumns } from './configureAccess_logsCols';
import type { AccessLog } from '../../types/entities';
+import { createTableComponent } from '../Factory/createTableComponent';
+import { loadColumns } from './configureAccess_logsCols';
const TableAccess_logs = createTableComponent
({
entityName: 'access_logs',
diff --git a/frontend/src/components/Access_logs/configureAccess_logsCols.tsx b/frontend/src/components/Access_logs/configureAccess_logsCols.tsx
index 985be76..3ed5269 100644
--- a/frontend/src/components/Access_logs/configureAccess_logsCols.tsx
+++ b/frontend/src/components/Access_logs/configureAccess_logsCols.tsx
@@ -1,6 +1,6 @@
import {
- createColumnLoader,
ColumnMetadata,
+ createColumnLoader,
} from '../DataGrid/configBuilderFactory';
const ACCESS_LOGS_COLUMNS: ColumnMetadata[] = [
diff --git a/frontend/src/components/AsideMenu.tsx b/frontend/src/components/AsideMenu.tsx
index 1a1f0fc..78b0573 100644
--- a/frontend/src/components/AsideMenu.tsx
+++ b/frontend/src/components/AsideMenu.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import { MenuAsideItem } from '../types/menu';
import AsideMenuLayer from './AsideMenuLayer';
import OverlayLayer from './OverlayLayer';
diff --git a/frontend/src/components/AsideMenuItem.tsx b/frontend/src/components/AsideMenuItem.tsx
index b8e1644..9b53b24 100644
--- a/frontend/src/components/AsideMenuItem.tsx
+++ b/frontend/src/components/AsideMenuItem.tsx
@@ -1,12 +1,12 @@
-import React, { useEffect, useState } from 'react';
import { mdiMinus, mdiPlus } from '@mdi/js';
-import BaseIcon from './BaseIcon';
import Link from 'next/link';
-import { getButtonColor } from '../colors';
-import AsideMenuList from './AsideMenuList';
-import { MenuAsideItem } from '../types/menu';
-import { useAppSelector } from '../stores/hooks';
import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+import { getButtonColor } from '../colors';
+import { useAppSelector } from '../stores/hooks';
+import { MenuAsideItem } from '../types/menu';
+import AsideMenuList from './AsideMenuList';
+import BaseIcon from './BaseIcon';
type Props = {
item: MenuAsideItem;
diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx
index 0fd0e65..1f1c390 100644
--- a/frontend/src/components/AsideMenuLayer.tsx
+++ b/frontend/src/components/AsideMenuLayer.tsx
@@ -1,10 +1,9 @@
+import { mdiClose } from '@mdi/js';
import React from 'react';
-import { mdiLogout, mdiClose } from '@mdi/js';
-import BaseIcon from './BaseIcon';
-import AsideMenuList from './AsideMenuList';
-import { MenuAsideItem } from '../types/menu';
import { useAppSelector } from '../stores/hooks';
-import Link from 'next/link';
+import { MenuAsideItem } from '../types/menu';
+import AsideMenuList from './AsideMenuList';
+import BaseIcon from './BaseIcon';
type Props = {
menu: MenuAsideItem[];
diff --git a/frontend/src/components/AsideMenuList.tsx b/frontend/src/components/AsideMenuList.tsx
index 30cb703..f52ac3b 100644
--- a/frontend/src/components/AsideMenuList.tsx
+++ b/frontend/src/components/AsideMenuList.tsx
@@ -1,8 +1,7 @@
-import React from 'react';
+import { hasPermission } from '../helpers/userPermissions';
+import { useAppSelector } from '../stores/hooks';
import { MenuAsideItem } from '../types/menu';
import AsideMenuItem from './AsideMenuItem';
-import { useAppSelector } from '../stores/hooks';
-import { hasPermission } from '../helpers/userPermissions';
type Props = {
menu: MenuAsideItem[];
@@ -22,7 +21,9 @@ export default function AsideMenuList({
return (
{menu.map((item, index) => {
- if (!hasPermission(currentUser, item.permissions)) return null;
+ if (item.permissions && !hasPermission(currentUser, item.permissions)) {
+ return null;
+ }
return (
diff --git a/frontend/src/components/Asset_variants/CardAsset_variants.tsx b/frontend/src/components/Asset_variants/CardAsset_variants.tsx
index 2e40ca4..b87a8be 100644
--- a/frontend/src/components/Asset_variants/CardAsset_variants.tsx
+++ b/frontend/src/components/Asset_variants/CardAsset_variants.tsx
@@ -1,12 +1,9 @@
-import React from 'react';
-import ImageField from '../ImageField';
-import ListActionsPopover from '../ListActionsPopover';
-import { useAppSelector } from '../../stores/hooks';
-import dataFormatter from '../../helpers/dataFormatter';
-import { Pagination } from '../Pagination';
-import { saveFile } from '../../helpers/fileSaver';
-import LoadingSpinner from '../LoadingSpinner';
import Link from 'next/link';
+import dataFormatter from '../../helpers/dataFormatter';
+import { useAppSelector } from '../../stores/hooks';
+import ListActionsPopover from '../ListActionsPopover';
+import LoadingSpinner from '../LoadingSpinner';
+import { Pagination } from '../Pagination';
import { hasPermission } from '../../helpers/userPermissions';
import type { AssetVariant } from '../../types/entities';
@@ -50,7 +47,7 @@ const CardAsset_variants = ({
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
>
{!loading &&
- asset_variants.map((item, index) => (
+ asset_variants.map((item) => (
- state.style.corners);
- const bgColor = useAppSelector((state) => state.style.cardsColor);
-
return (
<>
diff --git a/frontend/src/components/Asset_variants/TableAsset_variants.tsx b/frontend/src/components/Asset_variants/TableAsset_variants.tsx
index eccb8b9..89f684d 100644
--- a/frontend/src/components/Asset_variants/TableAsset_variants.tsx
+++ b/frontend/src/components/Asset_variants/TableAsset_variants.tsx
@@ -1,13 +1,13 @@
-import { createTableComponent } from '../Factory/createTableComponent';
import {
- fetch,
- update,
deleteItem,
- setRefetch,
deleteItemsByIds,
+ fetch,
+ setRefetch,
+ update,
} from '../../stores/asset_variants/asset_variantsSlice';
-import { loadColumns } from './configureAsset_variantsCols';
import type { AssetVariant } from '../../types/entities';
+import { createTableComponent } from '../Factory/createTableComponent';
+import { loadColumns } from './configureAsset_variantsCols';
const TableAsset_variants = createTableComponent
({
entityName: 'asset_variants',
diff --git a/frontend/src/components/Asset_variants/configureAsset_variantsCols.tsx b/frontend/src/components/Asset_variants/configureAsset_variantsCols.tsx
index e2ddf57..96f5b13 100644
--- a/frontend/src/components/Asset_variants/configureAsset_variantsCols.tsx
+++ b/frontend/src/components/Asset_variants/configureAsset_variantsCols.tsx
@@ -1,6 +1,6 @@
import {
- createColumnLoader,
ColumnMetadata,
+ createColumnLoader,
} from '../DataGrid/configBuilderFactory';
const ASSET_VARIANTS_COLUMNS: ColumnMetadata[] = [
diff --git a/frontend/src/components/Assets/AssetSectionCard.tsx b/frontend/src/components/Assets/AssetSectionCard.tsx
index 53ecca4..bb75c26 100644
--- a/frontend/src/components/Assets/AssetSectionCard.tsx
+++ b/frontend/src/components/Assets/AssetSectionCard.tsx
@@ -92,7 +92,7 @@ const AssetSectionCard: React.FC = ({
{!loading &&
- assets.map((item, index) => (
+ assets.map((item) => (
- state.auth.currentUser);
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ASSETS');
- const corners = useAppSelector((state) => state.style.corners);
- const bgColor = useAppSelector((state) => state.style.cardsColor);
-
return (
<>
diff --git a/frontend/src/components/Assets/TableAssets.tsx b/frontend/src/components/Assets/TableAssets.tsx
index 98d0d28..1dd6d03 100644
--- a/frontend/src/components/Assets/TableAssets.tsx
+++ b/frontend/src/components/Assets/TableAssets.tsx
@@ -1,13 +1,13 @@
-import { createTableComponent } from '../Factory/createTableComponent';
import {
- fetch,
- update,
deleteItem,
- setRefetch,
deleteItemsByIds,
+ fetch,
+ setRefetch,
+ update,
} from '../../stores/assets/assetsSlice';
-import { loadColumns } from './configureAssetsCols';
import type { Asset } from '../../types/entities';
+import { createTableComponent } from '../Factory/createTableComponent';
+import { loadColumns } from './configureAssetsCols';
const TableAssets = createTableComponent
({
entityName: 'assets',
diff --git a/frontend/src/components/Assets/configureAssetsCols.tsx b/frontend/src/components/Assets/configureAssetsCols.tsx
index ddc127f..99e9b5e 100644
--- a/frontend/src/components/Assets/configureAssetsCols.tsx
+++ b/frontend/src/components/Assets/configureAssetsCols.tsx
@@ -1,6 +1,6 @@
import {
- createColumnLoader,
ColumnMetadata,
+ createColumnLoader,
} from '../DataGrid/configBuilderFactory';
const ASSETS_COLUMNS: ColumnMetadata[] = [
diff --git a/frontend/src/components/Assets/useAssetUploader.ts b/frontend/src/components/Assets/useAssetUploader.ts
index ae29819..9aae229 100644
--- a/frontend/src/components/Assets/useAssetUploader.ts
+++ b/frontend/src/components/Assets/useAssetUploader.ts
@@ -1,15 +1,15 @@
import axios from 'axios';
import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
+import { logger } from '../../lib/logger';
+import {
+ isAudioMimeType,
+ isVideoMimeType,
+ probeMediaDuration,
+} from '../../lib/mediaDuration';
import FileUploader from '../Uploaders/UploadService';
import type { AssetSection } from './AssetSectionCard';
import type { UploadQueueItem } from './UploadProgressList';
-import { logger } from '../../lib/logger';
-import {
- probeMediaDuration,
- isVideoMimeType,
- isAudioMimeType,
-} from '../../lib/mediaDuration';
interface UseAssetUploaderOptions {
selectedProjectId: string;
diff --git a/frontend/src/components/BackdropPortal.tsx b/frontend/src/components/BackdropPortal.tsx
index 7f417bc..cce433c 100644
--- a/frontend/src/components/BackdropPortal.tsx
+++ b/frontend/src/components/BackdropPortal.tsx
@@ -11,11 +11,11 @@
import React, {
createContext,
+ useCallback,
useContext,
+ useMemo,
useRef,
useState,
- useCallback,
- useMemo,
} from 'react';
interface BackdropItem {
diff --git a/frontend/src/components/BaseButton.tsx b/frontend/src/components/BaseButton.tsx
index 427dec0..f91a00e 100644
--- a/frontend/src/components/BaseButton.tsx
+++ b/frontend/src/components/BaseButton.tsx
@@ -1,9 +1,9 @@
-import React from 'react';
import Link from 'next/link';
+import React from 'react';
import { getButtonColor } from '../colors';
-import BaseIcon from './BaseIcon';
-import type { ColorButtonKey } from '../types/ui';
import { useAppSelector } from '../stores/hooks';
+import type { ColorButtonKey } from '../types/ui';
+import BaseIcon from './BaseIcon';
type Props = {
label?: string;
diff --git a/frontend/src/components/BaseButtons.tsx b/frontend/src/components/BaseButtons.tsx
index bff8011..eed306b 100644
--- a/frontend/src/components/BaseButtons.tsx
+++ b/frontend/src/components/BaseButtons.tsx
@@ -1,5 +1,5 @@
-import { Children, cloneElement, ReactElement } from 'react';
import type { ReactNode } from 'react';
+import { Children, cloneElement, isValidElement } from 'react';
type Props = {
type?: string;
@@ -24,13 +24,15 @@ const BaseButtons = ({
noWrap ? 'flex-nowrap' : 'flex-wrap'
}`}
>
- {Children.map(children, (child: ReactElement) =>
- child
- ? cloneElement(child as ReactElement<{ className?: string }>, {
- className: `${classAddon} ${(child.props as { className?: string }).className || ''}`,
- })
- : null,
- )}
+ {Children.map(children, (child) => {
+ if (!isValidElement<{ className?: string }>(child)) {
+ return child;
+ }
+
+ return cloneElement(child, {
+ className: `${classAddon} ${child.props.className || ''}`,
+ });
+ })}
);
};
diff --git a/frontend/src/components/BaseDivider.tsx b/frontend/src/components/BaseDivider.tsx
index 52e7f29..c14b124 100644
--- a/frontend/src/components/BaseDivider.tsx
+++ b/frontend/src/components/BaseDivider.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import { useAppSelector } from '../stores/hooks';
type Props = {
navBar?: boolean;
diff --git a/frontend/src/components/BaseIcon.tsx b/frontend/src/components/BaseIcon.tsx
index d26fe1c..bdf19b4 100644
--- a/frontend/src/components/BaseIcon.tsx
+++ b/frontend/src/components/BaseIcon.tsx
@@ -1,4 +1,4 @@
-import React, { ReactNode } from 'react';
+import { ReactNode } from 'react';
type Props = {
path: string;
diff --git a/frontend/src/components/CanvasLoadingSpinner.tsx b/frontend/src/components/CanvasLoadingSpinner.tsx
index ace5cad..a5c7c17 100644
--- a/frontend/src/components/CanvasLoadingSpinner.tsx
+++ b/frontend/src/components/CanvasLoadingSpinner.tsx
@@ -6,7 +6,7 @@
* Includes delay to avoid flashing for quick operations.
*/
-import React, { useState, useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import { PRELOAD_CONFIG } from '../config/preload.config';
interface CanvasLoadingSpinnerProps {
diff --git a/frontend/src/components/CardBox.tsx b/frontend/src/components/CardBox.tsx
index dea7269..b822a9f 100644
--- a/frontend/src/components/CardBox.tsx
+++ b/frontend/src/components/CardBox.tsx
@@ -1,7 +1,7 @@
import React, { ReactNode } from 'react';
+import { useAppSelector } from '../stores/hooks';
import CardBoxComponentBody from './CardBoxComponentBody';
import CardBoxComponentFooter from './CardBoxComponentFooter';
-import { useAppSelector } from '../stores/hooks';
type Props = {
rounded?: string;
@@ -20,7 +20,6 @@ type Props = {
};
export default function CardBox({
- rounded = 'rounded',
flex = 'flex-col',
className = '',
hasComponentLayout = false,
@@ -28,7 +27,6 @@ export default function CardBox({
hasTable = false,
isHoverable = false,
isList = false,
- isModal = false,
children,
footer,
id = '',
diff --git a/frontend/src/components/CardBoxComponentBody.tsx b/frontend/src/components/CardBoxComponentBody.tsx
index 12448d8..e1c0cd9 100644
--- a/frontend/src/components/CardBoxComponentBody.tsx
+++ b/frontend/src/components/CardBoxComponentBody.tsx
@@ -1,4 +1,4 @@
-import React, { ReactNode } from 'react';
+import { ReactNode } from 'react';
type Props = {
noPadding?: boolean;
diff --git a/frontend/src/components/CardBoxComponentEmpty.tsx b/frontend/src/components/CardBoxComponentEmpty.tsx
index c9072bb..e6545e4 100644
--- a/frontend/src/components/CardBoxComponentEmpty.tsx
+++ b/frontend/src/components/CardBoxComponentEmpty.tsx
@@ -1,5 +1,3 @@
-import React from 'react';
-
const CardBoxComponentEmpty = () => {
return (
diff --git a/frontend/src/components/CardBoxComponentFooter.tsx b/frontend/src/components/CardBoxComponentFooter.tsx
index 184a058..dc8ebc8 100644
--- a/frontend/src/components/CardBoxComponentFooter.tsx
+++ b/frontend/src/components/CardBoxComponentFooter.tsx
@@ -1,4 +1,4 @@
-import React, { ReactNode } from 'react';
+import { ReactNode } from 'react';
type Props = {
className?: string;
diff --git a/frontend/src/components/CardBoxComponentTitle.tsx b/frontend/src/components/CardBoxComponentTitle.tsx
index 20990e6..d51cea4 100644
--- a/frontend/src/components/CardBoxComponentTitle.tsx
+++ b/frontend/src/components/CardBoxComponentTitle.tsx
@@ -1,4 +1,4 @@
-import React, { ReactNode } from 'react';
+import { ReactNode } from 'react';
type Props = {
title: string;
diff --git a/frontend/src/components/CardBoxModal.tsx b/frontend/src/components/CardBoxModal.tsx
index f72f320..2e7db2b 100644
--- a/frontend/src/components/CardBoxModal.tsx
+++ b/frontend/src/components/CardBoxModal.tsx
@@ -1,5 +1,5 @@
import { mdiClose } from '@mdi/js';
-import { ReactNode } from 'react';
+import type { MouseEvent, ReactNode } from 'react';
import type { ColorButtonKey } from '../types/ui';
import BaseButton from './BaseButton';
import BaseButtons from './BaseButtons';
@@ -32,12 +32,20 @@ const CardBoxModal = ({
return null;
}
+ const handleConfirm = (_event: MouseEvent) => {
+ onConfirm();
+ };
+
+ const handleCancel = (_event: MouseEvent) => {
+ onCancel?.();
+ };
+
const footer = (
{!!onCancel && (
@@ -45,7 +53,7 @@ const CardBoxModal = ({
label='Cancel'
color={buttonColor}
outline
- onClick={onCancel}
+ onClick={handleCancel}
/>
)}
@@ -53,7 +61,7 @@ const CardBoxModal = ({
return (
diff --git a/frontend/src/components/ClickOutside.tsx b/frontend/src/components/ClickOutside.tsx
index 4e031ad..88773d8 100644
--- a/frontend/src/components/ClickOutside.tsx
+++ b/frontend/src/components/ClickOutside.tsx
@@ -1,15 +1,15 @@
-import React, {
+import {
+ MutableRefObject,
+ ReactNode,
useCallback,
useEffect,
useRef,
- ReactNode,
- MutableRefObject,
} from 'react';
interface ClickOutsideProps {
children?: ReactNode;
onClickOutside: () => void;
- excludedElements: MutableRefObject[];
+ excludedElements: MutableRefObject[];
}
const ClickOutside = ({
@@ -17,19 +17,21 @@ const ClickOutside = ({
onClickOutside,
excludedElements,
}: ClickOutsideProps) => {
- const wrapperRef = useRef(null);
-
+ const wrapperRef = useRef(null);
const handleClickOutside = useCallback(
- (event) => {
+ (event: MouseEvent) => {
+ const target = event.target;
+ if (!(target instanceof Node)) return;
+
if (
wrapperRef.current &&
- !wrapperRef.current.contains(event.target) &&
- !excludedElements.some((el) => el.current.contains(event.target))
+ !wrapperRef.current.contains(target) &&
+ !excludedElements.some((el) => el.current?.contains(target))
) {
onClickOutside();
}
},
- [wrapperRef, onClickOutside, ...excludedElements],
+ [excludedElements, onClickOutside],
);
useEffect(() => {
diff --git a/frontend/src/components/Constructor/AssetSelectCompact.tsx b/frontend/src/components/Constructor/AssetSelectCompact.tsx
index f552875..75c6b41 100644
--- a/frontend/src/components/Constructor/AssetSelectCompact.tsx
+++ b/frontend/src/components/Constructor/AssetSelectCompact.tsx
@@ -6,8 +6,8 @@
*/
import React from 'react';
-import type { AssetOption } from './types';
import { addFallbackAssetOption } from '../../lib/constructorHelpers';
+import type { AssetOption } from './types';
interface AssetSelectCompactProps {
label: string;
diff --git a/frontend/src/components/Constructor/BackgroundSettingsEditor.tsx b/frontend/src/components/Constructor/BackgroundSettingsEditor.tsx
index 5531109..e0ce040 100644
--- a/frontend/src/components/Constructor/BackgroundSettingsEditor.tsx
+++ b/frontend/src/components/Constructor/BackgroundSettingsEditor.tsx
@@ -7,12 +7,12 @@
*/
import React from 'react';
+import { addFallbackAssetOption } from '../../lib/constructorHelpers';
import type {
AssetOption,
- VideoPlaybackSettings,
AudioPlaybackSettings,
+ VideoPlaybackSettings,
} from './types';
-import { addFallbackAssetOption } from '../../lib/constructorHelpers';
interface BackgroundSettingsEditorProps {
type: 'image' | 'video' | 'embed' | 'audio';
diff --git a/frontend/src/components/Constructor/CanvasBackground.tsx b/frontend/src/components/Constructor/CanvasBackground.tsx
index dfd0447..c245ec4 100644
--- a/frontend/src/components/Constructor/CanvasBackground.tsx
+++ b/frontend/src/components/Constructor/CanvasBackground.tsx
@@ -6,21 +6,20 @@
* Supports custom video playback settings (autoplay, loop, muted, start/end time).
*/
-import React, { useEffect, useState, useMemo, useCallback } from 'react';
-import { useBackgroundVideoPlayback } from '../../hooks/useBackgroundVideoPlayback';
-import { useBackgroundAudioPlayback } from '../../hooks/useBackgroundAudioPlayback';
-import PreviousBackgroundOverlay from '../PreviousBackgroundOverlay';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { baseURLApi } from '../../config';
+import { useBackgroundAudioPlayback } from '../../hooks/useBackgroundAudioPlayback';
+import { useBackgroundVideoPlayback } from '../../hooks/useBackgroundVideoPlayback';
import { buildChromeFreeEmbedUrl } from '../../lib/embedUrl';
-import CanvasBackgroundAudioLayer from './CanvasBackgroundAudioLayer';
-import CanvasBackgroundEmbedLayer from './CanvasBackgroundEmbedLayer';
+import PreviousBackgroundOverlay from '../PreviousBackgroundOverlay';
import {
getActiveCanvasVideoUrl,
getCanvasVideoSrc,
- isBlobUrl,
scheduleAfterPaint,
shouldUseNativeVideoLoop,
} from './CanvasBackground.helpers';
+import CanvasBackgroundAudioLayer from './CanvasBackgroundAudioLayer';
+import CanvasBackgroundEmbedLayer from './CanvasBackgroundEmbedLayer';
import CanvasBackgroundImageLayer from './CanvasBackgroundImageLayer';
import CanvasBackgroundVideoLayer from './CanvasBackgroundVideoLayer';
import { useCanvasBackgroundImageReady } from './useCanvasBackgroundImageReady';
diff --git a/frontend/src/components/Constructor/CanvasBackgroundImageLayer.tsx b/frontend/src/components/Constructor/CanvasBackgroundImageLayer.tsx
index 4426ac1..4b8de67 100644
--- a/frontend/src/components/Constructor/CanvasBackgroundImageLayer.tsx
+++ b/frontend/src/components/Constructor/CanvasBackgroundImageLayer.tsx
@@ -1,5 +1,5 @@
-import React from 'react';
import NextImage from 'next/image';
+import React from 'react';
import { isBlobUrl } from './CanvasBackground.helpers';
interface CanvasBackgroundImageLayerProps {
diff --git a/frontend/src/components/Constructor/CanvasElement.tsx b/frontend/src/components/Constructor/CanvasElement.tsx
index adec4cf..71d7bc0 100644
--- a/frontend/src/components/Constructor/CanvasElement.tsx
+++ b/frontend/src/components/Constructor/CanvasElement.tsx
@@ -8,20 +8,20 @@
*/
import React, { useCallback } from 'react';
-import UiElementRenderer from '../UiElements/UiElementRenderer';
-import { useElementEffects } from '../../hooks/useElementEffects';
import { useAudioEffects } from '../../hooks/useAudioEffects';
+import { useElementEffects } from '../../hooks/useElementEffects';
+import type { PreloadCacheProvider } from '../../hooks/video';
import {
- buildTransitionStyle,
buildAppearAnimationStyle,
+ buildTransitionStyle,
hasAnyEffects,
type ElementEffectProperties,
} from '../../lib/elementEffects';
+import { normalizeZIndexValue } from '../../lib/elementStyles';
+import { isInfoPanelElementType } from '../../lib/elementTypeGuards';
import type { CanvasElement as CanvasElementType } from '../../types/constructor';
import type { ResolvedTransitionSettings } from '../../types/transition';
-import type { PreloadCacheProvider } from '../../hooks/video';
-import { isInfoPanelElementType } from '../../lib/elementTypeGuards';
-import { normalizeZIndexValue } from '../../lib/elementStyles';
+import UiElementRenderer from '../UiElements/UiElementRenderer';
interface CanvasElementProps {
element: CanvasElementType;
@@ -180,6 +180,7 @@ const CanvasElement: React.FC = ({
// Check if we need the inner wrapper for effects
const needsEffectWrapper = !isEditMode && hasAnyEffects(effectProperties);
+ const { onMouseDown: eventMouseDown, ...outerEventHandlers } = eventHandlers;
return (
= ({
data-constructor-element-id={element.id}
className='absolute cursor-pointer'
style={positionStyle}
- onMouseDown={isEditMode ? onMouseDown : undefined}
+ onMouseDown={
+ isEditMode
+ ? onMouseDown
+ : !needsEffectWrapper
+ ? eventMouseDown
+ : undefined
+ }
onClick={handleClick}
onKeyDown={handleKeyDown}
aria-disabled={isDisabled}
- {...(!isEditMode && !needsEffectWrapper ? eventHandlers : {})}
+ {...(!isEditMode && !needsEffectWrapper ? outerEventHandlers : {})}
>
{needsEffectWrapper ? (
// Inner wrapper handles hover/focus/active effects independently from animation
diff --git a/frontend/src/components/Constructor/ConstructorCanvasElementsLayer.tsx b/frontend/src/components/Constructor/ConstructorCanvasElementsLayer.tsx
index a04afbb..a5fc259 100644
--- a/frontend/src/components/Constructor/ConstructorCanvasElementsLayer.tsx
+++ b/frontend/src/components/Constructor/ConstructorCanvasElementsLayer.tsx
@@ -1,15 +1,15 @@
import { mdiPlus } from '@mdi/js';
import type { CSSProperties, MouseEvent } from 'react';
-import BaseButton from '../BaseButton';
-import CanvasElementComponent from './CanvasElement';
+import type { PreloadCacheProvider } from '../../hooks/video';
+import { isElementFlagEnabled } from '../../lib/elementFlags';
+import {
+ isInfoPanelElementType,
+ isNavigationElementType,
+} from '../../lib/elementTypeGuards';
import type { CanvasElement } from '../../types/constructor';
import type { ResolvedTransitionSettings } from '../../types/transition';
-import type { PreloadCacheProvider } from '../../hooks/video';
-import {
- isNavigationElementType,
- isInfoPanelElementType,
-} from '../../lib/elementTypeGuards';
-import { isElementFlagEnabled } from '../../lib/elementFlags';
+import BaseButton from '../BaseButton';
+import CanvasElementComponent from './CanvasElement';
import { shouldRenderConstructorCanvasElement } from './constructorPage.helpers';
type ConstructorCanvasElementsLayerProps = {
@@ -22,7 +22,7 @@ type ConstructorCanvasElementsLayerProps = {
letterboxStyles?: CSSProperties;
pageTransitionSettings: ResolvedTransitionSettings;
preloadCache: PreloadCacheProvider;
- resolveUrl: (url: string) => string;
+ resolveUrl: (url: string | undefined) => string;
isElementVisible: (element: CanvasElement) => boolean;
isInfoPanelOpen: (elementId: string) => boolean;
onCreateFirstPage: () => void;
@@ -31,7 +31,7 @@ type ConstructorCanvasElementsLayerProps = {
onGalleryCardClick: (element: CanvasElement, cardIndex: number) => void;
onCarouselButtonPositionChange: (
elementId: string,
- button: 'prev' | 'next' | 'back',
+ button: 'prev' | 'next',
x: number,
y: number,
) => void;
diff --git a/frontend/src/components/Constructor/ConstructorCanvasStage.helpers.test.ts b/frontend/src/components/Constructor/ConstructorCanvasStage.helpers.test.ts
index efb9e84..cdddd2c 100644
--- a/frontend/src/components/Constructor/ConstructorCanvasStage.helpers.test.ts
+++ b/frontend/src/components/Constructor/ConstructorCanvasStage.helpers.test.ts
@@ -1,11 +1,11 @@
import assert from 'node:assert/strict';
import test from 'node:test';
+import type { TransitionPreviewState } from '../../types/presentation';
import {
shouldShowConstructorCanvasElements,
shouldShowConstructorCanvasSpinner,
} from './ConstructorCanvasStage.helpers';
-import type { TransitionPreviewState } from '../../types/presentation';
const transitionPreview = {
videoUrl: 'assets/transition.mp4',
diff --git a/frontend/src/components/Constructor/ConstructorCanvasStage.tsx b/frontend/src/components/Constructor/ConstructorCanvasStage.tsx
index e889c78..dbe657d 100644
--- a/frontend/src/components/Constructor/ConstructorCanvasStage.tsx
+++ b/frontend/src/components/Constructor/ConstructorCanvasStage.tsx
@@ -1,14 +1,7 @@
import type { CSSProperties, MouseEvent, PointerEvent, RefObject } from 'react';
-import { BackdropPortalProvider } from '../BackdropPortal';
-import CanvasLoadingSpinner from '../CanvasLoadingSpinner';
-import RuntimeControls from '../Runtime/RuntimeControls';
-import CanvasBackground from './CanvasBackground';
-import ConstructorCanvasElementsLayer from './ConstructorCanvasElementsLayer';
-import ElementEditorPanel from './ElementEditorPanel';
-import TransitionBlackOverlay from '../TransitionBlackOverlay';
-import { isSafari } from '../../lib/browserUtils';
import type { PreloadCacheProvider } from '../../hooks/video';
+import { isSafari } from '../../lib/browserUtils';
import type { CanvasElement } from '../../types/constructor';
import type { TourPage } from '../../types/entities';
import type { TransitionPreviewState } from '../../types/presentation';
@@ -17,10 +10,17 @@ import type {
ResolvedUiControlsSettings,
SystemUiControlType,
} from '../../types/uiControls';
+import { BackdropPortalProvider } from '../BackdropPortal';
+import CanvasLoadingSpinner from '../CanvasLoadingSpinner';
+import RuntimeControls from '../Runtime/RuntimeControls';
+import TransitionBlackOverlay from '../TransitionBlackOverlay';
+import CanvasBackground from './CanvasBackground';
+import ConstructorCanvasElementsLayer from './ConstructorCanvasElementsLayer';
import {
shouldShowConstructorCanvasElements,
shouldShowConstructorCanvasSpinner,
} from './ConstructorCanvasStage.helpers';
+import ElementEditorPanel from './ElementEditorPanel';
type Position = {
x: number;
@@ -71,11 +71,11 @@ type ConstructorCanvasStageProps = {
videoAutoplay: boolean;
videoLoop: boolean;
videoMuted: boolean;
- videoStartTime?: number;
- videoEndTime?: number;
+ videoStartTime?: number | null;
+ videoEndTime?: number | null;
audioLoop: boolean;
- audioStartTime?: number;
- audioEndTime?: number;
+ audioStartTime?: number | null;
+ audioEndTime?: number | null;
};
soundControl: {
isMuted: boolean;
@@ -110,7 +110,7 @@ type ConstructorCanvasStageProps = {
onGalleryCardClick: (element: CanvasElement, cardIndex: number) => void;
onCarouselButtonPositionChange: (
elementId: string,
- button: 'prev' | 'next' | 'back',
+ button: 'prev' | 'next',
x: number,
y: number,
) => void;
diff --git a/frontend/src/components/Constructor/ConstructorPageModals.tsx b/frontend/src/components/Constructor/ConstructorPageModals.tsx
index ea37982..906e4bf 100644
--- a/frontend/src/components/Constructor/ConstructorPageModals.tsx
+++ b/frontend/src/components/Constructor/ConstructorPageModals.tsx
@@ -1,6 +1,6 @@
import CardBoxModal from '../CardBoxModal';
-import CreatePageModal from './CreatePageModal';
import { getConstructorDeletePageName } from './ConstructorPageModals.helpers';
+import CreatePageModal from './CreatePageModal';
type ConstructorPageModalsProps = {
isCreatePageModalActive: boolean;
diff --git a/frontend/src/components/Constructor/ConstructorRuntimeOverlays.helpers.test.ts b/frontend/src/components/Constructor/ConstructorRuntimeOverlays.helpers.test.ts
index 679e300..28c59cf 100644
--- a/frontend/src/components/Constructor/ConstructorRuntimeOverlays.helpers.test.ts
+++ b/frontend/src/components/Constructor/ConstructorRuntimeOverlays.helpers.test.ts
@@ -1,11 +1,11 @@
import assert from 'node:assert/strict';
import test from 'node:test';
+import type { InfoPanelImage } from '../../types/infoPanel';
import {
getConstructorInfoPanelDetailImage,
shouldRenderConstructorImageDetailPanel,
} from './ConstructorRuntimeOverlays.helpers';
-import type { InfoPanelImage } from '../../types/infoPanel';
const image = { id: 'image-1', url: 'assets/image.jpg' } as InfoPanelImage;
diff --git a/frontend/src/components/Constructor/ConstructorRuntimeOverlays.helpers.ts b/frontend/src/components/Constructor/ConstructorRuntimeOverlays.helpers.ts
index 4129039..19201d4 100644
--- a/frontend/src/components/Constructor/ConstructorRuntimeOverlays.helpers.ts
+++ b/frontend/src/components/Constructor/ConstructorRuntimeOverlays.helpers.ts
@@ -4,7 +4,7 @@ export const getConstructorInfoPanelDetailImage = ({
activeDetailImages,
panelId,
}: {
- activeDetailImages: Record
;
+ activeDetailImages: Record;
panelId: string;
}) => activeDetailImages[panelId];
@@ -12,6 +12,6 @@ export const shouldRenderConstructorImageDetailPanel = ({
image,
isEditMode,
}: {
- image?: InfoPanelImage;
+ image?: InfoPanelImage | null;
isEditMode: boolean;
}) => Boolean(image) || isEditMode;
diff --git a/frontend/src/components/Constructor/ConstructorRuntimeOverlays.tsx b/frontend/src/components/Constructor/ConstructorRuntimeOverlays.tsx
index 6d92e9d..268fe1a 100644
--- a/frontend/src/components/Constructor/ConstructorRuntimeOverlays.tsx
+++ b/frontend/src/components/Constructor/ConstructorRuntimeOverlays.tsx
@@ -1,14 +1,14 @@
import { Fragment, type CSSProperties } from 'react';
-import GalleryCarouselOverlay from '../UiElements/GalleryCarouselOverlay';
-import ImageDetailPanel from '../UiElements/ImageDetailPanel';
-import InfoPanelOverlay from '../UiElements/InfoPanelOverlay';
import type {
CanvasElement,
GalleryCarouselMediaItem,
} from '../../types/constructor';
import type { InfoPanelImage } from '../../types/infoPanel';
import type { ResolvedTransitionSettings } from '../../types/transition';
+import GalleryCarouselOverlay from '../UiElements/GalleryCarouselOverlay';
+import ImageDetailPanel from '../UiElements/ImageDetailPanel';
+import InfoPanelOverlay from '../UiElements/InfoPanelOverlay';
import {
getConstructorInfoPanelDetailImage,
shouldRenderConstructorImageDetailPanel,
@@ -32,7 +32,7 @@ type ConstructorRuntimeOverlaysProps = {
activeInfoPanelGalleryElement: CanvasElement | null;
shouldShowInfoPanelOverlays: boolean;
infoPanelElementsToRender: CanvasElement[];
- activeDetailImages: Record;
+ activeDetailImages: Record;
isEditMode: boolean;
letterboxStyles: CSSProperties;
cssVars: CSSProperties;
@@ -42,7 +42,7 @@ type ConstructorRuntimeOverlaysProps = {
onCloseInfoPanelGallery: () => void;
onCloseAllInfoPanels: () => void;
onCloseInfoPanel: (panelId: string) => void;
- onSetDetailImage: (panelId: string, image: InfoPanelImage) => void;
+ onSetDetailImage: (panelId: string, image: InfoPanelImage | null) => void;
onCloseDetailImage: (panelId: string) => void;
onOpenInfoPanelGallery: (
panelId: string,
@@ -220,7 +220,7 @@ const ConstructorRuntimeOverlays = ({
}) && (
onCloseDetailImage(infoPanelElementToRender.id)}
resolveUrl={resolveUrl}
letterboxStyles={letterboxStyles}
diff --git a/frontend/src/components/Constructor/ConstructorToolbar.helpers.test.ts b/frontend/src/components/Constructor/ConstructorToolbar.helpers.test.ts
index 0e5be2a..b1e0b7c 100644
--- a/frontend/src/components/Constructor/ConstructorToolbar.helpers.test.ts
+++ b/frontend/src/components/Constructor/ConstructorToolbar.helpers.test.ts
@@ -1,12 +1,12 @@
import assert from 'node:assert/strict';
import test from 'node:test';
-import type { TourPage } from './types';
import {
getCollapsedToolbarPageName,
- getConstructorToolbarMaxWidth,
getConstructorToolbarActionState,
+ getConstructorToolbarMaxWidth,
sortToolbarPages,
} from './ConstructorToolbar.helpers';
+import type { TourPage } from './types';
const makePage = (id: string, name: string, sortOrder?: number): TourPage =>
({
diff --git a/frontend/src/components/Constructor/ConstructorToolbar.tsx b/frontend/src/components/Constructor/ConstructorToolbar.tsx
index c33d944..3b6ebdd 100644
--- a/frontend/src/components/Constructor/ConstructorToolbar.tsx
+++ b/frontend/src/components/Constructor/ConstructorToolbar.tsx
@@ -5,19 +5,19 @@
* Glassmorphism styling with draggable positioning.
*/
-import { useState, useRef, useEffect, forwardRef } from 'react';
import { mdiDotsVertical } from '@mdi/js';
+import { forwardRef, useEffect, useRef, useState } from 'react';
import BaseIcon from '../BaseIcon';
-import InteractionModeToggle from './InteractionModeToggle';
-import ConstructorToolbarCollapsed from './ConstructorToolbarCollapsed';
-import ConstructorToolbarElementActions from './ConstructorToolbarElementActions';
-import ConstructorToolbarPageActions from './ConstructorToolbarPageActions';
-import ConstructorToolbarSaveControls from './ConstructorToolbarSaveControls';
import {
getConstructorToolbarActionState,
getConstructorToolbarMaxWidth,
type ToolbarDropdown,
} from './ConstructorToolbar.helpers';
+import ConstructorToolbarCollapsed from './ConstructorToolbarCollapsed';
+import ConstructorToolbarElementActions from './ConstructorToolbarElementActions';
+import ConstructorToolbarPageActions from './ConstructorToolbarPageActions';
+import ConstructorToolbarSaveControls from './ConstructorToolbarSaveControls';
+import InteractionModeToggle from './InteractionModeToggle';
import type { ConstructorToolbarProps } from './types';
const ConstructorToolbar = forwardRef(
diff --git a/frontend/src/components/Constructor/ConstructorToolbarCollapsed.tsx b/frontend/src/components/Constructor/ConstructorToolbarCollapsed.tsx
index ba100e2..f4e5e72 100644
--- a/frontend/src/components/Constructor/ConstructorToolbarCollapsed.tsx
+++ b/frontend/src/components/Constructor/ConstructorToolbarCollapsed.tsx
@@ -1,8 +1,8 @@
+import { mdiChevronRight, mdiDotsVertical } from '@mdi/js';
import type { ForwardedRef, MouseEvent } from 'react';
import BaseIcon from '../BaseIcon';
-import { mdiChevronRight, mdiDotsVertical } from '@mdi/js';
-import type { Position, TourPage } from './types';
import { getCollapsedToolbarPageName } from './ConstructorToolbar.helpers';
+import type { Position, TourPage } from './types';
interface Props {
position: Position;
diff --git a/frontend/src/components/Constructor/ConstructorToolbarElementActions.tsx b/frontend/src/components/Constructor/ConstructorToolbarElementActions.tsx
index 067d989..927b0df 100644
--- a/frontend/src/components/Constructor/ConstructorToolbarElementActions.tsx
+++ b/frontend/src/components/Constructor/ConstructorToolbarElementActions.tsx
@@ -1,4 +1,3 @@
-import type { RefObject } from 'react';
import {
mdiChevronDown,
mdiContentDuplicate,
@@ -12,10 +11,11 @@ import {
mdiVideo,
mdiViewCarousel,
} from '@mdi/js';
+import type { RefObject } from 'react';
+import type { CanvasElementType } from '../../types/constructor';
import BaseIcon from '../BaseIcon';
import ClickOutside from '../ClickOutside';
import MenuActionButton from './MenuActionButton';
-import type { CanvasElementType } from '../../types/constructor';
import type { NavigationElementType } from './types';
interface Props {
diff --git a/frontend/src/components/Constructor/ConstructorToolbarLayer.helpers.test.ts b/frontend/src/components/Constructor/ConstructorToolbarLayer.helpers.test.ts
index 189f02c..8975e01 100644
--- a/frontend/src/components/Constructor/ConstructorToolbarLayer.helpers.test.ts
+++ b/frontend/src/components/Constructor/ConstructorToolbarLayer.helpers.test.ts
@@ -1,12 +1,12 @@
import assert from 'node:assert/strict';
import test from 'node:test';
+import type { TourPage } from '../../types/entities';
import {
findConstructorToolbarPage,
getConstructorExitHref,
shouldShowConstructorToolbar,
} from './ConstructorToolbarLayer.helpers';
-import type { TourPage } from '../../types/entities';
test('shouldShowConstructorToolbar requires pages and normal constructor mode', () => {
assert.equal(
diff --git a/frontend/src/components/Constructor/ConstructorToolbarLayer.tsx b/frontend/src/components/Constructor/ConstructorToolbarLayer.tsx
index c1ded6a..2a4c072 100644
--- a/frontend/src/components/Constructor/ConstructorToolbarLayer.tsx
+++ b/frontend/src/components/Constructor/ConstructorToolbarLayer.tsx
@@ -1,17 +1,17 @@
import type { RefObject } from 'react';
+import type { NavigationElementType } from '../../context/ConstructorContext';
+import type {
+ CanvasElementType,
+ EditorMenuItem,
+} from '../../types/constructor';
+import type { TourPage } from '../../types/entities';
import ConstructorToolbar from './ConstructorToolbar';
import {
findConstructorToolbarPage,
getConstructorExitHref,
shouldShowConstructorToolbar,
} from './ConstructorToolbarLayer.helpers';
-import type {
- CanvasElementType,
- EditorMenuItem,
-} from '../../types/constructor';
-import type { NavigationElementType } from '../../context/ConstructorContext';
-import type { TourPage } from '../../types/entities';
import type { ConstructorInteractionMode, Position } from './types';
type ConstructorToolbarLayerProps = {
@@ -19,7 +19,7 @@ type ConstructorToolbarLayerProps = {
position: Position;
isElementEditMode: boolean;
pages: TourPage[];
- activePageId: string;
+ activePageId: string | null;
projectId: string;
isReorderingPages: boolean;
isSaving: boolean;
@@ -100,7 +100,7 @@ const ConstructorToolbarLayer = ({
position={position}
onDragStart={onDragStart}
pages={pages}
- activePageId={activePageId}
+ activePageId={activePageId || ''}
onPageChange={(pageId) => {
const page = findConstructorToolbarPage(pages, pageId);
if (page) onSwitchToPage(page);
diff --git a/frontend/src/components/Constructor/ConstructorToolbarPageActions.tsx b/frontend/src/components/Constructor/ConstructorToolbarPageActions.tsx
index 20be375..5ed0e7a 100644
--- a/frontend/src/components/Constructor/ConstructorToolbarPageActions.tsx
+++ b/frontend/src/components/Constructor/ConstructorToolbarPageActions.tsx
@@ -1,4 +1,3 @@
-import type { RefObject } from 'react';
import {
mdiChevronDown,
mdiChevronUp,
@@ -10,11 +9,12 @@ import {
mdiPlus,
mdiVideo,
} from '@mdi/js';
+import type { RefObject } from 'react';
+import type { EditorMenuItem } from '../../types/constructor';
import BaseIcon from '../BaseIcon';
import ClickOutside from '../ClickOutside';
import MenuActionButton from './MenuActionButton';
import PageSelector from './PageSelector';
-import type { EditorMenuItem } from '../../types/constructor';
import type { ConstructorToolbarProps } from './types';
interface Props {
diff --git a/frontend/src/components/Constructor/ConstructorToolbarSaveControls.tsx b/frontend/src/components/Constructor/ConstructorToolbarSaveControls.tsx
index 061118f..f1e349a 100644
--- a/frontend/src/components/Constructor/ConstructorToolbarSaveControls.tsx
+++ b/frontend/src/components/Constructor/ConstructorToolbarSaveControls.tsx
@@ -1,7 +1,7 @@
import { mdiChevronLeft, mdiExitToApp } from '@mdi/js';
+import dataFormatter from '../../helpers/dataFormatter';
import BaseButton from '../BaseButton';
import BaseIcon from '../BaseIcon';
-import dataFormatter from '../../helpers/dataFormatter';
interface Props {
isSaving: boolean;
diff --git a/frontend/src/components/Constructor/CreatePageModal.tsx b/frontend/src/components/Constructor/CreatePageModal.tsx
index ad3e686..3a132fb 100644
--- a/frontend/src/components/Constructor/CreatePageModal.tsx
+++ b/frontend/src/components/Constructor/CreatePageModal.tsx
@@ -5,9 +5,9 @@
* Slug is auto-generated from the page name behind the scenes.
*/
-import React, { useState, useEffect, useMemo, useCallback } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { buildUniqueSlug, sanitizeSlug } from '../../lib/slugHelpers';
import CardBoxModal from '../CardBoxModal';
-import { sanitizeSlug, buildUniqueSlug } from '../../lib/slugHelpers';
interface CreatePageModalProps {
/** Whether the modal is visible */
diff --git a/frontend/src/components/Constructor/ElementEditorCommonCssSection.tsx b/frontend/src/components/Constructor/ElementEditorCommonCssSection.tsx
index 7ca18c2..fcd7e17 100644
--- a/frontend/src/components/Constructor/ElementEditorCommonCssSection.tsx
+++ b/frontend/src/components/Constructor/ElementEditorCommonCssSection.tsx
@@ -1,8 +1,8 @@
+import type { CanvasElement } from '../../types/constructor';
import {
StyleSettingsSectionCompact,
extractNumericValue,
} from '../ElementSettings';
-import type { CanvasElement } from '../../types/constructor';
import { buildElementCssPatch } from './elementEditorPanel.helpers';
interface ElementEditorCommonCssSectionProps {
diff --git a/frontend/src/components/Constructor/ElementEditorEffectsTab.tsx b/frontend/src/components/Constructor/ElementEditorEffectsTab.tsx
index d0a83a8..5721eac 100644
--- a/frontend/src/components/Constructor/ElementEditorEffectsTab.tsx
+++ b/frontend/src/components/Constructor/ElementEditorEffectsTab.tsx
@@ -1,5 +1,5 @@
-import { EffectsSettingsSectionCompact } from '../ElementSettings';
import type { AssetOption, CanvasElement } from '../../types/constructor';
+import { EffectsSettingsSectionCompact } from '../ElementSettings';
interface ElementEditorEffectsTabProps {
selectedElement: CanvasElement;
diff --git a/frontend/src/components/Constructor/ElementEditorGalleryCssSection.tsx b/frontend/src/components/Constructor/ElementEditorGalleryCssSection.tsx
index d37937e..0b73f08 100644
--- a/frontend/src/components/Constructor/ElementEditorGalleryCssSection.tsx
+++ b/frontend/src/components/Constructor/ElementEditorGalleryCssSection.tsx
@@ -1,5 +1,5 @@
-import { GallerySectionStyleInputs } from '../ElementSettings';
import type { CanvasElement } from '../../types/constructor';
+import { GallerySectionStyleInputs } from '../ElementSettings';
interface ElementEditorGalleryCssSectionProps {
selectedElement: CanvasElement;
diff --git a/frontend/src/components/Constructor/ElementEditorGeneralTab.tsx b/frontend/src/components/Constructor/ElementEditorGeneralTab.tsx
index e2fd886..f4bd3b8 100644
--- a/frontend/src/components/Constructor/ElementEditorGeneralTab.tsx
+++ b/frontend/src/components/Constructor/ElementEditorGeneralTab.tsx
@@ -1,28 +1,3 @@
-import {
- CommonSettingsSectionCompact,
- DescriptionSettingsSectionCompact,
- MediaSettingsSectionCompact,
- GallerySettingsSectionCompact,
- CarouselSettingsSectionCompact,
- GalleryCarouselSettingsSectionCompact,
- InfoPanelSettingsSectionCompact,
-} from '../ElementSettings';
-import NavigationSettingsSectionCompact from '../ElementSettings/NavigationSettingsSectionCompact';
-import {
- normalizeAppearDelaySec,
- normalizeAppearDurationSec,
-} from '../../lib/elementDefaults';
-import {
- isNavigationElementType,
- isDescriptionElementType,
- isGalleryElementType,
- isCarouselElementType,
- isMediaElementType,
- isVideoPlayerElementType,
- isInfoPanelElementType,
-} from '../../lib/elementTypeGuards';
-import type { AssetOption, CanvasElement } from '../../types/constructor';
-import type { TourPage } from '../../types/entities';
import type {
CarouselSlideOperations,
GalleryCardOperations,
@@ -30,6 +5,31 @@ import type {
InfoPanelSectionOperations,
NavigationElementType,
} from '../../context/ConstructorContext';
+import {
+ normalizeAppearDelaySec,
+ normalizeAppearDurationSec,
+} from '../../lib/elementDefaults';
+import {
+ isCarouselElementType,
+ isDescriptionElementType,
+ isGalleryElementType,
+ isInfoPanelElementType,
+ isMediaElementType,
+ isNavigationElementType,
+ isVideoPlayerElementType,
+} from '../../lib/elementTypeGuards';
+import type { AssetOption, CanvasElement } from '../../types/constructor';
+import type { TourPage } from '../../types/entities';
+import {
+ CarouselSettingsSectionCompact,
+ CommonSettingsSectionCompact,
+ DescriptionSettingsSectionCompact,
+ GalleryCarouselSettingsSectionCompact,
+ GallerySettingsSectionCompact,
+ InfoPanelSettingsSectionCompact,
+ MediaSettingsSectionCompact,
+} from '../ElementSettings';
+import NavigationSettingsSectionCompact from '../ElementSettings/NavigationSettingsSectionCompact';
interface ElementEditorGeneralTabProps {
selectedElement: CanvasElement;
diff --git a/frontend/src/components/Constructor/ElementEditorPanel.tsx b/frontend/src/components/Constructor/ElementEditorPanel.tsx
index f05e245..c57b57d 100644
--- a/frontend/src/components/Constructor/ElementEditorPanel.tsx
+++ b/frontend/src/components/Constructor/ElementEditorPanel.tsx
@@ -9,33 +9,31 @@
import React from 'react';
import {
- useConstructorContext,
- useConstructorElements,
- useConstructorBackground,
useConstructorAssets,
+ useConstructorBackground,
useConstructorCollectionOps,
useConstructorDuration,
- useConstructorNavigation,
useConstructorEditorTab,
+ useConstructorElements,
useConstructorMenu,
+ useConstructorNavigation,
} from '../../context/ConstructorContext';
+import {
+ isGalleryElementType,
+ isInfoPanelElementType,
+} from '../../lib/elementTypeGuards';
+import type { SystemUiControlSettings } from '../../types/uiControls';
import {
ElementSettingsTabsCompact,
GallerySectionStyleInputs,
InfoPanelStyleInputs,
} from '../ElementSettings';
-import ElementEditorHeader from './ElementEditorHeader';
-import {
- isGalleryElementType,
- isCarouselElementType,
- isInfoPanelElementType,
-} from '../../lib/elementTypeGuards';
-import type { SystemUiControlSettings } from '../../types/uiControls';
import { ElementEditorBackgroundSettings } from './ElementEditorBackgroundSettings';
import { ElementEditorCommonCssSection } from './ElementEditorCommonCssSection';
import { ElementEditorEffectsTab } from './ElementEditorEffectsTab';
import { ElementEditorGalleryCssSection } from './ElementEditorGalleryCssSection';
import { ElementEditorGeneralTab } from './ElementEditorGeneralTab';
+import ElementEditorHeader from './ElementEditorHeader';
import { InfoPanelMediaStyleSection } from './InfoPanelMediaStyleSection';
import { InfoPanelTextStyleSection } from './InfoPanelTextStyleSection';
import { SystemControlSettingsEditor } from './SystemControlSettingsEditor';
@@ -74,7 +72,6 @@ export function ElementEditorPanel({
// Get state from context
const {
selectedElement,
- selectedElementId,
updateSelectedElement,
removeSelectedElement,
selectedSystemControl,
diff --git a/frontend/src/components/Constructor/InfoPanelMediaStyleSection.tsx b/frontend/src/components/Constructor/InfoPanelMediaStyleSection.tsx
index 41525ea..cc54994 100644
--- a/frontend/src/components/Constructor/InfoPanelMediaStyleSection.tsx
+++ b/frontend/src/components/Constructor/InfoPanelMediaStyleSection.tsx
@@ -1,5 +1,5 @@
-import type { CanvasElement } from '../../types/constructor';
import { FONT_OPTIONS } from '../../lib/fonts';
+import type { CanvasElement } from '../../types/constructor';
interface InfoPanelMediaStyleSectionProps {
selectedElement: CanvasElement;
diff --git a/frontend/src/components/Constructor/InfoPanelTextStyleSection.tsx b/frontend/src/components/Constructor/InfoPanelTextStyleSection.tsx
index 75ba249..95ed08b 100644
--- a/frontend/src/components/Constructor/InfoPanelTextStyleSection.tsx
+++ b/frontend/src/components/Constructor/InfoPanelTextStyleSection.tsx
@@ -1,5 +1,5 @@
-import type { CanvasElement } from '../../types/constructor';
import { FONT_OPTIONS } from '../../lib/fonts';
+import type { CanvasElement } from '../../types/constructor';
type TextAlignValue = 'left' | 'center' | 'right';
diff --git a/frontend/src/components/Constructor/SystemControlSettingsEditor.tsx b/frontend/src/components/Constructor/SystemControlSettingsEditor.tsx
index 9d3aa41..1c9c391 100644
--- a/frontend/src/components/Constructor/SystemControlSettingsEditor.tsx
+++ b/frontend/src/components/Constructor/SystemControlSettingsEditor.tsx
@@ -1,13 +1,15 @@
-import {
- getSystemControlAnchorBounds,
- type SystemUiControlSettings,
-} from '../../types/uiControls';
-import type { AssetOption } from '../../types/constructor';
import { addFallbackAssetOption } from '../../lib/constructorHelpers';
import {
opacityToPercentInput,
percentInputToOpacityNumber,
} from '../../lib/opacityPercent';
+import type { AssetOption } from '../../types/constructor';
+import {
+ DEFAULT_UI_CONTROL_SETTINGS,
+ getSystemControlAnchorBounds,
+ type SystemUiControlAnchor,
+ type SystemUiControlSettings,
+} from '../../types/uiControls';
interface SystemControlSettingsEditorProps {
settings: SystemUiControlSettings;
@@ -61,9 +63,13 @@ export function SystemControlSettingsEditor({
canvasAspectRatio,
onChange,
}: SystemControlSettingsEditorProps) {
+ const effectiveSettings = {
+ ...DEFAULT_UI_CONTROL_SETTINGS.fullscreen,
+ ...settings,
+ };
const bounds = getSystemControlAnchorBounds(
- settings.anchor,
- settings.buttonSizePercent,
+ effectiveSettings.anchor,
+ effectiveSettings.buttonSizePercent,
canvasAspectRatio,
);
@@ -79,7 +85,7 @@ export function SystemControlSettingsEditor({
min={bounds.minX}
max={bounds.maxX}
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
- value={settings.xPercent}
+ value={effectiveSettings.xPercent}
onChange={(event) =>
onChange({
xPercent: clamp(
@@ -100,7 +106,7 @@ export function SystemControlSettingsEditor({
min={bounds.minY}
max={bounds.maxY}
className='w-full rounded border border-gray-300 px-2 py-1 text-xs'
- value={settings.yPercent}
+ value={effectiveSettings.yPercent}
onChange={(event) =>
onChange({
yPercent: clamp(
@@ -118,25 +124,24 @@ export function SystemControlSettingsEditor({